兩種 Log 的差距

出問題的時候,你去 log 系統搜尋,看到:

非結構化 log(不好用)

2026-04-22 10:30:15 User login failed: invalid password
2026-04-22 10:30:16 GET /api/users 200 45ms
2026-04-22 10:30:16 Database error: connection timeout

你知道「有個 DB connection timeout」,但不知道:是哪個 request 造成的?這個 request 是哪個 user?前後發生了什麼?

結構化 log(好用)

{
  "timestamp": "2026-04-22T10:30:16.000Z",
  "level": "error",
  "message": "Database connection timeout",
  "requestId": "req-abc123",
  "userId": "user-456",
  "path": "/api/orders",
  "method": "POST",
  "duration": 3041,
  "query": "INSERT INTO orders...",
  "service": "order-service",
  "environment": "production"
}

同一個 requestId 可以把這個請求的所有 log 串起來——從進來、查 DB、呼叫外部 API、回傳 response,全程可追蹤。


Correlation ID 的全鏈傳遞

Correlation ID(也叫 Request ID / Trace ID)是「這個請求的唯一識別碼」,要從請求進入的那一刻開始生成,然後帶到所有的 log、所有的下游服務呼叫。

import { AsyncLocalStorage } from 'async_hooks';
import { v4 as uuidv4 } from 'uuid';
 
// AsyncLocalStorage 讓 correlation ID 在同一個 async context 中自動傳遞
// 不需要手動把 requestId 傳進每個 function
const requestContext = new AsyncLocalStorage<{ requestId: string; userId?: string }>();
 
// Middleware:生成或讀取 correlation ID
export const correlationMiddleware = (req, res, next) => {
  // 如果上游(API Gateway / Load Balancer)已經加了 X-Request-Id,繼承它
  const requestId = (req.headers['x-request-id'] as string) || uuidv4();
 
  req.requestId = requestId;
  res.set('X-Request-Id', requestId);  // 回傳給 client,方便 debug
 
  // 把 context 注入 async storage,後續所有的 async call 都能取得
  requestContext.run({ requestId }, next);
};
 
// Logger:自動注入 context
export const logger = {
  info: (message: string, meta?: object) => {
    const ctx = requestContext.getStore();
    console.log(JSON.stringify({
      timestamp: new Date().toISOString(),
      level: 'info',
      message,
      requestId: ctx?.requestId,
      userId: ctx?.userId,
      ...meta,
    }));
  },
  error: (message: string, meta?: object) => {
    const ctx = requestContext.getStore();
    console.error(JSON.stringify({
      timestamp: new Date().toISOString(),
      level: 'error',
      message,
      requestId: ctx?.requestId,
      userId: ctx?.userId,
      ...meta,
    }));
  },
};
 
// 使用:不用手動傳 requestId
class UserService {
  async createUser(dto: CreateUserDto) {
    logger.info('Creating user', { email: dto.email });  // requestId 自動注入
 
    try {
      const user = await userRepo.create(dto);
      logger.info('User created', { userId: user.id });
      return user;
    } catch (error) {
      logger.error('Failed to create user', { error: error.message });
      throw error;
    }
  }
}

各層要 Log 什麼

Request 進入 / 離開(Middleware 層)

export const requestLogger = (req, res, next) => {
  const start = Date.now();
 
  res.on('finish', () => {
    logger.info('Request completed', {
      method: req.method,
      path: req.path,
      statusCode: res.statusCode,
      duration: Date.now() - start,
      userAgent: req.headers['user-agent'],
      ip: req.ip,
    });
  });
 
  next();
};

Service 層的業務事件(不是每個 function 都要 log,只 log 業務關鍵路徑):

// ✅ 值得 log 的業務事件
logger.info('Order placed', { orderId: order.id, amount: order.amount, userId });
logger.info('Payment processed', { paymentId, provider: 'stripe', amount });
logger.warn('Rate limit exceeded', { userId, path: req.path });
logger.error('Payment failed', { error: error.message, userId, amount });
 
// ❌ 不值得 log 的細節(太吵)
logger.info('Calling findById');
logger.info('findById returned');
logger.info('Checking if user exists');

慢查詢(Database 層):

sequelize.addHook('afterQuery', (options) => {
  const duration = /* query duration */;
  if (duration > 1000) {  // 超過 1 秒的查詢
    logger.warn('Slow query detected', {
      query: options.sql,
      duration,
      bind: options.bind,
    });
  }
});

Log Level 策略

標準是 7 個等級(由低到高):

