日志系统设计:从打 log 到可观测
日志系统从”能用”到”好用”,走了不少弯路。
日志的层次
| 级别 | 说明 | 场景 |
|---|---|---|
| DEBUG | 调试信息 | 开发环境 |
| INFO | 常规信息 | 生产环境 |
| WARN | 警告信息 | 需要关注 |
| ERROR | 错误信息 | 需要处理 |
| FATAL | 致命错误 | 系统崩溃 |
结构化日志
不要打字符串,打 JSON:
// 不好
console.log(`User ${userId} logged in at ${new Date()}`);
// 好
logger.info('User logged in', {
userId,
timestamp: new Date().toISOString(),
ip: req.ip,
userAgent: req.headers['user-agent'],
});
输出:
{
"level": "info",
"message": "User logged in",
"userId": "123",
"timestamp": "2025-11-25T10:30:00.000Z",
"ip": "192.168.1.1",
"userAgent": "Mozilla/5.0...",
"service": "auth-service",
"env": "production"
}
日志库选择
Node.js
| 库 | 特点 |
|---|---|
| winston | 功能全面 |
| pino | 高性能 |
| bunyan | JSON 原生 |
| log4js | 分级输出 |
使用 Pino
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' }
: undefined,
});
// 使用
logger.info({ userId: 123 }, 'User logged in');
logger.error({ err: error }, 'Database connection failed');
请求追踪
Trace ID
每个请求有唯一 ID,贯穿整个链路:
// 中间件生成 trace ID
app.use((req, res, next) => {
req.traceId = req.headers['x-trace-id'] || generateId();
res.setHeader('x-trace-id', req.traceId);
next();
});
// 日志中包含 trace ID
logger.info({ traceId: req.traceId, userId }, 'Processing request');
传递到下游
async function callDownstream(url, req) {
const response = await fetch(url, {
headers: {
'x-trace-id': req.traceId,
},
});
return response.json();
}
日志聚合
ELK Stack
应用 → Filebeat → Logstash → Elasticsearch → Kibana
配置示例:
# logstash.conf
input {
beats {
port => 5044
}
}
filter {
json {
source => "message"
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
}
}
Loki
更轻量的选择:
# promtail.yml
scrape_configs:
- job_name: app
static_configs:
- targets:
- localhost
labels:
job: app
__path__: /var/log/app/*.log
查询(LogQL):
{job="app"} |= "error" | json | level="error"
日志规范
必须包含的字段
| 字段 | 说明 |
|---|---|
| timestamp | ISO 8601 格式 |
| level | 日志级别 |
| message | 事件描述 |
| traceId | 请求追踪 ID |
| service | 服务名称 |
| env | 环境 |
不要做的事
| 禁止 | 原因 |
|---|---|
| 打印敏感信息 | 安全风险 |
| 打印大对象 | 影响性能 |
| 在循环中打日志 | 日志爆炸 |
| 用字符串拼接 | 解析困难 |
敏感信息处理
function sanitize(obj) {
const sensitive = ['password', 'token', 'creditCard'];
const copy = { ...obj };
for (const key of sensitive) {
if (copy[key]) {
copy[key] = '***REDACTED***';
}
}
return copy;
}
logger.info({ data: sanitize(requestBody) }, 'Request received');
告警设计
告警规则
# 错误率告警
- alert: HighErrorRate
expr: rate(log_entries{level="error"}[5m]) > 10
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
# 特定错误告警
- alert: DatabaseConnectionFailed
expr: count_over_time({level="error"} |= "Database connection failed" [5m]) > 3
labels:
severity: critical
告警分级
| 级别 | 响应时间 | 通知方式 |
|---|---|---|
| P0 致命 | 立即 | 电话 + 短信 |
| P1 严重 | 15 分钟 | 短信 + 即时通讯 |
| P2 一般 | 1 小时 | 即时通讯 |
| P3 低 | 工作时间 | 邮件 |
日志查询技巧
Kibana KQL
# 查错误日志
level: "error"
# 查特定用户
userId: "123"
# 组合查询
level: "error" AND service: "payment"
# 通配符
message: "timeout*"
Loki LogQL
# 基本过滤
{app="backend"} |= "error"
# JSON 解析后过滤
{app="backend"} | json | level = "error"
# 聚合统计
sum by (service) (count_over_time({level="error"}[1h]))
性能考虑
异步写入
const { createLogger, transports } = require('winston');
const logger = createLogger({
transports: [
new transports.File({
filename: 'app.log',
format: format.json(),
}),
],
});
// 生产环境添加异步传输
if (process.env.NODE_ENV === 'production') {
logger.add(new transports.Http({
host: 'log-collector',
port: 8080,
path: '/logs',
}));
}
采样
高流量场景,采样记录:
const SAMPLE_RATE = 0.1; // 10%
function shouldLog() {
return Math.random() < SAMPLE_RATE;
}
if (shouldLog()) {
logger.debug({ ... }, 'Detailed debug info');
}
总结
好的日志系统:
- 结构化:JSON 格式,易查询
- 可追踪:Trace ID 贯穿全链路
- 有告警:自动发现问题
- 可聚合:集中存储、集中查询
- 高性能:异步写入,不影响业务
日志是排查问题的关键,值得投入。