第 9 章:監控與日誌 — Prometheus + Grafana

第 9 章:監控與日誌 — Prometheus + Grafana

這一章在做什麼? 我們讓 app 吐出「指標(metrics)」,用 Prometheus 定期收集,再用 Grafana 把它畫成儀表板。做完這章,你能看見服務的請求量、延遲、錯誤率——出事能即時發現,而不是等使用者來罵。

為什麼要監控? 服務上線後,「它現在健康嗎?慢不慢?有沒有在噴錯?」如果看不見,你就是瞎子開車。可觀測性就是替你的服務裝上儀表板。

💸 本章在「本機 kind」叢集做,完全免費(第 8 章的 GKE 已 destroy)。若你刪了 kind 叢集,先用第 7 章的步驟重建並把 app 部署回去。 前置:第 7 章的本機叢集與 manifest。


步驟 1:先搞懂四個名詞

名詞 白話
可觀測性 / Metrics(指標) 用數字描述服務狀態:每秒幾個請求、延遲多少、錯誤幾%。指標是「可被監控的數據」。
Prometheus 主流的監控系統。它**主動定期去「抓取(scrape)」**各服務的 /metrics 端點,把數字存成「時序資料」(隨時間變化的數列)。
Grafana 把 Prometheus 的數據畫成漂亮的儀表板與圖表,還能設警報。
Helm Kubernetes 的「套件管理器」(像 K8s 的 apt/npm)。一個「chart」打包了一整套 K8s 資源,一行指令就裝好。

流程一句話:你的 app 開一個 /metrics 端點 → Prometheus 定時去抓 → Grafana 把抓來的數據畫成圖。


步驟 2:讓 app 吐出 /metrics

我們用官方的 Prometheus Go 客戶端,加一個中介層記錄「請求數」與「請求耗時」,並開一個 /metrics 端點。

  1. 取得套件:
cd code
go get github.com/prometheus/client_golang@v1.20.5
  1. 建立 code/internal/middleware/metrics.go
// code/internal/middleware/metrics.go
package middleware

import (
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
)

// 兩個指標:請求總數(counter)與請求耗時(histogram)
var (
	httpRequestsTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "http_requests_total",
			Help: "HTTP 請求總數",
		},
		[]string{"method", "path", "status"},
	)

	httpRequestDuration = promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "http_request_duration_seconds",
			Help:    "HTTP 請求耗時(秒)",
			Buckets: prometheus.DefBuckets,
		},
		[]string{"method", "path"},
	)
)

// Metrics 中介層:替每個請求記錄次數與耗時
func Metrics() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		c.Next() // 先讓請求被處理

		// 用「路由樣板」當 path 標籤(/api/quotes/:symbol),
		// 不用實際 URL,避免標籤值爆炸(high cardinality)。
		path := c.FullPath()
		if path == "" {
			path = "unmatched"
		}
		status := strconv.Itoa(c.Writer.Status())

		httpRequestsTotal.WithLabelValues(c.Request.Method, path, status).Inc()
		httpRequestDuration.WithLabelValues(c.Request.Method, path).Observe(time.Since(start).Seconds())
	}
}

重點:

  • promauto.NewCounterVec / NewHistogramVec:定義帶標籤的指標,並自動註冊。
    • counter(只增不減)記「總請求數」;histogram 記「耗時分布」(之後能算出 p95 延遲)。
  • []string{"method","path","status"}:標籤維度,讓你能「依方法/路徑/狀態碼」切分。
  • c.FullPath()(關鍵):用路由樣板(/api/quotes/:symbol)當標籤,而不是每個實際 URL。否則像 /api/quotes/AAPL/api/quotes/TSLA… 每個都變成不同標籤值,指標會爆量(這是 Prometheus 最常見的坑,叫 high cardinality)。
  1. code/cmd/api/main.go 掛上中介層與 /metrics 端點:
// code/cmd/api/main.go(import 區追加)
	"github.com/prometheus/client_golang/prometheus/promhttp"
