🎯 什麼情境該想到我

當你「想知道一則訊息一路上經過了哪些元件,用來除錯或分析流向」的時候。

⚙️ 怎麼用(步驟 / 公式)

意圖:在訊息本身累積記錄它所經過的每個元件,形成一份可隨訊息攜帶的歷史軌跡。

做法 / 結構要點:

  • 每個處理元件在轉出訊息前,把自己的識別附加到訊息的歷史欄位(通常在標頭)。
  • 歷史隨訊息一起流動。
  • 需要時讀取歷史,即可看出這則訊息的完整路徑。

★★ 今天的答案:用 W3C Trace Context,不要自己發明

★ EIP 的原始做法:訊息裡累積一個「經過的元件」清單
   → ★ 訊息會越走越大;而且只有你自己的系統看得懂

★★ 今天的做法:W3C traceparent / tracestate
   traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
                ↑版本 ↑trace-id(整條鏈相同)      ↑span-id(這一跳)  ↑flags
   → ★ OpenTelemetry、所有主流 APM 直接認得
   → ★★ 而且它是「固定長度」的 —— 不會像清單那樣越走越大

★★ 關鍵洞見:EIP 的「歷史清單」把路徑存在訊息裡;
現代追蹤把路徑存在 APM,訊息裡只帶一個指標(trace id + 當前 span id)。

後者解掉了「訊息越走越大」這個根本問題。

★ Go 的實作:把 trace 傳播做進閘道,不要讓開發者自己記

// ★ 發送端:把當前 span 的 context 注入訊息標頭
func (g *Gateway) Publish(ctx context.Context, topic string, msg any) error {
    carrier := propagation.MapCarrier{}
    otel.GetTextMapPropagator().Inject(ctx, carrier)     // ★ 寫入 traceparent
    return g.write(ctx, topic, withHeaders(msg, carrier))
}
 
// ★ 消費端:從標頭還原 context,並開一個新的 span
func (a *Activator) Handle(ctx context.Context, msg Message) error {
    ctx = otel.GetTextMapPropagator().Extract(ctx, headersOf(msg))
    ctx, span := tracer.Start(ctx, "handle "+msg.Topic,
        trace.WithSpanKind(trace.SpanKindConsumer))      // ★ 標成 consumer span
    defer span.End()
    …
}

★★ 做進 訊息閘道 是唯一可行的做法:
如果要求每個開發者在每個 publish 處自己傳 trace,一定會漏——
而漏一處,整條因果鏈就斷在那裡。

★★ 但 trace 不能取代 correlation id

trace-id★ correlation-id
取樣★★ 通常只留 1%(成本考量)★ 必須 100%
保留期★ 幾天到幾週★ 要能查幾個月前
用途效能分析、找瓶頸★★ 業務追溯(「這筆訂單為什麼沒出貨」)
在日誌裡有★ 必須有

★★ 兩者都要。 只有 trace → 客服問「上個月那筆訂單」時你什麼都查不到
(1% 取樣 + 兩週保留 = 幾乎確定不在)。
→ 見 關聯識別碼。

★★ 稽核歷史是第三件事,不要和上面兩者混

★ 三種「歷史」,需求完全不同:
① trace   → 效能分析。★ 可取樣、可短期保留
② correlation → 業務追溯。★ 100%、長期,但只是一個 ID
③ ★★ 稽核歷史 → 「這則訊息在 10:03 被 X 服務處理,結果是 Y」
   → ★ 必須完整、必須不可篡改、保留期可能是數年
   → ★★ 這不該放在訊息裡累積,而該是每個處理站「發一則稽核事件」
// ★ 稽核事件:每站處理完就發一則,而不是在訊息裡累積
type AuditEvent struct {
    CorrelationID string    `json:"correlation_id"`
    MessageID     string    `json:"message_id"`
    Component     string    `json:"component"`     // ★ 誰處理的
    Version       string    `json:"version"`       // ★ 哪一版的程式碼
    Outcome       string    `json:"outcome"`       // ok / rejected / failed
    At            time.Time `json:"at"`
}

★★ 這個做法勝過「在訊息裡累積清單」的三個理由:
訊息大小不變、稽核資料可以有自己的保留期與權限、而且失敗的處理也記得下來
(訊息被丟棄時,累積在訊息裡的歷史也一起消失了)。

★ 兄弟辨析

差別
關聯識別碼★ 只是一個 ID;訊息歷史是「經過了哪些站」
路由單★ 記「還要去哪」(往前看);訊息歷史記「已經過哪」(往回看)
竊聽器取得訊息複本的機制
智慧代理記錄請求-回覆的配對

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 歷史欄位會隨路徑變長,增加訊息體積。

  • 是分散式、動態路由下追蹤流向的輕量做法。

  • 只記「經過哪些元件」,非完整內容稽核(那用訊息存放區)。

  • ★★ 用 W3C traceparent,不要自己發明歷史清單。 它固定長度,不會讓訊息越走越大,而且 OTel 與所有 APM 都認得。

  • ★★ trace 傳播必須做進閘道。要求每個開發者手動傳,漏一處整條因果鏈就斷。

  • ★★ trace 不能取代 correlation id:trace 通常 1% 取樣、保留幾週;業務追溯需要 100% 且長期。

  • ★★ 稽核歷史是第三件事。不要在訊息裡累積清單——改成「每站發一則稽核事件」。

  • ★ 在訊息裡累積歷史的三個問題:訊息越走越大、無法有獨立的保留期與權限、訊息被丟棄時歷史也一起消失。

  • ★ 稽核事件要記 Version(哪一版程式碼處理的),否則事後無法判斷是哪次部署造成的。

  • 每一行日誌都要帶 correlation id,否則它只存在於訊息裡,查事故時用不到。

  • 歷史紀錄也含 PII,適用 竊聽器 的同一個警告。

🔗 相關工具