日志系统设计:从打 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');
}

总结

好的日志系统:

  1. 结构化:JSON 格式,易查询
  2. 可追踪:Trace ID 贯穿全链路
  3. 有告警:自动发现问题
  4. 可聚合:集中存储、集中查询
  5. 高性能:异步写入,不影响业务

日志是排查问题的关键,值得投入。