强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Node.js 开发指南 / 第 18 章 · 日志

第 18 章 · 日志

18.1 为什么不用 console.log

问题说明
无日志级别无法区分调试信息和错误
无结构化输出难以被日志系统解析
无日志轮转文件会无限增长
性能差同步输出到 stdout
无上下文无法关联请求 ID

18.2 Pino(推荐)

npm install pino pino-pretty

基本使用

const pino = require('pino');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
});

// 日志级别(从低到高)
logger.trace('详细跟踪信息');   // 10
logger.debug('调试信息');       // 20
logger.info('普通信息');        // 30
logger.warn('警告信息');        // 40
logger.error('错误信息');       // 50
logger.fatal('致命错误');       // 60

结构化日志

// 带上下文的日志
logger.info({ userId: 123, action: 'login' }, '用户登录成功');
logger.error({ err: new Error('数据库连接失败'), db: 'primary' }, '连接错误');

// 输出(JSON 格式):
// {"level":30,"time":1715337600000,"msg":"用户登录成功","userId":123,"action":"login"}
// {"level":50,"time":1715337600001,"msg":"连接错误","err":{"type":"Error","message":"..."},"db":"primary"}

子 Logger

const baseLogger = pino({ level: 'info' });

// 为不同模块创建子 Logger
const dbLogger = baseLogger.child({ module: 'database' });
const apiLogger = baseLogger.child({ module: 'api' });
const authLogger = baseLogger.child({ module: 'auth', component: 'jwt' });

dbLogger.info({ query: 'SELECT * FROM users' }, '查询执行');
apiLogger.info({ method: 'GET', path: '/api/users' }, '请求处理');
authLogger.warn({ userId: 123 }, '令牌即将过期');

Express 集成

const express = require('express');
const pino = require('pino');
const pinoHttp = require('pino-http');

const app = express();
const logger = pino({ level: 'info' });

// 请求日志中间件
app.use(pinoHttp({
  logger,
  // 自定义请求属性
  customLogLevel: (req, res, err) => {
    if (res.statusCode >= 500) return 'error';
    if (res.statusCode >= 400) return 'warn';
    return 'info';
  },
  customSuccessMessage: (req, res) => {
    return `${req.method} ${req.url} ${res.statusCode}`;
  },
  customErrorMessage: (req, res, err) => {
    return `${req.method} ${req.url} ${res.statusCode} - ${err.message}`;
  },
  // 为每个请求生成 ID
  genReqId: (req) => req.headers['x-request-id'] || require('crypto').randomUUID(),
}));

// 路由中使用
app.get('/api/users', (req, res) => {
  req.log.info('开始查询用户'); // 自动携带请求上下文
  // ... 处理逻辑
  res.json({ users: [] });
});

输出到文件和开发模式

const pino = require('pino');

// 生产环境:输出 JSON 到文件和 stdout
// 开发环境:使用 pino-pretty 美化输出
const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: process.env.NODE_ENV !== 'production'
    ? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:HH:MM:ss' } }
    : undefined,
});

// 或使用多路输出
const transport = pino.transport({
  targets: [
    { target: 'pino-pretty', level: 'debug', options: { destination: 1 } }, // stdout
    { target: 'pino/file', level: 'info', options: { destination: './logs/app.log' } }, // 文件
  ],
});
const fileLogger = pino({ level: 'debug' }, transport);

18.3 Winston

npm install winston
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'my-app' },
  transports: [
    // 控制台输出
    new winston.transports.Console({
      format: process.env.NODE_ENV !== 'production'
        ? winston.format.combine(winston.format.colorize(), winston.format.simple())
        : winston.format.json(),
    }),
    // 文件输出
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' }),
  ],
});

// 使用
logger.info('用户登录', { userId: 123 });
logger.error('数据库错误', { error: err.message, stack: err.stack });

日志轮转

npm install winston-daily-rotate-file
const DailyRotateFile = require('winston-daily-rotate-file');

const transport = new DailyRotateFile({
  filename: 'logs/app-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  maxSize: '20m',       // 单文件最大 20MB
  maxFiles: '14d',      // 保留 14 天
  zippedArchive: true,  // 压缩旧文件
});

logger.add(transport);

18.4 日志级别对比

级别PinoWinston使用场景
trace10-极详细的调试信息
debug20debug开发调试信息
info30info正常业务事件
warn40warn可恢复的问题
error50error需要关注的错误
fatal60-进程即将崩溃

生产环境建议

开发环境:debug 或 trace
测试环境:info
生产环境:info(可临时调为 debug 排查问题)

18.5 结构化日志最佳实践

// ✅ 好的结构化日志
logger.info({
  event: 'order_created',
  orderId: 'ORD-12345',
  userId: 'U-789',
  amount: 299.99,
  currency: 'CNY',
  items: 3,
  duration_ms: 45,
}, '订单创建成功');

// ❌ 不好的日志(字符串拼接)
logger.info(`用户 ${userId} 创建了订单 ${orderId},金额 ${amount} 元`);

请求上下文传播

const { AsyncLocalStorage } = require('node:async_hooks');

const als = new AsyncLocalStorage();

function requestLogger(req, res, next) {
  const requestId = req.headers['x-request-id'] || crypto.randomUUID();
  const logger = baseLogger.child({ requestId, userId: req.user?.id });
  als.run({ logger, requestId }, () => {
    req.logger = logger;
    next();
  });
}

// 在任何地方获取当前请求的 Logger
function getLogger() {
  const store = als.getStore();
  return store?.logger || baseLogger;
}

// 业务代码中使用
async function processOrder(order) {
  const logger = getLogger();
  logger.info({ orderId: order.id }, '开始处理订单');
  // ...
  logger.info({ orderId: order.id }, '订单处理完成');
}

注意事项

⚠️ 不要记录敏感信息:密码、令牌、信用卡号等永远不要出现在日志中。

⚠️ 日志级别合理使用:生产环境避免 debug,会严重影响性能和磁盘空间。

⚠️ Pino 比 Winston 快 5 倍以上:对性能敏感的场景推荐 Pino。

⚠️ 日志要可搜索:结构化 JSON 日志比纯文本更易被 ELK、Grafana 等工具索引。

业务场景

  1. 请求追踪:通过 requestId 串联一次请求的所有日志
  2. 错误监控:错误日志自动发送到 Sentry 等监控平台
  3. 业务分析:通过事件日志分析用户行为
  4. 性能监控:记录关键操作的耗时

扩展阅读


上一章第 17 章 · 测试 下一章第 19 章 · 错误处理 — 错误类型、全局捕获和优雅退出。