Home » blog » How to Monitor a NestJS App in Production

How to Monitor a NestJS App in Production

NestJS প্রোডাকশনে (Production) যখন কোনো অ্যাপ্লিকেশন ক্র্যাশ করে বা ভেঙে পড়ে, তখন আমাদের প্রথম প্রশ্নটিই থাকে “কেন এমন হলো?” শুধু বিচ্ছিন্ন কিছু লগ (Logs) দেখার চেয়ে একটি ভালো মনিটরিং সিস্টেম আপনাকে এর অনেক দ্রুত এবং সঠিক উত্তর দিতে পারে।

বেশিরভাগ অ্যাপ্লিকেশনের শুরুটা হয় কিছু সাধারণ লগ এবং একটি হেলথ চেক (Health check) দিয়ে, আর বিস্তারিত মনিটরিংয়ের কাজটা সাধারণত “ফ্রি সময়ে করব” বলে ফেলে রাখা হয়। শুরু করার জন্য এটা ঠিক আছে।

How to Monitor a NestJS App
How to Monitor a NestJS App

তবে ইনসিডেন্ট বা বড় কোনো সমস্যার সময় সবার আগে যে জিনিসটা প্রয়োজন হয়, তা হলো সঠিক কন্টেক্সট।

চলুন দেখে নিই, একটি NestJS অ্যাপ্লিকেশনে আসলে কী কী মনিটরিং থাকা উচিত এবং কোন সিরিয়ালে এগুলো যুক্ত করা ভালো।

Health checks

মনিটরিংয়ের একদম প্রাথমিক লেয়ার হলো হেলথ চেক। NestJS-এ এই কাজের জন্য দারুণ একটি প্যাকেজ রয়েছে @nestjs/terminus।

@Controller("health")
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([() => this.db.pingCheck("database")]);
  }
}

আপনার লোড ব্যালেন্সারকে (Load balancer) /health এন্ডপয়েন্টে পয়েন্ট করে রাখুন, তাহলে এটি বুঝতে পারবে কখন কোনো ইনস্ট্যান্স কাজ করা বন্ধ করে দিয়েছে।

তবে মনে রাখবেন, লোড ব্যালেন্সার শুধু এটুকুই বোঝে।

আপনার অ্যাপ হয়তো /health এর রেসপন্স ২ মিলি-সেকেন্ডে দিচ্ছে, কিন্তু অন্যান্য রিয়েল রিকোয়েস্ট প্রসেস করতে ৯ সেকেন্ড সময় নিচ্ছে লোড ব্যালেন্সারের কাছে মনে হবে সবকিছু একদম ঠিকঠাক চলছে!

একটি টিপস: আপনার মূল মেট্রিক্স বা ট্র্যাকিং থেকে এই এন্ডপয়েন্টটিকে বাদ রাখা উচিত।

কারণ প্রতি কয়েক সেকেন্ড পরপর আসা এই প্রোব (Probe) রিকোয়েস্টগুলো দিনে হাজার হাজার লগ তৈরি করে, যা আপনার আসল অ্যাপ্লিকেশনের ল্যাটেন্সি (Latency) ডেটাকে ভুলভাবে উপস্থাপন করতে পারে।

Logs

ডেভেলপমেন্টের সময় NestJS-এর বিল্ট-ইন Logger দিয়ে বেশ ভালোভাবেই কাজ চলে যায়। কিন্তু প্রোডাকশনে ConsoleLogger-এর কিছু অপশন ব্যবহার করলে অনেক সুবিধা পাওয়া যায়:

const levels = (process.env.LOG_LEVELS ?? "fatal,error,warn,log").split(
  ",",
) as LogLevel[];

const logger = new ConsoleLogger({
  json: true,
  flattenParams: true,
  logLevels: levels,
});

const app = await NestFactory.create(AppModule, { logger });

এখানে json: true ব্যবহার করলে আপনার লগ সিস্টেম রঙিন টেক্সটের বদলে প্রতি লাইনে একটি করে JSON অবজেক্ট পাবে, যা পরবর্তীতে পার্স (Parse) বা ফিল্টার করা অনেক সহজ।

Levels are a contract

কোন লগগুলো প্রিন্ট হবে তা logLevels ঠিক করে দেয়।