// code/cmd/api/main.go(在 r := gin.Default() 之後加)
	// 記錄每個請求的指標,並開一個 /metrics 端點給 Prometheus 抓取
	r.Use(middleware.Metrics())
	r.GET("/metrics", gin.WrapH(promhttp.Handler()))
  • r.Use(middleware.Metrics()):每個請求都會經過這個中介層被記錄。
  • gin.WrapH(promhttp.Handler()):把 Prometheus 官方的 HTTP handler「包」成 Gin 能用的格式,掛在 /metrics

本機先確認還能 build / 測試:

go build ./... && go test ./...
# 預期:build 無誤、internal/handler 測試 ok

步驟 3:重 build image、重新部署到叢集

叢集裡跑的是舊 image(沒有 /metrics),要更新:

cd code
docker build -t nlt-api:local .
kind load docker-image nlt-api:local --name nlt
kubectl rollout restart -n nlt deployment/nlt-api   # 讓 Pod 換成新 image
kubectl rollout status -n nlt deployment/nlt-api

確認 /metrics 有東西(port-forward app 直接看):

kubectl port-forward -n nlt svc/nlt-api 8080:80
# 另開終端機:
curl -s http://localhost:8080/metrics | grep http_requests_total | head
# 預期:看到 http_requests_total{...} 的指標輸出

步驟 4:用 Helm 裝 Prometheus + Grafana

我們用一個打包好的 chart:kube-prometheus-stack(一次裝好 Prometheus、Grafana、以及讓 Prometheus 知道「要抓誰」的 Operator)。

  1. 安裝 Helm:
brew install helm
helm version
  1. 建立 code/monitoring/values.yaml(給 chart 的自訂設定):
# code/monitoring/values.yaml
grafana:
  adminPassword: admin   # 教學用密碼;正式環境請改強密碼

prometheus:
  prometheusSpec:
    # 預設 Prometheus 只抓「自己這個 release 標記過」的 ServiceMonitor。
    # 設成 false 讓它抓「叢集裡所有」ServiceMonitor(包含我們等下建的)。
    serviceMonitorSelectorNilUsesHelmValues: false
  • serviceMonitorSelectorNilUsesHelmValues: false 是新手最容易卡的一行:不設它,Prometheus 不會理你自己寫的 ServiceMonitor,永遠抓不到 app。
  1. 安裝(會拉不少元件,第一次要等幾分鐘):
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace \
  -f code/monitoring/values.yaml

確認元件起來:

kubectl get pods -n monitoring
# 預期:prometheus、grafana、kube-state-metrics、operator 等都 Running

步驟 5:告訴 Prometheus「來抓我的 app」

Prometheus 靠一個叫 ServiceMonitor 的物件(由 Operator 提供)知道要抓哪個 Service 的 /metrics

先確認 code/k8s/app.yamlService 有標籤、且 port 有名字(ServiceMonitor 要靠它們對上):

# code/k8s/app.yaml 的 Service 改成這樣
apiVersion: v1
kind: Service
metadata:
  name: nlt-api
  namespace: nlt
  labels:
    app: nlt-api          # ★ ServiceMonitor 會用這個標籤找到本 Service
spec:
  selector:
    app: nlt-api
  ports:
    - name: http          # ★ 替 port 命名,ServiceMonitor 會引用它
      port: 80
      targetPort: 8080

再建立 code/k8s/servicemonitor.yaml

# code/k8s/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: nlt-api
  namespace: nlt
spec:
  selector:
    matchLabels:
      app: nlt-api        # 找帶這個標籤的 Service(上面那個)
  namespaceSelector:
    matchNames: [ nlt ]
  endpoints:
    - port: http          # 對應 Service 那個名為 http 的 port
      path: /metrics      # 抓 /metrics
      interval: 15s       # 每 15 秒抓一次

套用:

kubectl apply -f code/k8s/app.yaml
kubectl apply -f code/k8s/servicemonitor.yaml

