NestJS — Enterprise Node.js

Logging, Health Checks & Observability

16 min Lesson 45 of 48

Logging, Health Checks & Observability

Production-grade applications must answer three questions at any moment: What is happening? (logging), Is the service alive? (health checks), and How do I trace a request end-to-end? (correlation IDs / distributed tracing). NestJS ships with a built-in logger and integrates cleanly with structured loggers like Pino or Winston and the @nestjs/terminus health-check library.

The built-in Logger

NestJS provides Logger from @nestjs/common. Instantiate it with a context string (the class name) to tag every log line:

import { Injectable, Logger } from '@nestjs/common'; @Injectable() export class OrdersService { private readonly logger = new Logger(OrdersService.name); async create(dto: CreateOrderDto) { this.logger.log(`Creating order for user ${dto.userId}`); try { const order = await this.ordersRepo.save(dto); this.logger.verbose(`Order saved: ${order.id}`); return order; } catch (err) { this.logger.error('Failed to save order', err.stack); throw err; } } }

Log levels in ascending severity: verbosedebuglogwarnerrorfatal. Control which levels are active via app.useLogger() or the logger option on NestFactory.create().

Replacing the logger with Pino (structured JSON)

Plain text logs are hard to query in production. nestjs-pino replaces the default logger with Pino, which emits newline-delimited JSON that log aggregators (Loki, CloudWatch, Datadog) parse natively:

// app.module.ts import { LoggerModule } from 'nestjs-pino'; @Module({ imports: [ LoggerModule.forRoot({ pinoHttp: { level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } // human-readable in dev : undefined, }, }), ], }) export class AppModule {} // main.ts import { Logger } from 'nestjs-pino'; const app = await NestFactory.create(AppModule, { bufferLogs: true }); app.useLogger(app.get(Logger)); await app.listen(3000);
bufferLogs: true holds early boot messages until Pino is ready so nothing is lost. Always pair it with app.useLogger(app.get(Logger)).

Health checks with @nestjs/terminus

@nestjs/terminus exposes a /health endpoint that orchestrates multiple indicators (database, disk, memory, HTTP dependencies). Kubernetes liveness and readiness probes call this endpoint to decide whether to route traffic to a pod.

// health.controller.ts import { Controller, Get } from '@nestjs/common'; import { HealthCheckService, HealthCheck, TypeOrmHealthIndicator, MemoryHealthIndicator, DiskHealthIndicator, } from '@nestjs/terminus'; @Controller('health') export class HealthController { constructor( private health: HealthCheckService, private db: TypeOrmHealthIndicator, private mem: MemoryHealthIndicator, private disk: DiskHealthIndicator, ) {} @Get() @HealthCheck() check() { return this.health.check([ () => this.db.pingCheck('database'), () => this.mem.checkHeap('memory_heap', 300 * 1024 * 1024), // 300 MB () => this.disk.checkStorage('storage', { path: '/', thresholdPercent: 0.9 }), ]); } }

The response JSON has a top-level status ("ok" or "error") and a per-indicator breakdown. HTTP 200 = healthy; HTTP 503 = degraded.

Request tracing with correlation IDs

A correlation ID is a UUID generated (or forwarded from the caller) at the entry point of every request and attached to all logs and outgoing HTTP calls. This lets you grep a single ID across microservices to reconstruct a full trace:

// correlation.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import { randomUUID } from 'crypto'; @Injectable() export class CorrelationMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const id = (req.headers['x-correlation-id'] as string) ?? randomUUID(); req.headers['x-correlation-id'] = id; res.setHeader('x-correlation-id', id); next(); } }
Pass the correlation ID downstream. When your service calls another HTTP service (via HttpService / Axios), forward the same x-correlation-id header. In Pino, include it via pinoHttp.customProps so every log line carries the ID automatically.
Never log sensitive data. Correlation IDs, request paths, and timing are safe. Passwords, tokens, PII, and credit-card numbers must never appear in logs — even in development. Implement a log-redaction strategy (Pino supports redact paths natively).

Summary

Use NestJS's built-in Logger for simple scenarios and replace it with nestjs-pino for structured JSON logs in production. Add @nestjs/terminus health indicators for database, memory, and disk so that Kubernetes (or any health-check system) can monitor service readiness. Inject correlation IDs via middleware to tie all log lines from a single request together — across services. These three pillars (logging, health, tracing) form the foundation of production observability.