তবে এই লেভেলগুলো তখনই কাজে আসবে যখন টিমের সবাই এগুলোকে একই নিয়মে ব্যবহার করবে। আমি সাধারণত নিচের মতো করে লেভেলগুলো ভাগ করি:

  • fatal: যখন প্রসেস আর চলতে পারে না। যেমন: স্টার্টআপে কনফিগারেশন ভুল, বা ডেটাবেস কানেকশন লস্ট যা আর ফিরে আসবে না।
  • error: কোনো অপারেশন ফেইল করেছে এবং কারও এটি দেখা উচিত। যদি এমন কোনো ইস্যু হয় যা নিয়ে কেউ কোনো পদক্ষেপ নেবে না, তবে সেটি আসলে error নয়।
  • warn: কিছু একটা ভুল হয়েছিল, কিন্তু তা হ্যান্ডেল করা হয়েছে (যেমন: Retry, Fallback)। এগুলোর একটি-দুটি হয়তো তেমন ব্যাপার না, কিন্তু মিনিটে ১০০টি ওয়ার্নিং মানে বড় কোনো সমস্যা হতে যাচ্ছে।
  • log: গুরুত্বপূর্ণ বিজনেস ইভেন্ট। ইনসিডেন্টের সময় এগুলো খুব কাজে লাগে (যেমন: Order placed, payment captured)।
  • debug এবং verbose: প্রোডাকশনে এগুলো বন্ধ রাখুন। এগুলো দরকারি লগগুলোকে চাপা দিয়ে দেয় এবং আপনার লগিং সার্ভারের স্টোরেজ খরচ বাড়ায়।

Structured params

Nestjs 12 থেকে, মেসেজের পর পাঠানো প্লেইন অবজেক্টগুলোকে অতিরিক্ত মেসেজ হিসেবে প্রিন্ট না করে ডেটা (Data) হিসেবে ট্রিট করা হয়।

flattenParams: true ব্যবহার করলে, অবজেক্টের key-গুলো সরাসরি JSON-এর টপ লেভেলে চলে আসে:

this.logger.warn("Payment declined", { orderId, provider: "stripe", attempt });
// {"level":"warn",...,"message":"Payment declined","context":"PaymentsService","orderId":"ord_91f2","provider":"stripe","attempt":2}

এর ফলে আপনি সহজেই orderId বা provider দিয়ে লগ ফিল্টার করতে পারবেন। এটি আরও কার্যকর করতে কিছু নিয়ম মানতে পারেন:

  • মেসেজ ফিক্সড রাখুন, ভ্যারিয়েবল দিন প্যারামিটারে: “Payment declined” ৪০০ বার প্রিন্ট হলে তা একটি প্যাটার্ন। কিন্তু মেসেজের ভেতরেই ৪০০টি আলাদা orderId যুক্ত থাকলে তা ট্র্যাক করা কঠিন।
  • সবসময় Object ব্যবহার করুন, extra strings নয়: this.logger.log('Order shipped', order.id) ব্যবহার করা ঠিক নয়। এর বদলে { orderId: order.id } ব্যবহার করুন।
  • Error-এর stack ঠিক রাখুন: this.logger.error('Charge failed', { orderId }, err.stack) – এভাবে লিখলে এক লাইনেই orderId এবং stack পাওয়া যায়।
  • Framework fields win: message, level বা timestamp নাম দিয়ে কোনো প্যারামিটার পাঠাবেন না, কারণ এগুলো ফ্রেমওয়ার্ক নিজের কাজে ব্যবহার করে।
  • Ids, not objects: { user } পাস করলে ইউজারের পুরো অবজেক্ট লগে চলে যাবে, যা সিকিউরিটির জন্য ভালো নয়। এর বদলে { userId: user.id } পাস করুন।

Trace ids

বিল্ট-ইন লগার আপনাকে প্রতিটি লাইনে একটি ইউনিক trace id দিতে পারে না।

এটি ছাড়া, মিনিটে আসা হাজার হাজার লগের মধ্য থেকে একটি নির্দিষ্ট ফেইল হওয়া রিকোয়েস্টের লগ খুঁজে বের করা খড়ের গাদায় সুই খোঁজার মতো।

হাতে তৈরি কোনো middleware দিয়ে HTTP রিকোয়েস্টের জন্য আইডি তৈরি করা যায় ঠিকই, কিন্তু queue jobs, crons বা microservice কলগুলোর ক্ষেত্রে তা কাজ করে না। ConsoleLogger নিজে জানে না সে কোন অপারেশনের জন্য লগ লিখছে।