TRACE  → 極細節追蹤(特定 code path 的每一步,幾乎只在本地排查特定 bug 時開)
DEBUG  → 開發用資訊(每次 DB 查詢、middleware 執行、變數值)
INFO   → 業務關鍵事件(訂單建立、用戶登入、payment 成功)
WARN   → 值得注意但不緊急(rate limit 觸發、deprecated endpoint 被用、慢查詢)
ERROR  → 需要處理的問題(DB 查詢失敗、外部 API 噴錯、payment 失敗)
FATAL  → 應用即將崩潰(無法取得 DB connection pool、config 驗證失敗、OOM)
SILENT → 關閉所有 log(測試環境可用)

FATAL vs ERROR 的差異:ERROR 是「這個 request 失敗了,但應用還活著繼續服務」;FATAL 是「整個 process 無法繼續,準備退出」。大部分 logger 在呼叫 logger.fatal() 後會 flush 然後 process.exit(1)

各環境的 log level

development  → DEBUG(看到所有細節)
staging      → INFO(和 prod 行為一致,不要 DEBUG 噪音)
production   → INFO(只留業務事件和警告;特定問題排查時可臨時調低)

pino 和 winston 都支援 runtime 動態調整 log level(透過 HTTP endpoint 或 signal),不需要重啟 server。


三種 Log 類型:Access / Error / Audit

多數人把所有 log 混在一起,實際上有三種目的完全不同的 log:

Access Log(請求日誌)

記錄每一個進來的 HTTP request,格式接近 Apache/Nginx 的 access.log:

{
  "type": "access",
  "requestId": "req-abc123",
  "method": "POST",
  "path": "/api/orders",
  "statusCode": 201,
  "duration": 45,
  "ip": "203.0.113.1",
  "userAgent": "Mozilla/5.0 ...",
  "userId": "user-456"
}

量大、結構固定——適合放 Loki 用 LogQL 查詢;用來做 QPS 統計、慢請求分析。Express 的 morgan 就是在做這件事,但建議用結構化 JSON 而不是預設的 Apache 格式字串。

Error Log(錯誤日誌)

記錄非預期的例外和系統錯誤,需要包含 stack trace:

{
  "type": "error",
  "requestId": "req-abc123",
  "level": "error",
  "message": "Database connection timeout",
  "stack": "Error: connect ETIMEDOUT\n    at ...",
  "userId": "user-456",
  "path": "/api/orders"
}

Error log 要接告警——ERROR 和 FATAL 等級的 log 應該觸發 Slack 通知或 PagerDuty。WARN 視情況。

Audit Log(稽核日誌)

記錄「誰對哪筆資料做了什麼」,用於安全合規和事後追查:

{
  "type": "audit",
  "actor": { "userId": "user-456", "email": "alice@example.com", "ip": "203.0.113.1" },
  "action": "user.update",
  "resource": { "type": "user", "id": "user-789" },
  "changes": { "role": { "before": "viewer", "after": "admin" } },
  "timestamp": "2026-04-22T10:30:00.000Z",
  "requestId": "req-abc123"
}

Audit log 的特殊要求:

  • 不能被刪除或修改(append-only)——可以存進獨立的 DB table 或 write-once storage
  • retention 通常更長(一般 log 30 天,audit log 可能要 1–7 年,視合規要求)
  • 不一定要存進 log 系統——很多場景存進 DB table 更容易查詢

Express proto 的 audit.ts middleware 就是在做這個:每個寫操作(POST/PUT/PATCH/DELETE)自動記錄 actor + action + resource。

這三種 log 的分工

類型觸發時機主要用途Retention
Access每個 HTTP request流量分析、debug7–30 天
Error非預期例外告警、incident 排查30–90 天
Audit寫操作、敏感讀取合規、安全追查1–7 年

Log Sink:Log 要送到哪裡

開發環境  → stdout(直接看終端機)
CI/CD    → stdout(CI 系統收集)
K8s 生產  → stdout → Fluent Bit → Loki / Elasticsearch

Kubernetes 的標準做法是讓應用只寫 stdout,由 log 收集器(Fluent Bit、Filebeat)負責把 log 送到集中的 log 系統(Grafana Loki、ELK Stack)。應用不管 log rotation、不管 log 壓縮——這些是 infra 的責任。


各框架的 Structured Logging 工具

框架推薦工具特點
Express / Node.jspino(最快)或 winstonpino 序列化快 5x;winston 自訂性高
FastAPIPython 標準 logging + python-json-logger內建 logging 搭配 JSON formatter
NestJS內建 Logger 可替換;pino 也支援nestjs-pino 直接整合
Spring BootLogback(預設)+ Logstash encoderlogstash-logback-encoder 輸出 JSON
LaravelMonolog(內建)channel 設定為 singledaily

延伸閱讀