日志系统设计:从打 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高性能
bunyanJSON 原生
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"

日志规范

必须包含的字段

字段说明
timestampISO 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. 高性能:异步写入,不影响业务

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