正在加载,请稍候…

Node.js 日志与可观测性:使用 Pino 实现结构化日志

在 Node.js 中使用 Pino 实现生产级日志。学习结构化日志、日志级别、关联 ID、请求日志记录以及与可观测性平台的集成。

Node.js 日志与可观测性:使用 Pino 实现结构化日志

Node.js 日志与可观测性

为什么选择结构化日志?

// 非结构化(难以查询)
[2024-01-15 10:23:45] ERROR: User 123 failed to login from 192.168.1.1

// 结构化 JSON(可查询、可解析)
{
  "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"
}

Node.js 日志与可观测性:使用 Pino 实现结构化日志插图

Pino 设置

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,
  },
  // 开发环境下可读性更好
  transport: process.env.NODE_ENV === 'development'
    ? { target: 'pino-pretty', options: { colorize: true } }
    : undefined,
});

export default logger;

带上下文的子日志器

// 带有关联 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();
});

// 在控制器中使用
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' });
  }
}

Node.js 日志与可观测性:使用 Pino 实现结构化日志插图

请求/响应日志记录

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,
      // 不记录认证头
      headers: { ...req.headers, authorization: '[REDACTED]' },
    }),
  },
}));

错误日志记录

// 记录带有完整上下文的错误
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');
  }
}

Node.js 日志与可观测性:使用 Pino 实现结构化日志插图

日志级别

// 日志级别指南:
logger.trace({ query, params }, 'DB query executing');     // 非常详细
logger.debug({ userId, sessionId }, 'User session checked'); // 调试信息
logger.info({ userId }, 'User logged in');                  // 正常操作
logger.warn({ attempts }, 'Rate limit approaching');         // 警告信号
logger.error({ err }, 'Payment processing failed');          // 错误
logger.fatal({ err }, 'Database connection lost');           // 系统严重

OpenTelemetry 集成

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();
});

结构化日志支持在 Elasticsearch、Loki 或 CloudWatch Insights 中进行强大的查询。