正在加载,请稍候…

Logging and Observability in Node.js: Structured Logs with Pino

Implement production-grade logging in Node.js with Pino. Learn structured logging, log levels, correlation IDs, request logging, and integrating with observability platforms.

Logging and Observability in Node.js

Why Structured Logging?

// Unstructured (hard to query)
[2024-01-15 10:23:45] ERROR: User 123 failed to login from 192.168.1.1

// Structured JSON (queryable, parseable)
{
  "level": "error",
  "time": "2024-01-15T10:23:45.000Z",
  "msg": "Login failed",
  "userId": "123",
  "ip": "192.168.1.1",
  "reason": "invalid_password",
  "requestId": "req_abc123"
}

Pino Setup

import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level(label) { return { level: label }; },
  },
  base: {
    service: 'user-api',
    env: process.env.NODE_ENV,
    version: process.env.APP_VERSION,
  },
  // Human-readable in development
  transport: process.env.NODE_ENV === 'development'
    ? { target: 'pino-pretty', options: { colorize: true } }
    : undefined,
});

export default logger;

Child Loggers with Context

// Request-scoped logger with correlation ID
app.use((req, res, next) => {
  const requestId = req.headers['x-request-id'] as string || generateId();
  req.logger = logger.child({
    requestId,
    method: req.method,
    url: req.url,
    userAgent: req.headers['user-agent'],
  });
  res.setHeader('x-request-id', requestId);
  next();
});

// Use in controllers
async function createUser(req: Request, res: Response) {
  const log = req.logger;
  log.info('Creating user', { email: req.body.email });

  try {
    const user = await userService.create(req.body);
    log.info('User created', { userId: user.id });
    res.json(user);
  } catch (err) {
    log.error({ err }, 'Failed to create user');
    res.status(500).json({ error: 'Internal error' });
  }
}

Request/Response Logging

import pinoHttp from 'pino-http';

app.use(pinoHttp({
  logger,
  customLogLevel: (req, res) => {
    if (res.statusCode >= 500) return 'error';
    if (res.statusCode >= 400) return 'warn';
    return 'info';
  },
  customSuccessMessage: (req, res) => {
    return `${req.method} ${req.url} ${res.statusCode}`;
  },
  serializers: {
    req: (req) => ({
      id: req.id,
      method: req.method,
      url: req.url,
      // Don't log auth headers
      headers: { ...req.headers, authorization: '[REDACTED]' },
    }),
  },
}));

Error Logging

// Log errors with full context
function logError(logger: Logger, err: unknown, context?: Record<string, unknown>) {
  if (err instanceof Error) {
    logger.error({
      err: {
        message: err.message,
        name: err.name,
        stack: err.stack,
        ...('code' in err ? { code: err.code } : {}),
      },
      ...context,
    }, err.message);
  } else {
    logger.error({ err, ...context }, 'Unknown error');
  }
}

Log Levels

// Guideline for log levels:
logger.trace({ query, params }, 'DB query executing');     // Very verbose
logger.debug({ userId, sessionId }, 'User session checked'); // Debug info
logger.info({ userId }, 'User logged in');                  // Normal operations
logger.warn({ attempts }, 'Rate limit approaching');         // Warning signs
logger.error({ err }, 'Payment processing failed');          // Errors
logger.fatal({ err }, 'Database connection lost');           // System critical

OpenTelemetry Integration

import { trace, context, propagation } from '@opentelemetry/api';

app.use((req, res, next) => {
  const span = trace.getActiveSpan();
  const traceId = span?.spanContext().traceId;

  req.logger = logger.child({ traceId });
  next();
});

Structured logs enable powerful queries in Elasticsearch, Loki, or CloudWatch Insights.