iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0

昨天我們把 audit 的保底遮罩補好,讓「誰來過、做了什麼」這筆紀錄就算上游守門被設成 mock 也不會把原文漏進日誌。但 audit 回答的是「單一事件的合規足跡」——它一筆一筆躺在那裡,要靠人去翻。今天換一個視角:當這套分散式、非同步的系統真的上線、幾百個行員同時在用,你要怎麼一眼看清「整個系統現在好不好」?這是**可觀測性(observability)**要解的題,也是第七章的收尾;而把這一切重新縫回一起的線頭,正是 Day 9 那組從進門就發出去的 a1b2c3d4

本篇結構:

  • 上線之後,你得回答的那幾個問題
  • 用 Micrometer 把每次對話記成可切片的數字
  • correlation id:把指標和事件縫回同一次請求
  • 埋點要貫徹到每一條路徑,還有 tracing 這塊欠的帳

上線之後,你得回答的那幾個問題

開發期你盯著本機日誌,一次一個請求,看得清清楚楚。上線後完全是另一回事,你得回答的是一連串「整體」的問題:

  • 現在多少人在用?
  • 這個月燒了多少 token、花了多少錢?
  • 哪個環節變慢了?
  • 錯誤率有沒有偷偷往上爬?
  • 是哪一家供應商在拖後腿?

這些問題的共通點是——它們都不是「某一次請求對不對」,而是**「整體趨勢健不健康」**。你沒辦法靠翻日誌回答,因為日誌是離散的事件流,而你要的是可聚合、可切片、可畫成曲線的數字。沒有這層指標,營運就是盲飛:使用者已經在抱怨慢了,你還在猜是模型慢還是檢索慢。模型供應商悄悄漲價、某條檢索路徑慢慢劣化、某家供應商半夜開始狂回 5xx——這些都不會有人「報案」,它們只會反映在曲線上,看不到曲線你就只能等投訴,那已經太晚。

更麻煩的是這套系統是 reactive 的。Day 5、Day 6 講過 event loop 那一套——一條請求會在不同執行緒間流轉、被切碎、產出散落各處。在這種架構下「看清全貌」本身就比同步系統難,因為沒有一條從頭到尾的執行緒可以讓你貼著走。

用 Micrometer 把每次對話記成可切片的數字

做法是在每次 /chat 結束時,用 Micrometer(Java 生態的指標門面,等於指標界的 slf4j,底層可換 Prometheus/OTLP 等實作)記下幾組關鍵指標。重點不在「記了什麼」,而在每個指標都帶上維度標籤(dimensional metrics)——這樣同一組埋點,事後才能任意切片:

指標名 標籤(tags) 記的是什麼
portal.chat.requests agentllmmapsretrieverstatussuccess/blocked/error 每次對話一筆,可沿任一維度拆解的流量計數
portal.llm.tokens providerkindprompt/completion token 用量,輸入/輸出分開記
portal.chat.duration agentllm 單次對話延遲
portal.chat.errors agentexception 錯誤計數,可按例外型別拆

標籤是這套設計的關鍵。portal.chat.requests 這個計數器並不籠統,它能沿著 llmstatus 任意拆解成多維量。於是那些宏觀問題都變成簡單的聚合查詢:

想問的問題 怎麼聚合
上週各供應商燒了多少 token portal.llm.tokensprovider 加總;想拆輸入/輸出花費,再按 kind 細分
gemini-http 在正式環境的錯誤率 portal.chat.requestsllm=gemini-httpstatus=error 的計數,除以同條件總數
內建助理的平均延遲 portal.chat.durationagent=builtin 取平均
錯誤率為什麼爬高 portal.chat.errorsexception 拆,一眼看出是逾時、下游 5xx,還是被守門擋下的量暴增

特別留意 status 的三態:

  • success——正常完成。
  • blocked——被守門擋下(Day 16 的注入攔截、Day 19 的出境攔截)。它既不是成功也不是系統錯誤,得單獨記。
  • error——系統錯誤。

blocked 獨立出來是關鍵:不然你會把「安全機制正常運作」誤算成「系統壞了」,或反過來把它埋進 success 裡看不見。一個維度的設計,就決定你看到的是真相還是錯覺。

