日志与监控
线上问题不可复现时,日志就是唯一的破案线索;服务悄悄变慢、内存悄悄上涨时,监控就是唯一的报警器。本文覆盖 Node 服务从"写日志"到"看监控"的完整链路:日志库、日志文件管理、进程守护、错误上报与性能分析。
一、日志的重要性与 console 的局限
日志的三大作用:排查问题(报错时回溯上下文)、审计(谁在何时做了什么)、监控(从日志统计错误率、响应时间)。
直接用 console.log 的问题:
| 问题 | 说明 |
|---|---|
| 没有级别 | 无法区分调试信息与错误 |
| 输出不可控 | 生产环境无法关闭调试日志 |
| 格式不统一 | 无法被日志采集系统解析 |
| 容易阻塞 | 同步写 stdout 在高并发下有开销 |
| 无轮转 | 日志文件会无限膨胀 |
javascript
// 临时调试可以,生产别用它当日志系统
console.log("用户登录", userId);二、日志级别
日志按严重程度分级,输出时可以按级别过滤:
| 级别 | 含义 | 示例 |
|---|---|---|
debug | 开发调试细节 | 中间变量、SQL 语句 |
info | 常规业务信息 | 登录成功、订单创建 |
warn | 值得注意但不致命 | 重试、缓存未命中、参数异常 |
error | 错误但进程可继续 | 数据库查询失败、第三方调用失败 |
fatal | 致命错误,进程将退出 | 未捕获异常、内存耗尽 |
约定俗成:开发环境输出 debug,生产环境只输出 warn 及以上,避免日志量淹没关键信息。
三、winston 使用
bash
npm install winston3.1 创建 logger
javascript
const winston = require("winston");
const logger = winston.createLogger({
level: process.env.NODE_ENV === "production" ? "warn" : "debug",
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.errors({ stack: true }), // 打印错误堆栈
winston.format.json() // JSON 结构化输出
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: "logs/error.log", level: "error" }),
new winston.transports.File({ filename: "logs/combined.log" }),
],
});
logger.info("用户登录成功", { userId: 1, ip: "127.0.0.1" });
logger.warn("重试第三次,仍失败", { url });
logger.error("数据库连接失败", new Error("ECONNREFUSED"));3.2 transports:输出到哪里
| Transport | 作用 |
|---|---|
Console | 输出到控制台,PM2/Docker 收集用 |
File | 写入文件 |
DailyRotateFile | 按天分割文件(见下) |
Http | 把日志 POST 到远程日志服务 |
| 自定义 | 转发到 Kafka、Sentry 等 |
3.3 按天分割
bash
npm install winston-daily-rotate-filejavascript
const DailyRotateFile = require("winston-daily-rotate-file");
logger.add(new DailyRotateFile({
filename: "logs/app-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m", // 单文件超过 20MB 自动切割
maxFiles: "14d", // 只保留 14 天
}));四、pino:高性能结构化日志
pino 以极低开销著称(号称比大多数日志库快 5 倍以上),适合对性能敏感的接口,与 Node 异步模型契合:
bash
npm install pinojavascript
const pino = require("pino");
const logger = pino({
level: process.env.LOG_LEVEL || "info",
// 生产环境输出 JSON;开发环境用 pino-pretty 美化
transport: process.env.NODE_ENV === "development"
? { target: "pino-pretty", options: { colorize: true } }
: undefined,
});
logger.info({ userId: 1 }, "用户登录成功"); // 第一参数为结构化字段
logger.error({ err: error }, "查询失败");| 对比 | winston | pino |
|---|---|---|
| 性能 | 中等 | 极快(近零开销) |
| 输出 | 默认 JSON 可配 | 原生 JSON 结构 |
| 生态 | 插件丰富 | 简洁,依赖 pino-pretty |
| 适用 | 功能需求多、团队熟悉 | 高吞吐、追求性能 |
五、日志格式化与 JSON 日志
结构化 JSON 日志是生产标配:每条日志一行 JSON,字段固定,可被 ELK、Loki 等系统直接采集、搜索、聚合。
json
{"level":"error","message":"数据库连接失败","timestamp":"2026-08-09 12:00:00","service":"order-api","pid":1234,"stack":"Error: ..."}javascript
// 统一的请求日志中间件(记录每个请求的耗时与结果)
function requestLogger(req, res, next) {
const start = Date.now();
res.on("finish", () => {
logger.info({
method: req.method,
path: req.originalUrl,
status: res.statusCode,
duration: Date.now() - start,
ip: req.ip,
}, "request");
});
next();
}| 推荐字段 | 说明 |
|---|---|
timestamp | 事件时间(统一 UTC 或固定时区) |
level / message | 级别与描述 |
service / pid | 哪个服务、哪个进程 |
requestId / traceId | 关联一次请求的完整链路 |
userId | 便于按用户排查 |
永远不要在日志中记录密码、token、身份证号等敏感信息。
六、日志文件管理
| 方案 | 工具 | 说明 |
|---|---|---|
| 按天轮转 | winston-daily-rotate-file | 一天一个文件 |
| 按大小轮转 | logrotate(Linux) | 超过阈值切割并压缩 |
| 统一采集 | PM2 日志 + pm2-logrotate | 进程日志集中管理 |
| 集中存储 | ELK / Loki / 云日志服务 | 多实例日志汇总检索 |
生产上日志不应只落在本地磁盘,应通过 stdout 输出(容器/PM2 收集)或直接上报到日志平台,保证多实例可检索。
七、PM2 进程管理
PM2 是 Node 生产进程守护的事实标准:崩溃自动重启、日志收集、负载均衡、零停机发布。
bash
npm install -g pm2
pm2 start app.js --name my-api # 启动
pm2 start app.js -i max # 集群模式:按 CPU 核数启动多实例
pm2 start app.js -i 2 --max-memory-restart 300M # 内存超 300MB 自动重启
pm2 logs my-api # 实时查看日志
pm2 logrotate # 配置日志轮转
pm2 restart my-api # 重启
pm2 reload my-api # 无缝重载(零停机)
pm2 delete my-api # 移除
pm2 monit # 监控面板(CPU/内存/请求数)javascript
// 在代码中接入 PM2 事件:记录重启原因
process.on("uncaughtException", (err) => {
console.error("未捕获异常,进程将退出:", err);
process.exit(1); // 交给 PM2 重启,保证状态干净
});| 功能 | 说明 |
|---|---|
| 守护进程 | 崩溃、OOM 自动拉起 |
| 集群模式 | 多进程共享端口,Node 单线程吃满多核 |
| 负载均衡 | 内置 round-robin 分发请求 |
| 日志收集 | stdout/stderr 统一进 ~/.pm2/logs |
| 监控面板 | pm2 monit / pm2 status |
八、错误监控
8.1 未捕获异常上报
javascript
process.on("uncaughtException", (err) => {
errorReporter.report(err); // 上报错误跟踪服务
console.error("uncaughtException:", err);
process.exit(1); // 状态未知,安全退出交由守护进程重启
});
process.on("unhandledRejection", (reason) => {
errorReporter.report(reason);
});8.2 Sentry 简介
Sentry 是应用错误跟踪平台,自动捕获未处理异常、附带堆栈与请求上下文:
bash
npm install @sentry/nodejavascript
const Sentry = require("@sentry/node");
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.2, // 采样率:兼顾性能与覆盖率
});
// 主动上报
try {
await processOrder(orderId);
} catch (error) {
Sentry.captureException(error);
}配合 @sentry/profiling-node 还能做性能采样(慢接口、慢函数定位)。
九、Node 性能分析
| 工具 | 用途 | 用法 |
|---|---|---|
--inspect | 开启调试协议,配合 Chrome DevTools | node --inspect app.js |
--cpu-prof | 生成 CPU 火焰图数据 | node --cpu-prof app.js |
clinic | 一键生成性能诊断报告 | clinic doctor -- node app.js |
node --prof | V8 内置采样 | 分析热点函数 |
bash
# CPU 分析:运行一段时间后生成 *.cpuprofile
node --cpu-prof --cpu-prof-dir=./profiles app.js
# 在 Chrome 打开 chrome://inspect 或 DevTools → Performance 面板导入分析
# clinic:自动诊断 CPU、内存、IO 瓶颈
npx clinic doctor -- node app.js十、指标监控:进程内存与 CPU
用 process.memoryUsage() 与 os 模块采集指标,暴露成 /metrics 接口供 Prometheus 抓取:
javascript
const os = require("os");
function collectMetrics() {
const mem = process.memoryUsage();
return {
process: {
pid: process.pid,
uptime: process.uptime(),
heapUsedMB: +(mem.heapUsed / 1024 / 1024).toFixed(1), // V8 堆内存
rssMB: +(mem.rss / 1024 / 1024).toFixed(1), // 常驻内存
cpuPercent: process.cpuUsage(),
},
os: {
totalMemMB: +(os.totalmem() / 1024 / 1024).toFixed(0),
freeMemMB: +(os.freemem() / 1024 / 1024).toFixed(0),
loadAvg: os.loadavg(),
},
};
}
// 定时上报(或暴露 /metrics 接口)
setInterval(() => {
const m = collectMetrics();
metricsClient.push(m); // 推送到监控系统
if (m.process.rssMB > 300) console.warn("内存偏高:", m.process.rssMB, "MB");
}, 15000);| 指标 | 含义 | 危险信号 |
|---|---|---|
heapUsed | V8 堆使用量 | 持续上涨 = 疑似内存泄漏 |
rss | 进程常驻内存 | 超阈值触发 OOM 重启 |
cpuUsage | CPU 时间 | 接近 100% 持续 → 检查热点 |
| 事件循环延迟 | 用 monitor-event-loop-delay 测量 | 延迟过高 = 阻塞代码 |
os.loadavg | 系统负载 | 配合 CPU 核数判断过载 |
监控闭环:指标采集 → 阈值报警(钉钉/邮件/短信)→ 定位日志与链路 → 修复上线。日志负责"发生了什么",指标负责"现在怎么样",两者结合才能快速止血。