結論先講
Tracing 告訴你「哪裡慢了」,Log 告訴你「為什麼慢」。 兩者缺一不可。但微服務的 log 散在 N 個 container 裡,格式五花八門,你需要一套集中式 Log 系統把它們收攏到同一個地方,並且用 Correlation ID 串起來。小團隊用 Loki,大團隊用 ELK。
單體 vs 微服務的 Log 體驗
單體:一個檔案走天下
# 出問題了?
tail -f /var/log/app.log | grep "ERROR"
# 找到了
[2026-04-15 14:03:22] ERROR OrderService: insufficient stock for product 123一個檔案、一個格式、一個時區。人生美好。
微服務:五份 log 拼拼圖
# 訂單服務(Node.js,JSON 格式,UTC)
{"level":"error","msg":"stock check failed","ts":"2026-04-15T06:03:22.001Z"}
# 庫存服務(Go,text 格式,UTC+8)
2026/04/15 14:03:22 [ERROR] product 123: stock insufficient
# 通知服務(Python,自訂格式,UTC)
ERROR 2026-04-15 06:03:22,350 notification.py:42 - Failed to send notification
# API Gateway(nginx,access log 格式)
192.168.1.100 - - [15/Apr/2026:14:03:22 +0800] "POST /orders" 500 0.342
# 訊息佇列(Java,Log4j 格式)
2026-04-15 14:03:22.500 ERROR [consumer-thread-1] c.o.OrderConsumer - Processing failed五個服務、五種語言、五種 log 格式、兩個時區。你甚至不確定這五行是不是同一個請求。
第一步:統一 Log 格式
Structured Logging(JSON 格式)
所有服務統一用 JSON 輸出 log,欄位名稱一致:
{
"timestamp": "2026-04-15T06:03:22.001Z",
"level": "error",
"service": "order-service",
"trace_id": "abc-123-def-456",
"span_id": "span-789",
"message": "stock check failed",
"error": "insufficient stock",
"product_id": 123,
"user_id": 456
}必備欄位:
| 欄位 | 用途 |
|---|---|
timestamp | UTC,ISO 8601 格式 |
level | error / warn / info / debug |
service | 哪個服務 |
trace_id | 連結到 Tracing(第 43 篇) |
message | 人讀的訊息 |
error | 錯誤細節(如果有) |
各語言怎麼做
// Node.js — pino(最快的 JSON logger)
const pino = require('pino');
const logger = pino({
level: 'info',
formatters: {
level: (label) => ({ level: label }),
},
timestamp: pino.stdTimeFunctions.isoTime,
});
// 使用時帶上 context
logger.info({ userId: 456, productId: 123 }, 'creating order');
logger.error({ err, userId: 456 }, 'stock check failed');// Go — zerolog
logger := zerolog.New(os.Stdout).With().
Timestamp().
Str("service", "stock-service").
Logger()
logger.Error().
Str("trace_id", traceID).
Int("product_id", 123).
Msg("insufficient stock")# Python — structlog
import structlog
logger = structlog.get_logger()
logger.error("stock check failed",
service="notification-service",
trace_id=trace_id,
product_id=123)第二步:集中收集
方案一:Grafana Loki(推薦小中團隊)
你的服務 → stdout(JSON)
↓
Docker log driver / Promtail
↓
Loki(只索引 label,不索引全文)
↓
Grafana(查詢 + 展示)
Loki 的設計哲學是「like Prometheus, but for logs」——它不像 Elasticsearch 那樣索引每個欄位,只索引 label(service name、level 等),全文搜尋用暴力掃描。
優點:資源佔用極低、與 Grafana 原生整合、S3 存儲成本低 缺點:全文搜尋慢(大量 log 時)、不適合複雜查詢
# docker-compose.logging.yml
services:
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
volumes:
- ./loki-config.yaml:/etc/loki/config.yaml
promtail:
image: grafana/promtail:latest
volumes:
- /var/log:/var/log
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./promtail-config.yaml:/etc/promtail/config.yaml方案二:ELK Stack(大團隊 / 複雜查詢)
你的服務 → stdout(JSON)
↓
Filebeat / Fluentd
↓
Elasticsearch(全文索引)
↓
Kibana(查詢 + 展示)
優點:全文搜尋極快、複雜查詢(聚合、統計)強大 缺點:Elasticsearch 吃資源(建議最少 4GB RAM)、維護成本高
怎麼選
| 維度 | Loki | ELK |
|---|---|---|
| 資源佔用 | 低(< 1GB RAM) | 高(4GB+ RAM) |
| 全文搜尋 | 慢 | 快 |
| 複雜查詢 | 有限 | 強大 |
| 成本 | 低 | 高 |
| 與 Grafana 整合 | 原生 | 需設定 |
| 適合規模 | < 100GB log/天 | 任意規模 |
5 個服務以下,先用 Loki。 等 log 量大到 Loki 查詢太慢,再考慮遷移到 ELK。
第三步:Correlation ID 串起一切
Correlation ID(或叫 Request ID)是貫穿整個請求的唯一識別碼。跟 第 43 篇 的 Trace ID 是同一個概念——差別是 Trace ID 由 Tracing SDK 產生,Correlation ID 可以手動產生。
最佳實務:直接用 Trace ID 當 Correlation ID
// Express middleware
const { trace } = require('@opentelemetry/api');
app.use((req, res, next) => {
const span = trace.getActiveSpan();
const traceId = span?.spanContext().traceId || crypto.randomUUID();
// 注入到 request context
req.traceId = traceId;
// 注入到 response header(方便前端 debug)
res.setHeader('X-Trace-Id', traceId);
// logger 自動帶上
req.logger = logger.child({ trace_id: traceId });
next();
});這樣當使用者回報「下單失敗」,前端可以把 X-Trace-Id 給你,你直接在 Loki / Kibana 搜:
{service=~".+"} | json | trace_id = "abc-123-def-456"
所有服務、所有相關的 log 一次撈出來,按時間排序。
Log Level 怎麼訂
| Level | 什麼時候用 | 生產環境開不開 |
|---|---|---|
error | 系統無法正常完成操作 | 永遠開 |
warn | 不影響功能但需要注意(快沒空間、連線池快滿) | 永遠開 |
info | 重要業務事件(訂單成立、付款完成) | 永遠開 |
debug | 開發時的細節(SQL 查詢、HTTP request body) | 只在 debug 時開 |
trace | 極度詳細(每一行程式碼的執行狀態) | 永遠不開 |
生產環境用 info 以上。 debug level 在微服務裡開下去,log 量會爆炸——5 個服務每秒各吐 100 行 debug log,一天就是 4300 萬行。
動態調整 Log Level
好的做法是支援不重啟就能調整 log level:
// 透過環境變數或 API 調整
app.post('/admin/log-level', (req, res) => {
const { level } = req.body;
logger.level = level; // pino 支援動態調整
res.json({ ok: true, level });
});某個服務出問題時,把那個服務的 log level 暫時調到 debug,看完再調回來。
常見的坑
1. Log 裡塞了敏感資料
// 千萬不要
{"message": "login failed", "password": "abc123", "credit_card": "4111-1111-1111-1111"}
// 應該要
{"message": "login failed", "user_id": 456, "ip": "192.168.1.100"}用 pino 的 redact 功能自動過濾:
const logger = pino({
redact: ['password', 'creditCard', 'token', 'authorization'],
});2. Log 量太大撐爆硬碟
設定 retention policy:
# Loki: 7 天後自動刪
limits_config:
retention_period: 168h
# Elasticsearch: 14 天後自動刪(用 ILM policy)3. 不同服務的時區不一致
全部用 UTC。 不要在 log 裡用 local time。顯示給人看的時候再轉時區。
跟 Tracing 怎麼串
最終目標:在 Grafana 裡點一筆 Trace,直接跳到對應的 Log。
Grafana → Tempo(Tracing) → 點某個 Span
↓
自動帶 trace_id 跳到 Loki(Log)
↓
看到該 Span 期間所有的 log
這就是可觀測性三大支柱(Traces + Logs + Metrics)互相連結的威力。Metrics 的部分我們留到下一篇。
下一篇
監控 Dashboard 該看什麼指標 — Trace 找到瓶頸、Log 找到原因,但你不可能每次都手動去查。Dashboard 讓你「一眼」就知道系統健不健康——但 Dashboard 上該放什麼指標?放錯了比沒有更糟。
本系列文章
完整 68 篇目錄見 系列首頁
← 上一篇:可觀測性(一):分散式 Tracing 怎麼追一個請求 → 下一篇:可觀測性(三):監控 Dashboard 該看什麼指標