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: verbose → debug → log → warn → error → fatal. 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.