Beginner

Centralized Logging for NestJS Applications

Replace NestJS's default console logger with structured JSON output and ship logs to LogFlow for search, alerts, and anomaly detection.

LogFlow TeamJuly 13, 202615 minNestJSTypeScriptNode.js

NestJS ships with a built-in Logger class that writes plain text to the console. It's fine for local development but useless in production — you can't filter by level, search by metadata, or set up alerts on unstructured text.

This tutorial replaces the default logger with Pino (the fastest Node.js JSON logger), integrates it with NestJS's dependency injection system, and ships every log to LogFlow.

Prerequisites

  • Node.js 18+ and a NestJS project
  • A LogFlow account (sign up free)
  • Your LogFlow API key (Settings → API Keys)

Step 1 — Install dependencies

npm install @getlogflow/js pino nestjs-pino pino-http
  • pino — fast JSON logger
  • nestjs-pino — NestJS module that replaces the built-in logger
  • pino-http — automatic HTTP request logging
  • @getlogflow/js — LogFlow SDK for shipping logs

Step 2 — Configure the logger module

In your app.module.ts, import and configure LoggerModule:

import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';

@Module({
  imports: [
    LoggerModule.forRoot({
      pinoHttp: {
        level: process.env.LOG_LEVEL || 'info',
        transport: process.env.NODE_ENV === 'development'
          ? { target: 'pino-pretty', options: { colorize: true } }
          : undefined,
        serializers: {
          req: (req) => ({
            method: req.method,
            url: req.url,
            params: req.params,
          }),
          res: (res) => ({
            statusCode: res.statusCode,
          }),
        },
      },
    }),
  ],
})
export class AppModule {}

In development you get pretty-printed output. In production, raw JSON — perfect for log forwarders.

Step 3 — Replace the default logger in main.ts

import { NestFactory } from '@nestjs/core';
import { Logger } from 'nestjs-pino';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { bufferLogs: true });
  app.useLogger(app.get(Logger));
  await app.listen(3000);
}
bootstrap();

bufferLogs: true ensures NestJS's startup logs also go through Pino instead of the default console logger.

Step 4 — Use the logger in services

Inject PinoLogger anywhere in your application:

import { Injectable } from '@nestjs/common';
import { PinoLogger, InjectPinoLogger } from 'nestjs-pino';

@Injectable()
export class OrderService {
  constructor(
    @InjectPinoLogger(OrderService.name)
    private readonly logger: PinoLogger,
  ) {}

  async createOrder(userId: string, items: CartItem[]) {
    const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);

    this.logger.info({ userId, itemCount: items.length, total }, 'Order created');

    try {
      const order = await this.orderRepo.save({ userId, items, total });
      this.logger.info({ orderId: order.id, userId }, 'Order saved to database');
      return order;
    } catch (err) {
      this.logger.error({ userId, err }, 'Failed to save order');
      throw err;
    }
  }
}

Every log line automatically includes the class name as context, plus any request-scoped data (request ID, method, URL) from the HTTP layer.

Step 5 — Ship logs to LogFlow

Option A — LogFlow SDK (recommended)

Create a logflow.service.ts that initializes the SDK and exposes a transport:

import { Injectable, OnModuleInit } from '@nestjs/common';
import LogFlow from '@getlogflow/js';

@Injectable()
export class LogFlowService implements OnModuleInit {
  private client: LogFlow;

  onModuleInit() {
    this.client = new LogFlow({
      apiKey: process.env.LOGFLOW_API_KEY,
      service: 'nestjs-app',
    });
  }

  log(level: string, message: string, meta?: Record<string, any>) {
    this.client[level]?.(message, meta);
  }
}

Option B — Pino transport to LogFlow HTTP API

For zero-code-change shipping, pipe Pino output to LogFlow using a custom transport:

// logflow-transport.ts
import { Transform } from 'stream';
import { fetch } from 'undici';

const LOGFLOW_URL = 'https://api.getlogflow.com/v1/logs';
const API_KEY = process.env.LOGFLOW_API_KEY;

const buffer: any[] = [];
let timer: NodeJS.Timeout | null = null;

async function flush() {
  if (buffer.length === 0) return;
  const batch = buffer.splice(0, buffer.length);

  const payload = batch.map((entry) => ({
    level: pinoLevelToString(entry.level),
    message: entry.msg || '',
    service: 'nestjs-app',
    timestamp: new Date(entry.time).toISOString(),
    attributes: extractAttributes(entry),
  }));

  try {
    await fetch(LOGFLOW_URL, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(payload),
    });
  } catch {
    // don't crash the app over logging failures
  }
}

function pinoLevelToString(level: number): string {
  if (level >= 60) return 'fatal';
  if (level >= 50) return 'error';
  if (level >= 40) return 'warn';
  if (level >= 30) return 'info';
  return 'debug';
}

function extractAttributes(entry: any): Record<string, string> {
  const skip = new Set(['level', 'time', 'pid', 'hostname', 'msg', 'v']);
  const attrs: Record<string, string> = {};
  for (const [k, v] of Object.entries(entry)) {
    if (!skip.has(k)) attrs[k] = String(v);
  }
  return attrs;
}

export function createLogFlowTransport() {
  return new Transform({
    objectMode: true,
    transform(chunk, _enc, cb) {
      try {
        const entry = JSON.parse(chunk.toString());
        buffer.push(entry);
        if (!timer) timer = setTimeout(() => { flush(); timer = null; }, 1000);
      } catch {}
      cb();
    },
  });
}

This batches logs and flushes every second, keeping network overhead low.

Step 6 — Add request correlation

For tracing requests across services, add a correlation ID middleware:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { randomUUID } from 'crypto';

@Injectable()
export class CorrelationMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    const traceId = req.headers['x-trace-id'] || randomUUID();
    req.headers['x-trace-id'] = traceId;
    res.setHeader('x-trace-id', traceId);
    next();
  }
}

Then include traceId in your Pino config:

pinoHttp: {
  customProps: (req) => ({
    traceId: req.headers['x-trace-id'],
  }),
}

Every log from that request now carries the same traceId. In LogFlow, click any log's trace ID to see the full request timeline across services.

Step 7 — Set up alerts in LogFlow

With structured logs flowing in, set up alerts for common failure patterns:

Alert Condition Channel
High error rate Error rate > 5% for nestjs-app Slack
Payment failures Keyword payment_failed Telegram
Service silence No logs for 10 minutes Email

Go to Alerts → Create in the LogFlow dashboard. Each alert takes 30 seconds to configure.

What you've built

Your NestJS app now has:

  • ✓ Structured JSON logs replacing the default console output
  • ✓ Automatic HTTP request logging (method, path, status, latency)
  • ✓ Request correlation via trace IDs
  • ✓ Log shipping to LogFlow with batched transport
  • ✓ Contextual business logging in services

Total setup time: about 15 minutes. No infrastructure to manage.

Next steps

Start monitoring your logs today

Free plan available. No credit card required. Up and running in 2 minutes.

Get started free