這四個指標裡,有三個就是經典的 RED method(token 那項是 RED 之外多加的,下面再說):

  • Rate(流量,requests
  • Errors(錯誤,errors
  • Duration(延遲,duration

這是任何線上服務都該有的基本三項。但這裡在 RED 之上多加了一個 LLM 專屬的維度:成本portal.llm.tokensprompt/completion 拆開記,因為這兩者的單價往往不同,分開才算得準。傳統 web 服務你不太需要逐請求記「這次花多少錢」,一次 DB 查詢的成本固定又微小;但 LLM 每次呼叫按 token 計費,一個失控的 prompt、一個被濫用的端點,帳單就會失控。把 token 做成可聚合的 counter,等於把「這套 AI 到底多花錢」從月底對帳單拉到即時儀表板上——你能在花錢的當下就看見它在花。Day 3 對照 LiteLLM 時提過它在回應 header 回傳 x-litellm-response-cost,兩者解的是同一件事;這邊選擇記 token 數而非直接記金額,是因為單價會變、會因供應商而異,記原始 token 最不失真,換算成錢交給儀表板那層做就好。

指標有兩個出口,服務的對象不一樣:

端點 格式 作用
GET /metrics-summary 人可讀的彙總 一打開就能用肉眼掃出「現在大概什麼狀況」
GET /actuator/prometheus 機器可讀(Prometheus 格式) 在 GCP 上由 Managed Service for Prometheus 抓取,進 Cloud Monitoring 畫儀表板、設告警

/metrics-summary 是給人臨時看一眼的,不必接監控系統就能知道個大概;/actuator/prometheus 才是長期營運的正規軍。這套跑在 GCP 上,由 Managed Service for Prometheus 定時抓那個端點,不必自己架、自己顧一台 Prometheus server。時序資料進 Cloud Monitoring,在上面畫出延遲的 p99 曲線、錯誤率折線。告警則用 alerting policy 設門檻(例如「gemini-http 錯誤率連續 5 分鐘超過 5% 就叫人」);日誌那條則收進 Cloud Logging前者解燃眉之急,後者撐長期維運,兩者並行。 /actuator/prometheus 吐的是通用的 Prometheus 格式——換到別的雲或地端,同一個端點照樣能被自架的 Prometheus+Grafana 抓,換的只是「誰來抓、儀表板擺哪」,埋點程式碼一行不動。

correlation id:把指標和事件縫回同一次請求

到這裡得把 Day 9 那條伏筆收回來。當時我說,correlation id 的價值會從「單服務日誌的分組鍵」升級成「串起 metrics、tracing 與稽核的那條線」——現在就是兌現的時候。

光有聚合數字還不夠。當儀表板的曲線告訴你「過去十分鐘錯誤率飆高」,你接下來要做的是「鑽進去看到底哪幾次請求出事、為什麼」——這時候就得從聚合的世界跳回單一事件的世界。把這兩個世界縫起來的線,就是那組 id。Day 9 提過,請求一進門就拿到一組追蹤碼(例如 a1b2c3d4),透過 logging 框架的 MDC 印進這次請求的每一行日誌、回填到回應的 X-Request-Id。今天要補上的是:這同一個 id 也貫穿了 metric 與 audit。所以一次請求的三類產出,全用同一把鑰匙串著:

# log(每一行都有 req=)
2026-06-22 09:15:03.412  INFO [req=a1b2c3d4] ChatController : dispatch agent=builtin llm=gemini-http
2026-06-22 09:15:03.661  INFO [req=a1b2c3d4] audit          : emit status=success durationMs=249

# metric(這次請求對哪幾個維度各加一筆)
portal.chat.requests{agent=builtin, llm=gemini-http, status=success} +1
portal.chat.duration{agent=builtin, llm=gemini-http} = 249ms
portal.llm.tokens{provider=gemini-http, kind=completion} += 86

# audit(這次互動的合規快照,requestId 對得起來)
{ "requestId": "a1b2c3d4", "userId": "<corpId>", "status": "success", "durationMs": 249, ... }

實戰價值就在這裡,三者用同一組 id 接力下鑽:

  1. 告警響了,你在 Cloud Monitoring 看到一批 status=error 的請求——metric 負責「發現異常、縮小範圍」,告訴你「有一批變慢/出錯了」,但說不出是哪一題。
  2. 鎖定時間窗、撈出其中一筆的 requestId,回到日誌搜 req=a1b2c3d4,整條處理鏈每一步都攤在眼前——log 負責「還原這一題」
  3. 再對到同 id 的 audit,連這次互動的脈絡都齊了——audit 負責「留痕」

這就是 Day 9 結尾說的「這一路都圍著這組從進門就發出去的 a1b2c3d4 轉」。而它在 reactive 下不會掉,是因為 Day 7 講過——身分和追蹤碼隨框架的請求上下文走、不綁在執行緒上,所以執行緒怎麼換,這把鑰匙都還在。

埋點要貫徹到每一條路徑,還有 tracing 這塊欠的帳

這套設計最該強調、也最容易被忽略的取捨是:**可觀測性不能只是「在某個地方加個監控套件」就完事,它得是一種貫徹到底的紀律。**每多一條請求路徑、每加一家供應商、每寫一筆 audit,都得問一個問題:「這條路徑有沒有同步把對應的 log/metric/audit 補上」。漏一處,曲線就有盲區——而盲區的特性是,平常你不會發現它不存在,等真的出事、想下鑽時才發現那段路根本沒埋點,無從追起;偏偏沒人盯的角落最容易壞。這筆帳,早在 Day 9 進門那一刻就欠下了:correlation id 的好處要兌現,前提是「需貫徹到每條 log/metric/audit」。這跟 Day 6 那條「reactive 的正確性靠的是被貫徹到每個角落的簡單規矩」是同一個道理——可觀測性的價值不取決於你埋得多漂亮,而取決於你埋得多齊。所以它得做成框架層級的橫切預設:

  • 進門統一生成 id。
  • /chat 結束統一記那四組 metric。
  • log pattern 統一帶 id。
  • audit 統一對齊。

這就像幫系統開一對白眼。日向一族的白眼幾乎全視角,還能看穿平常看不見的查克拉經脈——可觀測性想做的正是這個:讓一個請求在分散式系統裡流經哪些服務、卡在哪一段,變得看得見。但白眼有個著名的後頸盲點,這裡也一樣:只要有一條請求路徑沒埋點,那條就是你的盲區,平常無感,出事想追時才發現那裡根本沒開眼。所以埋點得貫徹到每一條路徑,白眼才沒有死角。

代價是維度得謹慎。多一個標籤、多一個 metric,都要先想清楚它的**基數(cardinality,標籤值的種類數)**會不會爆——一個不小心把高基數的東西(例如把 requestId 當成 metric 標籤)塞進去,時間序列會炸開、把監控系統拖垮。所以維度只選「種類有限、值得切片」的(providerstatusagent 這種),高基數的識別資訊留給 log 和 audit。這條界線恰好也是 metric 與 log 分工的本質:

管什麼 對基數的要求
metric 聚合 要低基數
log 細節 容得下唯一 id

也得誠實講清楚另一塊邊界。完整的可觀測性有三根支柱——metrics、logging、tracing。目前 metrics 和 logging 這兩根紮實,但 tracing 這根還能再強化

這裡的 tracing 指的是分散式追蹤:把「單次請求跨越 Portal、後端 Engine、知識庫」的完整時間軸串成一張瀑布圖,讓你一眼看出時間花在哪一段——是 Portal 自己慢,還是在等下游。

這正是 Day 9 提過的那道坎:correlation id 能把單一服務內部的日誌串起來,但它本身不等於分散式追蹤。現在用 correlation id 配日誌還能「人工拼」出這條時間軸,但那是靠肉眼搜 id、自行推斷先後;真正的 tracing 要有標準的 trace context(trace id/span id)隨服務對服務(s2s,Day 11)的呼叫一路傳到下游、每個服務都回報自己的 span,再由 collector 收攏。在 GCP 上的落地路徑很直接——接上 OpenTelemetry 把 trace context 自動帶下去、再送進 Cloud Trace 收成瀑布圖,框架上 Micrometer 也留了銜接的路;這是這塊明確的後續工程。現階段先靠 correlation id 撐著,夠用但不夠優雅——把話說在前面,比假裝完備誠實。

到這裡,第七章「可觀測性與稽核」就收完了:Day 24 把 audit 的資料模型立起來、Day 25 補上保底遮罩這個合規漏洞、今天用 metrics 加 correlation id 把整條路徑串成看得見的全貌。稽核回答「誰來過」,可觀測性回答「系統好不好」,而它們和散落各處的日誌,全靠 Day 9 那組 correlation id 串在同一條線上。一個分散式非同步系統能不能在真實世界長期跑下去,分水嶺往往不在它跑得多快,而在它出事時你看不看得清。

明天 Day 27,我們轉進第八章,看當供應商不再是一包打包進服務裡的函式庫、而是一個在外頭獨立跑的服務時,Portal 怎麼用 MCP 這條 Agent 軌把它接進來,以及它跟前面講的 SDK 軌差在哪。


上一篇
Day 25|audit 保護你我他
下一篇
Day 27|終於來到Agent 軌與 MCP
系列文
轉生到全端工程師沒多久就要負責公司的大平台??30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言