前一篇已經把一次 Create Todo 串成一條 Trace。不過只在 Collector 的 debug log 裡找 Trace ID,只能確認資料有送進來;要知道延遲從什麼時候開始、影響哪個服務,還是得回到時間序列和查詢介面。
這篇部署 Prometheus、Loki、Tempo 和 Grafana,讓 Todo 系統的 Metrics、Logs、Traces 有固定的查詢位置。環境刻意維持單一副本,資料放在 emptyDir,適合本機或測試叢集驗證資料流;Pod 重建後資料會消失,不能直接拿去當正式環境的儲存設計。
Todo API 和 BFF 將 OTLP 資料送到 Collector。Collector 依訊號將 Metrics、Logs 和 Traces 送往不同 backend:
:8889,Prometheus 每 15 秒 scrape 一次。為了讓 dashboard 能以服務名稱篩選,exporter 會將 resource attributes 轉為 Prometheus labels(例如 service.name 變成 service_name)。/otlp endpoint。建議僅使用 service.name 或環境等穩定欄位作為 label,避免將 trace_id、Todo title、Authorization header 或使用者資料變成 label。Grafana 啟動時會載入三個 datasource。dashboard 不需要知道叢集裡的實際 Pod IP,只使用 Kubernetes Service 名稱:
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
url: http://prometheus.observability.svc:9090
- name: Loki
uid: loki
type: loki
url: http://loki.observability.svc:3100
- name: Tempo
uid: tempo
type: tempo
url: http://tempo.observability.svc:3200
這組設定同時把 Tempo 的 Trace-to-Logs 連到 Loki。從 Trace 詳細頁點選 Logs 時,Grafana 會帶入 service.name 和 Trace ID;資料是否查得到仍取決於 application 有沒有真的送出對應 log。
第一張 dashboard 主要關注四個和 Create Todo 有關的問題:HTTP request rate、HTTP request duration p95、HTTP 5xx ratio,以及 Application logs。
service 與 route 都是 dashboard 變數。以下是 p95 panel 使用的 PromQL;http_route 是 ASP.NET Core instrumentation 提供的正規化 route,不會把每個 Todo ID 變成新的時間序列:
histogram_quantile(
0.95,
sum by (le, http_route) (
rate(
http_server_request_duration_seconds_bucket{
service_name=~"$service",
http_route=~"$route"
}[$__rate_interval]
)
)
)
5xx ratio 以相同的 counter 計算,不把 trace_id 或 request ID 放進 Metric label:
sum(
rate(
http_server_request_duration_seconds_count{
service_name=~"$service",
http_route=~"$route",
http_response_status_code=~"5.."
}[$__rate_interval]
)
)
/
sum(
rate(
http_server_request_duration_seconds_count{
service_name=~"$service",
http_route=~"$route"
}[$__rate_interval]
)
)
Dashboard JSON 將這些 panel、變數與 datasource UID 一起保存。匯入後的標題是 Todo service overview,不用再手動建立 panel:
{
"title": "Todo service overview",
"uid": "todo-service-overview",
"templating": {
"list": [
{
"name": "service",
"definition": "label_values(http_server_request_duration_seconds_count, service_name)"
},
{
"name": "route",
"definition": "label_values(http_server_request_duration_seconds_count{service_name=~\"$service\"}, http_route)"
}
]
}
}
先確認 Collector、Prometheus、Loki、Tempo 和 Grafana 都已就緒:
kubectl get deployment -n observability
開啟 Grafana:
kubectl port-forward service/grafana 3000:3000 -n observability
瀏覽器開啟 [http://127.0.0.1:3000](http://127.0.0.1:3000),以測試環境的帳密 admin/admin 登入。進入 Todo service overview 後,先選擇 todo-api 或 todo-bff。沒有資料時,確認服務確實已將 OTLP endpoint 指向 Collector,並等待至少一個 15 秒 scrape interval。
接著轉送 BFF service,建立幾筆測試資料:
kubectl port-forward service/todo-bff 8080:8080 -n todo
for title in trace-one trace-two trace-three; do
curl --fail \
-H 'content-type: application/json' \
--data "{\"title\":\"$title\"}" \
http://127.0.0.1:8080/api/todos
done
回到 dashboard 選擇 todo-api 和 /api/todos。request rate 會出現 POST 流量,todo-api 在建立 Todo 時會寫入 Todo created.,因此同一時間窗的 Application logs panel 應該會看得到三筆事件(範例中不會把 Todo title 寫進 log)。p95 和 5xx ratio 使用 rate() 計算;只有零星幾筆請求時,p95 可能暫時沒有值,請多送幾筆請求或拉長 dashboard 的時間範圍再判讀。沒有 5xx 時,5xx ratio 沒有序列可顯示,不代表查詢失敗。
dashboard 用 Metrics 確認影響範圍,Trace 則負責拆開單一請求的時間。從 Grafana 的 Explore 選擇 Tempo datasource,使用 TraceQL 找 todo-api 的 Trace:
{ resource.service.name = "todo-api" }
選擇剛剛建立的 Trace,應能看見 todo-bff 的 HTTP server span、BFF 呼叫 API 的 HTTP client span、todo-api 的 HTTP server span,以及 todo.create activity。這四段共用同一個 Trace ID。點選 Trace-to-Logs 後,Grafana 會交給 Loki 查詢同一個服務與 Trace ID 的 logs。
目前 Todo 資料保存在記憶體,因此 Trace 裡沒有資料庫 span;這不是 dashboard 漏資料。真正接上資料庫後,才需要在資料庫 client instrumentation、查詢欄位與保留政策上增加對應的設計。
這裡的 Prometheus、Loki、Tempo、Grafana 都是單一 Pod,也沒有持久化 Volume。它們的用途是確認 OpenTelemetry 資料能被接收、保存和查詢,而不是處理 production 的容量、HA、retention、帳號權限或 backup。
另外,這個 dashboard 只顯示目前服務實際產生的 HTTP metrics 和 Todo created. log。沒有資料庫、部署版本或 GitOps revision 的 telemetry 時,不能先加一個看似完整的 panel。新增這些資料來源後,再讓 dashboard 回答相對應的問題。
中秋節回老家無法連上 K8s 環境,改天再把測試圖補上,下一篇會把程式碼在合併前應通過的檢查放進 CI,避免來源無法建置或測試失敗的變更直接進入交付流程。