逐段:selector.matchLabels 找到 app 的 Service → endpoints 指定抓它名為 http 的 port 上的 /metrics,每 15 秒一次。


✅ 如何驗證這一章成功

驗證 1:先製造一些流量

# 確保 app 的 port-forward 還開著(步驟 3);打幾十次讓指標有數據
for i in $(seq 1 50); do curl -s http://localhost:8080/health > /dev/null; done

驗證 2:Prometheus 有抓到 app(target = up)

kubectl port-forward -n monitoring svc/monitoring-kube-prometheus-prometheus 9090:9090

瀏覽器開 http://localhost:9090 → 上方 Status → Targets → 找 nlt-api,狀態應為 UP。 或在 Graph 輸入 http_requests_total 查詢,看到數據。

驗證 3:在 Grafana 看圖

kubectl port-forward -n monitoring svc/monitoring-grafana 3000:80

瀏覽器開 http://localhost:3000,帳號 admin、密碼 admin(values 設的)。進去後左側 Explore → 資料源選 Prometheus → 輸入查詢:

# 每秒請求數,依路徑與狀態碼切分
sum(rate(http_requests_total[5m])) by (path, status)

再試試 p95 延遲:

histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, path))

應該能看到曲線。(也可在 Grafana Dashboards → Import 輸入社群儀表板 ID 直接套版,例如 Go/Gin 相關的 dashboard。)

  • [ ] curl /metrics 看得到 http_requests_total
  • [ ] Prometheus 的 Targets 裡 nlt-apiUP
  • [ ] Grafana Explore 用上面的 PromQL 查得到曲線。

看日誌(順帶一提)

指標是「數字」,日誌是「事件文字」。最直接:

kubectl logs -n nlt deployment/nlt-api --tail=50
kubectl logs -n nlt deployment/nlt-api -f      # 即時跟著看

正式環境會用集中式日誌(如 Loki + Grafana)把所有 Pod 的日誌集中查詢——本教學先用 kubectl logs 即可。

收拾(免費,保持乾淨)

helm uninstall monitoring -n monitoring
kubectl delete namespace monitoring
# 要全清就連叢集一起:kind delete cluster --name nlt

🧯 常見錯誤 / 踩雷點

症狀 原因 解法
Prometheus Targets 裡沒有 nlt-api 沒設 serviceMonitorSelectorNilUsesHelmValues: false,或 ServiceMonitor 標籤對不上 確認 values 那行;ServiceMonitor 的 selector.matchLabels 要對上 Service 的 metadata.labels
Target 有但狀態 down port 名稱對不上、或路徑錯 Service 的 port 要命名 http,ServiceMonitor endpoints.port: httppath: /metrics
kind: ServiceMonitor apply 報錯(unknown kind) Operator/CRD 還沒裝好 helm install kube-prometheus-stack(它會帶 CRD),再 apply ServiceMonitor
Grafana 進不去 / 忘記密碼 沒設或記錯 `kubectl get secret -n monitoring monitoring-grafana -o jsonpath='{.data.admin-password}'
指標標籤暴量、Prometheus 吃很多記憶體 用了實際 URL 當標籤(high cardinality) c.FullPath() 路由樣板當 path 標籤(本章已這樣做)
整台電腦變很卡 kube-prometheus-stack 在小機器上吃資源 給 Docker Desktop 多一點記憶體(4GB+);或只裝 Prometheus 子集
port-forward 服務名找不到 各家 chart 的 service 名不同 kubectl get svc -n monitoring 看實際名稱再 forward

下一步 → 第 10 章:收尾 — 完整 pipeline 全貌 + 安全拆除 最後我們把前面所有環節串成一條「push → 測試 → build → 推 GHCR → 自動部署 → 監控」的完整自動化線(用 Workload Identity 讓 GitHub Actions 安全地部署到 GCP),給出更新後的架構圖,並附上一份完整的資源拆除清單,確保你不會留下任何在偷偷計費的東西。