এ জন্যই @nestjs/observe এর মতো টুল ব্যবহার করা হয়, যা কনসোল লগারের সাথে যুক্ত হয়ে প্রতিটি traced অপারেশনের লগে একটি traceId বসিয়ে দেয়:

{
  "level": "warn",
  "message": "Payment declined",
  "orderId": "ord_91f2",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}

এই আইডির সাহায্যে আপনি ড্যাশবোর্ড থেকে একটি নির্দিষ্ট রিকোয়েস্টের সবগুলো লগ টাইমলাইন অনুযায়ী দেখতে পারবেন।

Metrics

Requests per second (RPS), error rate, latency, memory, event loop delay এগুলো কালেক্ট করা খুব সহজ এবং এগুলোর ওপর ভিত্তি করেই অ্যালার্ট সেট করা হয়।

এক্ষেত্রে দুটি বিষয় মাথায় রাখা জরুরি: প্রথমত, গড় (Average) ল্যাটেন্সি অনেক সময় আসল চিত্র লুকিয়ে রাখে, তাই ধীরগতির রিকোয়েস্টগুলো ধরতে p95 (95th percentile) ব্যবহার করা ভালো। দ্বিতীয়ত, ডেটাকে রাউট (Route) অনুযায়ী ভাগ করে দেখুন।

মেট্রিক্স আপনাকে বলে যে “কিছু একটা পরিবর্তন হয়েছে”, কিন্তু “কেন হয়েছে” তা বলতে পারে না।

Traces

লগ এবং মেট্রিক্স থেকে পাওয়া ধারণাকে আরও স্পষ্ট করে ট্র্যাকিং বা Traces।

একটি ইউজার রিকোয়েস্ট ভেতরে কতগুলো ধাপ পার হলো (গার্ড, কন্ট্রোলার, সার্ভিস, ডেটাবেস কুয়েরি, থার্ড-পার্টি এপিআই) তার প্রতিটি ধাপের শুরুর সময় এবং ব্যাপ্তিকাল (duration) ট্রেসের মাধ্যমে দেখা যায়।

ঐতিহ্যবাহী OpenTelemetry সেটআপ করা বেশ সময়সাপেক্ষ।

তবে ভালো একটি ট্রেসিং টুলে যা যা থাকা উচিত: আপনার কোডের নামে স্প্যান (Spans), টোটাল টাইমের পাশাপাশি সেলফ টাইম (Self time), ডেটাবেস কুয়েরি এবং থার্ড-পার্টি HTTP কলগুলো স্প্যান হিসেবে দেখানো ইত্যাদি।

Error tracking

লগে এরর দেখা যায় ঠিকই, কিন্তু এটি আপনাকে বলবে না যে এই TypeError টি গত ডেপ্লয়মেন্টের পর থেকে ৩,১৪০ বার ঘটেছে, বা এটি একটি নতুন বাগ।

এরর ট্র্যাকিং প্ল্যাটফর্মগুলো আনহ্যান্ডেলড এররগুলোকে গ্রুপ করে এবং নতুন কোনো এরর এলে অ্যালার্ট দেয়।

তবে মনে রাখবেন, ইচ্ছা করে থ্রো করা এরর (যেমন: NotFoundException) এবং আসল আনহ্যান্ডেলড এরর আলাদা রাখতে হবে, তা না হলে এরর রেটের আসল অবস্থা বোঝা যাবে না।

Queues

আপনি যদি Bull বা BullMQ ব্যবহার করেন, তবে আপনার অ্যাপের অর্ধেক কাজ এমন জায়গায় হয় যা সাধারণ HTTP ড্যাশবোর্ডে দেখা যায় না।

কিউয়ের ক্ষেত্রে জানা দরকার: একটি জব পিক আপ হওয়ার আগে কতক্ষণ অপেক্ষা করছে, কতবার ফেইল হচ্ছে এবং কোন রিকোয়েস্ট থেকে জবটি তৈরি হয়েছে।

Doing all of this without losing a week

উপরের সবকিছু আপনি আলাদা আলাদা টুল (Terminus, Prometheus, Grafana, OpenTelemetry, Sentry ইত্যাদি) দিয়ে সেটআপ করতে পারেন।

অনেকেই তা করে, কিন্তু এতে ৫-৬টি আলাদা সিস্টেম মেইনটেইন করতে হয়।

