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 日志级别对比
| 级别 | Pino | Winston | 使用场景 |
|---|---|---|---|
| trace | 10 | - | 极详细的调试信息 |
| debug | 20 | debug | 开发调试信息 |
| info | 30 | info | 正常业务事件 |
| warn | 40 | warn | 可恢复的问题 |
| error | 50 | error | 需要关注的错误 |
| fatal | 60 | - | 进程即将崩溃 |
生产环境建议
开发环境: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 等工具索引。
业务场景
- 请求追踪:通过 requestId 串联一次请求的所有日志
- 错误监控:错误日志自动发送到 Sentry 等监控平台
- 业务分析:通过事件日志分析用户行为
- 性能监控:记录关键操作的耗时
扩展阅读
上一章:第 17 章 · 测试 下一章:第 19 章 · 错误处理 — 错误类型、全局捕获和优雅退出。