日志与监控
What — 是什么
日志与监控是 Node.js 生产环境的可观测性基石,通过结构化日志记录运行状态,通过健康检查和 APM 监控发现异常,确保服务稳定运行。
核心概念:
- Winston:功能丰富的日志库,支持多传输(Console/File/HTTP),日志级别,格式化
- Pino:极致性能的 JSON 日志库,Fastify 默认集成,子进程写文件避免阻塞主线程
- 结构化日志:每条日志是 JSON 对象,包含时间/级别/请求ID/上下文等字段,便于机器检索
- APM:应用性能监控,追踪请求链路、数据库查询、HTTP 调用的耗时
- 健康检查:
/health端点检查服务依赖(数据库/Redis/外部 API)是否正常 - PM2 Metrics:PM2 内置的进程级监控(CPU/内存/事件循环延迟/请求数)
关键特性:
- Pino 比 Winston 快 5-10 倍,是 Node.js 最快的日志库
- 结构化日志配合 ELK/Loki 等日志系统实现集中检索
- 健康检查是 K8s liveness/readiness 探针的基础
- APM 可追踪分布式请求的完整链路
Why — 为什么
适用场景:
- 故障排查:通过日志定位错误原因
- 性能监控:追踪慢请求和瓶颈
- 审计合规:记录关键操作和访问
- 告警:异常指标触发通知
对比日志库:
| 维度 | Pino | Winston | console.log |
|---|---|---|---|
| 性能 | 极快(~10M ops/s) | 中(~1M ops/s) | 快 |
| 格式 | JSON | 可配置 | 文本 |
| 传输 | 子进程写文件 | 多传输 | 终端 |
| 子日志 | child() 绑定上下文 | 手动 | 无 |
| 适用 | 生产高性能 | 灵活配置 | 开发调试 |
优缺点:
- ✅ 优点:
- Pino 极致性能不阻塞事件循环
- 结构化日志便于机器检索分析
- 健康检查实现自动化故障检测
- APM 全链路追踪定位分布式问题
- ❌ 缺点:
- 日志量大增加存储成本
- APM 引入额外性能开销
- 结构化日志不如文本日志直观
How — 怎么用
安装配置
npm install pino pino-pretty
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: { level: (label) => ({ level: label }) },
timestamp: pino.stdTimeFunctions.isoTime
}, pino.destination({ dest: 'logs/app.log', mkdir: true, sync: false }));
代码示例
Pino 结构化日志 + 请求上下文:
const pino = require('pino');
const logger = pino({
level: 'info',
redact: ['req.headers.authorization', 'req.body.password'], // 脱敏
serializers: {
req(req) {
return { method: req.method, url: req.url, id: req.id };
},
err: pino.stdSerializers.err
}
});
// 子日志绑定上下文
function requestLogger(req, res, next) {
req.log = logger.child({ reqId: req.id, userId: req.user?.id });
req.log.info({ req }, 'Request started');
const start = Date.now();
res.on('finish', () => {
req.log.info({ res: { statusCode: res.statusCode }, duration: Date.now() - start }, 'Request completed');
});
next();
}
// 使用
app.use(requestLogger);
app.get('/users/:id', async (req, res) => {
req.log.info({ userId: req.params.id }, 'Fetching user');
const user = await getUser(req.params.id);
req.log.debug({ user }, 'User fetched');
res.json(user);
});
健康检查端点:
app.get('/health', async (req, res) => {
const checks = {
database: await checkDatabase(),
redis: await checkRedis(),
externalApi: await checkExternalApi()
};
const isHealthy = Object.values(checks).every(c => c.status === 'ok');
res.status(isHealthy ? 200 : 503).json({
status: isHealthy ? 'healthy' : 'degraded',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
checks
});
});
async function checkDatabase() {
try {
await db.query('SELECT 1');
return { status: 'ok', latency: 1 };
} catch (err) {
return { status: 'error', message: err.message };
}
}
async function checkRedis() {
try {
const start = Date.now();
await redis.ping();
return { status: 'ok', latency: Date.now() - start };
} catch (err) {
return { status: 'error', message: err.message };
}
}
性能调优
| 参数 | 默认值 | 调优建议 | 说明 |
|---|---|---|---|
sync | true | 生产用 false | 异步写文件不阻塞主线程 |
level | info | 生产 info,开发 debug | 低级别日志量增大 |
redact | 无 | 敏感字段列表 | 防止密码/Token 入日志 |
| 日志轮转 | 无 | pino-roll/pmv-logrotate | 防止日志文件无限增长 |
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 日志丢失 | 异步写入时进程崩溃 | 关键日志用 logger.flush() 确保写入 |
| 敏感信息泄露 | 未脱敏 | 配置 redact 字段列表 |
| 日志文件过大 | 未轮转 | pino-roll 或外部 logrotate |
| 日志影响性能 | 日志级别太低或同步写 | 生产用 info+async |
| 健康检查假阳性 | 检查逻辑太敏感 | 设置合理超时和重试 |
最佳实践
- 生产环境使用 Pino + 异步写入
- 每个请求绑定请求 ID 和用户 ID(
logger.child()) - 敏感字段脱敏(
redact) - 实现健康检查端点供 K8s/负载均衡探针
- 日志级别:开发 debug,生产 info
- 关键操作记录审计日志(创建/删除/权限变更)
面试题
Q1: Pino 为什么比 Winston 快?
三个原因:① Pino 最小化主线程工作——只做 JSON 序列化,文件写入交由子进程(
pino.destination的sync: false),不阻塞事件循环;② 无格式化开销——Winston 的格式化链(timestamp/printf/colorize)在主线程执行,Pino 直接输出 JSON;③ 无动态特性——Winston 支持多传输和动态配置,Pino 设计极简,牺牲灵活性换性能。
Q2: 结构化日志的好处?
结构化日志每条记录是 JSON 对象,好处:① 机器可解析——ELK/Loki/Datadog 直接索引字段,全文检索和聚合分析;② 上下文丰富——每条日志携带请求 ID/用户 ID/trace ID,串联完整请求链路;③ 脱敏方便——
redact规则自动过滤敏感字段;④ 告警精准——按字段值设告警规则(如level=error AND statusCode>=500);⑤ 兼容微服务——各服务日志格式统一,集中检索。
Q3: 健康检查的 liveness 和 readiness 区别?
liveness(存活探针):进程是否存活,失败则重启容器。检查进程本身(死锁/无限循环),简单返回 200 即可。readiness(就绪探针):服务是否准备好接收流量,失败则从负载均衡移除。检查依赖服务(数据库/Redis/外部 API),所有依赖正常才返回 200。区别:liveness 失败=重启进程,readiness 失败=暂停流量。启动时 readiness 先失败(加载配置/连接数据库),就绪后通过;liveness 一直正常。
相关链接:
- 进程管理与守护
- 性能调优
- 错误处理与调试
- Pino 文档:https://getpino.io/