এই ঝামেলা কমাতেই মূলত NestJS Observe তৈরি করা হয়েছে। এটি কোনো সাধারণ Node.js এজেন্টের মতো নয়, বরং Nest-এর ইন্টারনাল হুক ব্যবহার করে কাজ করে।

npm install @nestjs/observe

এরপর শুধু কনফিগারেশন:

// app.module.ts
import { createObserveModule } from "@nestjs/observe";
export const { ObserveModule, ObserveInstrument } = createObserveModule();

@Module({
  imports: [
    ObserveModule.forRoot({
      appKey: process.env.OBSERVE_APP_KEY,
      appSecret: process.env.OBSERVE_APP_SECRET,
      serviceId: "orders-api",
      http: {
        ignore: [/^\/health(?:\?|$)/], // হেলথ চেক মেট্রিক্স থেকে বাদ রাখুন
      },
    }),
  ],
})
export class AppModule {}
// main.ts
const app = await NestFactory.create(AppModule, {
  instrument: ObserveInstrument,
});

ব্যাস! কোনো এক্সট্রা কোড লিখতে হবে না। HTTP, GraphQL, Microservices, Cron সবকিছুর মেট্রিক্স এবং ট্রেস অটোমেটিকভাবে ড্যাশবোর্ডে চলে আসবে এবং আপনার লগে trace id যুক্ত হয়ে যাবে।

If I were starting from zero today

আমি যদি আজ একদম শুরু থেকে মনিটরিং সেটআপ করি, তবে এই সিরিয়াল মানব:

  • ১. সবার আগে Health check (এবং এটিকে অন্য মেট্রিক্স থেকে আলাদা রাখব)।
  • ২. প্রোডাকশন লগের জন্য JSON logs এবং structured params সেট করব।
  • ৩. Tracing এবং Error tracking একসাথে ইমপ্লিমেন্ট করব (কারণ ট্রেস ছাড়া এরর খোঁজা কঠিন)।
  • ৪. এক সপ্তাহের ডেটা জমা হলে Error rate এবং p95 এর ওপর ভিত্তি করে Alerts সেট করব।
  • ৫. যেদিন থেকে Queue ব্যবহার শুরু করব, সেদিনই Queue monitoring যুক্ত করব।

আকর্ষণীয় ড্যাশবোর্ড তৈরি করা আমাদের মূল লক্ষ্য নয়।

আসল লক্ষ্য হলো প্রোডাকশনে কোনো সমস্যা হলে, ঠিক কোন লাইনের কোড বা ডিপেন্ডেন্সির কারণে সমস্যাটি হচ্ছে, তা যত দ্রুত সম্ভব খুঁজে বের করা।

Conclusion

একটি ভালো মনিটরিং সিস্টেমের আসল উদ্দেশ্য কেবল সুন্দর বা আকর্ষণীয় কিছু ড্যাশবোর্ড তৈরি করে রাখা নয়।

এর মূল লক্ষ্য হলো প্রোডাকশনে কোনো সমস্যা দেখা দিলে, ঠিক কোন লাইনের কোড বা কোন ডিপেন্ডেন্সির কারণে সমস্যাটি হচ্ছে, তা যত দ্রুত সম্ভব খুঁজে বের করার একটি সহজ পথ তৈরি করা।

সঠিক Health Checks, Structured Logs, Metrics এবং Tracing-এর সমন্বয়ে আপনি আপনার NestJS অ্যাপ্লিকেশনের পুরো নিয়ন্ত্রণ নিজের হাতে রাখতে পারবেন।

প্রথম দিকে এই টুলগুলো সেটআপ করতে হয়তো কিছুটা সময় ও শ্রম দিতে হবে, কিন্তু প্রোডাকশনের জটিল কোনো বাগ (Bug) বা ইনসিডেন্টের সময় এই সিস্টেমটিই আপনার ঘণ্টার পর ঘণ্টা সময় বাঁচিয়ে দেবে।

আপনার NestJS প্রজেক্টের মনিটরিং সেটআপ নিয়ে কোনো প্রশ্ন থাকলে বা আপনি কোন টুলস ব্যবহার করছেন তা কমেন্টে জানাতে পারেন। হ্যাপি কোডিং!

All Tech Update

Technology এর সকল আপডেট সবার আগে বিস্তারিত পড়ুন –

Scroll to Top