This episode covers basic NestJS observability: the built-in Logger and a custom logger service, external logging integration with Winston and Pino, health checks using @nestjs/terminus, and basic metrics and observability patterns.

An application that can't be observed is a black box — when an error happens in production, you have no idea what's going on. Observability starts with good logging, health checks, and basic monitoring.
Episode 9 covers the built-in logger, integration with Winston and Pino, health checks with @nestjs/terminus, and basic observability patterns.
NestJS provides Logger from @nestjs/common:
import { Injectable, Logger } from "@nestjs/common";
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
findAll(): string[] {
this.logger.log("Memuat semua user");
this.logger.warn("Ini peringatan");
return ["Arman", "Budi"];
}
}The log, warn, error, and debug methods provide different levels. The UsersService.name context helps identify the source of the log.
For full control, create a service that implements LoggerService:
import { Injectable, LoggerService } from "@nestjs/common";
@Injectable()
export class AppLogger implements LoggerService {
log(message: string): void {
console.log(`[INFO] ${message}`);
}
error(message: string, trace?: string): void {
console.error(`[ERROR] ${message}`, trace ?? "");
}
warn(message: string): void {
console.warn(`[WARN] ${message}`);
}
}A custom logger can be injected across the whole application and replace the built-in logger.
Pino is a high-performance logger with JSON output. Integrate it through the nestjs-pino package:
npm install nestjs-pino pino-httpimport { Module } from "@nestjs/common";
import { LoggerModule } from "nestjs-pino";
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
transport: process.env.NODE_ENV !== "production"
? { target: "pino-pretty" }
: undefined,
},
}),
],
})
export class AppModule {}Pino's JSON output is easy for log tools like Loki, CloudWatch, or ELK to parse.
Winston is a popular logger with a flexible transport system — it can write to files, console, or external services. The choice between Pino and Winston depends on your needs: Pino wins on performance, Winston wins on transport flexibility.
@nestjs/terminus provides ready-to-use health indicators:
npm install @nestjs/terminusimport { Controller, Get } from "@nestjs/common";
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from "@nestjs/terminus";
@Controller("health")
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly db: TypeOrmHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck("database"),
]);
}
}The GET /health endpoint now checks the database connection and returns a status.
{
"status": "ok",
"info": {
"database": { "status": "up" }
},
"details": {
"database": { "status": "up" }
}
}This health check is used by Kubernetes and load balancers to decide whether an instance should receive traffic.
Observability has three pillars: logs, metrics, and traces. For basic metrics, NestJS provides Prometheus integration via the @willsoto/nestjs-prometheus package. Patterns to note from the start:
npm install @willsoto/nestjs-prometheus prom-clientMetrics are then exposed at the /metrics endpoint, which is scraped by Prometheus.
Episode 9 builds the observability foundation: built-in and custom loggers, Pino and Winston integration, health checks with terminus, and an introduction to metrics with Prometheus.
Key takeaways:
Logger from @nestjs/common provides levels and context.LoggerService.nestjs-pino produces high-performance JSON logs.@nestjs/terminus provides health checks for databases.In the next episode 10 we'll discuss transaction and advanced persistence — transaction management with the database adapter, unit of work and repository orchestration, query optimization with eager and lazy loading, and connection pooling for performance tuning.