@vincent119/go-copilot-rules 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,684 @@
1
+ ---
2
+ name: go-observability
3
+ description: |
4
+ Go 可觀測性規範:結構化日誌(zap/slog)、Prometheus Metrics 規範、OpenTelemetry 整合、
5
+ Context 傳遞與 Trace ID 串接、日誌等級管理、Metrics 命名慣例。
6
+
7
+ **適用場景**:實作結構化日誌、設計 Prometheus Metrics、整合 OpenTelemetry Tracing、
8
+ 配置日誌欄位、實作 Context 傳遞、分散式追蹤、監控告警。
9
+
10
+ **關鍵字**:logging, structured logging, zap, slog, prometheus, metrics, counter, gauge,
11
+ histogram, opentelemetry, tracing, trace id, span, observability, monitoring, alerting
12
+ ---
13
+
14
+ # Go 可觀測性規範
15
+
16
+ > **相關 Skills**:本規範建議搭配 `go-grpc`(gRPC Interceptor)與 `go-http-advanced`(HTTP Middleware)
17
+
18
+ ---
19
+
20
+ ## 結構化日誌(Structured Logging)
21
+
22
+ ### 推薦套件
23
+
24
+ 1. **[vincent119/zlogger](https://github.com/vincent119/zlogger)** - 基於 zap 的簡化封裝(**推薦**)
25
+ 2. **[uber-go/zap](https://github.com/uber-go/zap)** - 高效能、低分配
26
+ 3. **[log/slog](https://pkg.go.dev/log/slog)** - Go 1.21+ 標準庫
27
+
28
+ **原則**:
29
+ - 使用**結構化日誌**,禁止純字串拼接
30
+ - 固定欄位:`trace_id`, `request_id`, `user_id`, `subsystem`
31
+ - 使用 Context 方法自動注入追蹤欄位
32
+ - 錯誤使用 `zlogger.Err(err)` 而非字串拼接
33
+
34
+ ### zlogger 基本使用(推薦)
35
+
36
+ ```go
37
+ package main
38
+
39
+ import (
40
+ "github.com/vincent119/zlogger"
41
+ )
42
+
43
+ func main() {
44
+ // 初始化(使用預設配置)
45
+ zlogger.Init(nil)
46
+ defer zlogger.Sync()
47
+
48
+ // 基本日誌
49
+ zlogger.Info("伺服器啟動", zlogger.String("port", "8080"))
50
+ zlogger.Debug("除錯訊息", zlogger.Int("count", 42))
51
+
52
+ // 錯誤日誌
53
+ if err := someOperation(); err != nil {
54
+ zlogger.Error("操作失敗", zlogger.Err(err)) // ✅ 正確
55
+ // ❌ 錯誤:zlogger.Error(fmt.Sprintf("操作失敗: %v", err))
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### 自訂配置
61
+
62
+ ```go
63
+ package main
64
+
65
+ import (
66
+ "github.com/vincent119/zlogger"
67
+ )
68
+
69
+ func main() {
70
+ cfg := &zlogger.Config{
71
+ Level: "info", // debug, info, warn, error
72
+ Format: "json", // json 或 console
73
+ Outputs: []string{"console", "file"},
74
+ LogPath: "./logs",
75
+ FileName: "app.log",
76
+ AddCaller: true,
77
+ Development: false,
78
+ ColorEnabled: true, // Console 顏色輸出
79
+ }
80
+ zlogger.Init(cfg)
81
+ defer zlogger.Sync()
82
+ }
83
+ ```
84
+
85
+ ### 從設定檔載入
86
+
87
+ ```yaml
88
+ # config.yaml
89
+ log:
90
+ level: debug
91
+ format: json
92
+ outputs:
93
+ - console
94
+ - file
95
+ log_path: ./logs
96
+ file_name: app.log
97
+ add_caller: true
98
+ development: false
99
+ color_enabled: true
100
+ ```
101
+
102
+ ```go
103
+ import (
104
+ "github.com/spf13/viper"
105
+ "github.com/vincent119/zlogger"
106
+ )
107
+
108
+ type Config struct {
109
+ Log zlogger.Config `yaml:"log"`
110
+ }
111
+
112
+ func main() {
113
+ viper.SetConfigFile("config.yaml")
114
+ viper.ReadInConfig()
115
+
116
+ var cfg Config
117
+ viper.Unmarshal(&cfg)
118
+
119
+ zlogger.Init(&cfg.Log)
120
+ defer zlogger.Sync()
121
+ }
122
+ ```
123
+
124
+ ### Context 支援(自動追蹤)
125
+
126
+ ```go
127
+ import (
128
+ "context"
129
+ "github.com/vincent119/zlogger"
130
+ )
131
+
132
+ func HandleRequest(ctx context.Context) {
133
+ // 建立帶追蹤資訊的 Context
134
+ ctx = zlogger.WithRequestID(ctx, "req-123")
135
+ ctx = zlogger.WithUserID(ctx, 12345)
136
+ ctx = zlogger.WithTraceID(ctx, "trace-abc")
137
+
138
+ // 使用 Context 方法,自動帶入追蹤欄位
139
+ zlogger.InfoContext(ctx, "處理請求",
140
+ zlogger.String("action", "login"),
141
+ )
142
+
143
+ if err := processUser(ctx); err != nil {
144
+ // 自動包含 request_id, user_id, trace_id
145
+ zlogger.ErrorContext(ctx, "處理失敗", zlogger.Err(err))
146
+ }
147
+ }
148
+
149
+ func processUser(ctx context.Context) error {
150
+ // Context 欄位自動繼承
151
+ zlogger.DebugContext(ctx, "查詢使用者資料",
152
+ zlogger.String("table", "users"),
153
+ )
154
+ return nil
155
+ }
156
+ ```
157
+
158
+ ### 日誌等級規範
159
+
160
+ | 等級 | 使用時機 | 範例 |
161
+ |------|----------|------|
162
+ | **Debug** | 開發除錯資訊 | 變數值、中間狀態 |
163
+ | **Info** | 一般資訊 | 請求開始、完成、配置載入 |
164
+ | **Warn** | 警告(不影響功能) | 重試、備援機制觸發、參數預設值 |
165
+ | **Error** | 錯誤(影響功能) | 請求失敗、DB 錯誤、外部 API 失敗 |
166
+ | **Fatal** | 致命錯誤(程式終止) | 僅限 `main()` 初始化階段 |
167
+
168
+ **禁止**:
169
+ - 不在 library 或 handler 使用 `logger.Fatal()`(會跳過 defer 與資源清理)
170
+ - 不在迴圈內使用 `logger.Debug()`(效能問題)
171
+
172
+ ---
173
+
174
+ ## Prometheus Metrics 規範
175
+
176
+ ### 核心原則
177
+
178
+ #### Metric 類型
179
+
180
+ | 類型 | 特性 | 適用場景 |
181
+ |------|------|----------|
182
+ | **Counter** | **僅能增長 (Increment)**,不可減少 | 請求總數、錯誤總數、任務完成次數 |
183
+ | **Gauge** | 可增減 | 當前記憶體用量、Goroutine 數量、Queue 長度 |
184
+ | **Histogram** | 數值分佈統計 | 請求延遲 (Latency)、Payload 大小 |
185
+ | **Summary** | 類似 Histogram(較少使用) | 客戶端計算百分位數 |
186
+
187
+ #### 命名慣例
188
+
189
+ **格式**:`<namespace>_<subsystem>_<name>_<unit>`
190
+
191
+ **規則**:
192
+ - 使用蛇形命名法 (Snake Case):`http_requests_total`
193
+ - **必須**包含單位後綴:
194
+ - `_seconds` - 延遲、時間
195
+ - `_bytes` - 大小
196
+ - `_total` - 計數(Counter 專用)
197
+ - `_ratio` - 比率(0-1 之間)
198
+
199
+ **範例**:
200
+ ```
201
+ ✅ 正確
202
+ http_requests_total
203
+ http_request_duration_seconds
204
+ db_query_duration_seconds
205
+ cache_hit_ratio
206
+
207
+ ❌ 錯誤
208
+ httpRequestsCount # 應用蛇形命名
209
+ request_latency # 缺少單位
210
+ total_requests # total 應在結尾
211
+ ```
212
+
213
+ ### Label 規範
214
+
215
+ **原則**:
216
+ - **禁止**高基數 (High Cardinality) 值(如 `user_id`, `email`, `trace_id`),避免搞垮 Prometheus
217
+ - 必備 Label:`service` (服務名), `env` (環境), `code` (錯誤碼/狀態碼)
218
+ - Label 數量建議 ≤ 5 個
219
+
220
+ **範例**:
221
+ ```go
222
+ // ✅ 正確:低基數 Labels
223
+ http_requests_total{method="GET", status="200", service="api", env="prod"}
224
+
225
+ // ❌ 錯誤:高基數 Labels
226
+ httpGin Middleware 整合(zlogger)
227
+
228
+ ```go
229
+ package middleware
230
+
231
+ import (
232
+ "time"
233
+ "github.com/gin-gonic/gin"
234
+ "github.com/google/uuid"
235
+ "github.com/vincent119/zlogger"
236
+ )
237
+
238
+ const (
239
+ LogFieldsKey = "log_fields"
240
+ LogSkipKey = "log_skip"
241
+ )
242
+
243
+ // SetLogFields 設定自訂日誌欄位
244
+ func SetLogFields(c *gin.Context, fields ...zlogger.Field) {
245
+ if existing, exists := c.Get(LogFieldsKey); exists {
246
+ fields = append(existing.([]zlogger.Field), fields...)
247
+ }
248
+ c.Set(LogFieldsKey, fields)
249
+ }
250
+
251
+ // SkipMiddlewareLog 跳過中間件日誌(handler 自行記錄時)
252
+ func SkipMiddlewareLog(c *gin.Context) {
253
+ c.Set(LogSkipKey, true)
254
+ }
255
+
256
+ // Logger 日誌 + Metrics 中間件
257
+ func Logger() gin.HandlerFunc {
258
+ return func(c *gin.Context) {
259
+ start := time.Now()
260
+
261
+ // 生成 Request ID
262
+ requestID := uuid.New().String()
263
+ c.Set("requestID", requestID)
264
+ c.Header("X-Request-ID", requestID)
265
+
266
+ // 注入 Context
267
+ ctx := zlogger.WithRequestID(c.Request.Context(), requestID)
268
+ c.Request = c.Request.WithContext(ctx)
269
+
270
+ c.Next()
271
+
272
+ // 檢查是否跳過日誌
273
+ if skip, exists := c.Get(LogSkipKey); exists && skip.(bool) {
274
+ return
275
+ }
276
+
277
+ latency := time.Since(start)
278
+
279
+ // 基本欄位
280
+ fields := []zlogger.Field{
281
+ zlogger.String("method", c.Request.Method),
282
+ zlogger.String("path", c.Request.URL.Path),
283
+ zlogger.String("query", c.Request.URL.RawQuery),
284
+ zlogger.String("ip", c.ClientIP()),
285
+ zlogger.Int("status", c.Writer.Status()),
286
+ zlogger.Duration("latency", latency),
287
+ zlogger.String("user-agent", c.Request.UserAgent()),
288
+ }
289
+
290
+ // 加入自訂欄位
291
+ if customFields, exists := c.Get(LogFieldsKey); exists {
292
+ fields = append(fields, customFields.([]zlogger.Field)...)
293
+ }
294
+
295
+ // 記錄日誌
296
+ if len(c.Errors) > 0 {
297
+ fields = append(fields, zlogger.String("error", c.Errors.String()))
298
+ zlogger.ErrorContext(ctx, "HTTP Request Error", fields...)
299
+ } else {
300
+ zlogger.InfoContext(ctx, "HTTP Request", fields...)
301
+ }
302
+
303
+ // 記錄 Metrics
304
+ metrics.RecordRequest(
305
+ c.Request.Method,
306
+ strconv.Itoa(c.Writer.Status()),
307
+ c.Request.URL.Path,
308
+ latency.Seconds(),
309
+ )
310
+ }
311
+ }
312
+
313
+ // 使用範例(Handler)
314
+ func GetUserHandler(c *gin.Context) {
315
+ // 方式一:使用 SetLogFields 新增自訂欄位
316
+ middleware.SetLogFields(c,
317
+ zlogger.String("category", "user"),
318
+ zlogger.String("action", "get"),
319
+ )
320
+
321
+ // 方式二:直接使用 Context 記錄日誌
322
+ zlogger.InfoContext(c.Request.Context(), "獲取使用者",
323
+ zlogger.Uint("user_id", 12345),
324
+ )
325
+
326
+ // 若 Handler 已記錄,可跳過中間件日誌
327
+ middleware.SkipMiddlewareLog(c)
328
+
329
+ c.JSON(200, gin.H{"status": "ok"})
330
+ }
331
+ ```
332
+
333
+ ### HTTP Middleware(標準庫)er_id="user-12345", trace_id="abc123..."} // 會產生大量 time series
334
+ ```
335
+
336
+ ### 實作範例
337
+
338
+ ```go
339
+ package metrics
340
+
341
+ import (
342
+ "github.com/prometheus/client_golang/prometheus"
343
+ "github.com/prometheus/client_golang/prometheus/promauto"
344
+ )
345
+
346
+ var (
347
+ // Counter: 僅能 Inc
348
+ HttpRequestsTotal = promauto.NewCounterVec(
349
+ prometheus.CounterOpts{
350
+ Name: "http_requests_total",
351
+ Help: "Total number of HTTP requests",
352
+ },
353
+ []string{"method", "status", "path"}, // Labels
354
+ )
355
+
356
+ // Histogram: 觀測延遲分佈
357
+ HttpRequestDuration = promauto.NewHistogramVec(
358
+ prometheus.HistogramOpts{
359
+ Name: "http_request_duration_seconds",
360
+ Help: "HTTP request duration in seconds",
361
+ Buckets: prometheus.DefBuckets, // 或自訂:[]float64{0.01, 0.05, 0.1, 0.5, 1, 2.5, 5, 10}
362
+ },
363
+ []string{"method", "path"},
364
+ )
365
+
366
+ // Gauge: 當前 Goroutine 數量
367
+ CurrentGoroutines = promauto.NewGauge(
368
+ prometheus.GaugeOpts{
369
+ Name: "goroutines_current",
370
+ Help: "Current number of goroutines",
371
+ },
372
+ )
373
+ )
374
+
375
+ // 使用範例
376
+ func RecordRequest(method, status, path string, duration float64) {
377
+ // Counter: 增加計數
378
+ HttpRequestsTotal.WithLabelValues(method, status, path).Inc()
379
+
380
+ // Histogram: 記錄延遲
381
+ HttpRequestDuration.WithLabelValues(method, path).Observe(duration)
382
+ }
383
+
384
+ // Gauge 使用範例
385
+ func UpdateGoroutineCount() {
386
+ count := runtime.NumGoroutine()
387
+ CurrentGoroutines.Set(float64(count))
388
+ }
389
+ ```
390
+
391
+ ### HTTP Middleware 整合
392
+
393
+ ```go
394
+ func MetricsMiddleware(next http.Handler) http.Handler {
395
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
396
+ start := time.Now()
397
+
398
+ // 包裝 ResponseWriter 以捕獲狀態碼
399
+ rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
400
+
401
+ next.ServeHTTP(rw, r)
402
+
403
+ // 記錄 Metrics
404
+ duration := time.Since(start).Seconds()
405
+ metrics.RecordRequest(
406
+ r.Method,
407
+ strconv.Itoa(rw.statusCode),
408
+ r.URL.Path,
409
+ duration,
410
+ )
411
+ })
412
+ }
413
+
414
+ type responseWriter struct {
415
+ http.ResponseWriter
416
+ statusCode int
417
+ }
418
+
419
+ func (rw *responseWriter) WriteHeader(code int) {
420
+ rw.statusCode = code
421
+ rw.ResponseWriter.WriteHeader(code)
422
+ }
423
+ ```
424
+
425
+ ### 暴露 Metrics Endpoint
426
+
427
+ ```go
428
+ import (
429
+ "github.com/prometheus/client_golang/prometheus/promhttp"
430
+ )
431
+
432
+ func main() {
433
+ // 業務 API
434
+ http.Handle("/api/", apiHandler)
435
+
436
+ // Metrics Endpoint(通常使用不同 Port)
437
+ metricsServer := &http.Server{
438
+ Addr: ":9090",
439
+ Handler: promhttp.Handler(), // 暴露 /metrics
440
+ }
441
+
442
+ go metricsServer.ListenAndServe()
443
+
444
+ // ...啟動主 Server
445
+ }
446
+ ```
447
+
448
+ ---
449
+
450
+ ## OpenTelemetry 整合
451
+
452
+ ### 安裝套件
453
+
454
+ ```bash
455
+ go get go.opentelemetry.io/otel
456
+ go get go.opentelemetry.io/otel/exporters/jaeger
457
+ go get go.opentelemetry.io/otel/sdk/trace
458
+ go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
459
+ go get go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
460
+ ```
461
+
462
+ ### 初始化 Tracer
463
+
464
+ ```go
465
+ package tracing
466
+
467
+ import (
468
+ "go.opentelemetry.io/otel"
469
+ "go.opentelemetry.io/otel/exporters/jaeger"
470
+ "go.opentelemetry.io/otel/sdk/resource"
471
+ sdktrace "go.opentelemetry.io/otel/sdk/trace"
472
+ semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
473
+ )
474
+
475
+ func InitTracer(serviceName, jaegerEndpoint string) (func(), error) {
476
+ // Jaeger Exporter
477
+ exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(jaegerEndpoint)))
478
+ if err != nil {
479
+ return nil, fmt.Errorf("failed to create jaeger exporter: %w", err)
480
+ }
481
+
482
+ // Trace Provider
483
+ tp := sdktrace.NewTracerProvider(
484
+ sdktrace.WithBatcher(exporter),
485
+ sdktrace.WithResource(resource.NewWithAttributes(
486
+ zlogger 自動整合 Trace ID
487
+ func HandleRequest(ctx context.Context) {
488
+ // 從 OpenTelemetry 取得 Trace ID
489
+ traceID := GetTraceID(ctx)
490
+ ctx = zlogger.WithTraceID(ctx, traceID)
491
+
492
+ // 日誌自動包含 trace_id
493
+ zlogger.InfoContext(ctx, "處理請求")
494
+ }
495
+ ```
496
+
497
+ ---
498
+
499
+ ## 按級別分離日誌檔案(zlogger)
500
+
501
+ ```go
502
+ import (
503
+ "go.uber.org/zap/zapcore"
504
+ "github.com/vincent119/zlogger"
505
+ )
506
+
507
+ func main() {
508
+ // 建立分離輸出的核心
509
+ core, cleanup, err := zlogger.GetSplitCore("./logs", "app", zapcore.EncoderConfig{
510
+ TimeKey: "ts",
511
+ LevelKey: "level",
512
+ MessageKey: "msg",
513
+ EncodeTime: zapcore.ISO8601TimeEncoder,
514
+ EncodeLevel: zapcore.CapitalLevelEncoder,
515
+ })
516
+ if err != nil {
517
+ panic(err)
518
+ }
519
+ defer cleanup()
520
+
521
+ // 會產生以下檔案:
522
+ // - logs/app-info-2024-01-01.log
523
+ // - logs/app-warn-2024-01-01.log
524
+ // - logs/app-error-2024-01-01.log
525
+ }
526
+ ```
527
+
528
+ ---
529
+
530
+ ## 動態調整日誌等級
531
+
532
+ ```go
533
+ // 運行時調整等級(例如透過 API)
534
+ func SetLogLevel(level string) {
535
+ zlogger.SetLevel(level) // "debug", "info", "warn", "error"
536
+ }
537
+
538
+ // 範例:HTTP Endpoint
539
+ func handleSetLogLevel(c *gin.Context) {
540
+ level := c.Query("level")
541
+ zlogger.SetLevel(level)
542
+ c.JSON(200, gin.H{"level": level})
543
+ }
544
+ ```
545
+
546
+ ---
547
+
548
+ ## 檢查清單
549
+
550
+ **結構化日誌**
551
+ - [ ] 使用 zlogger(或 zap/slog)
552
+ - [ ] 使用 Context 方法(InfoContext, ErrorContext)
553
+ - [ ] 固定欄位包含 `request_id`, `trace_id`, `user_id`
554
+ - [ ] 使用 `zlogger.Err(err)` 而非字串拼接
555
+ - [ ] 不在 library 使用 `
556
+ ```go
557
+ import (
558
+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
559
+ )
560
+
561
+ func main() {
562
+ // 初始化 Tracer
563
+ shutdown, err := tracing.InitTracer("myservice", "http://jaeger:14268/api/traces")
564
+ if err != nil {
565
+ log.Fatal(err)
566
+ }
567
+ defer shutdown()
568
+
569
+ // 包裝 Handler
570
+ handler := otelhttp.NewHandler(http.HandlerFunc(myHandler), "myservice")
571
+
572
+ http.ListenAndServe(":8080", handler)
573
+ }
574
+ ```
575
+
576
+ ### gRPC Server 整合
577
+
578
+ ```go
579
+ import (
580
+ "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
581
+ )
582
+
583
+ func main() {
584
+ grpcServer := grpc.NewServer(
585
+ grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
586
+ grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
587
+ )
588
+
589
+ // ...註冊服務
590
+ }
591
+ ```
592
+
593
+ ### Context 傳遞與 Trace ID
594
+
595
+ **核心原則**:
596
+ - 所有跨函式呼叫(特別是跨邊界的 DB/HTTP 呼叫)必須傳遞 `ctx`,以確保 Trace ID 能正確串接
597
+
598
+ ```go
599
+ import (
600
+ "go.opentelemetry.io/otel"
601
+ "go.opentelemetry.io/otel/trace"
602
+ )
603
+
604
+ func ProcessOrder(ctx context.Context, orderID string) error {
605
+ tracer := otel.Tracer("myservice")
606
+
607
+ // 建立 Span
608
+ ctx, span := tracer.Start(ctx, "ProcessOrder")
609
+ defer span.End()
610
+
611
+ // 傳遞 ctx 至子函式(Trace 自動串接)
612
+ if err := validateOrder(ctx, orderID); err != nil {
613
+ span.RecordError(err) // 記錄錯誤到 Span
614
+ return err
615
+ }
616
+
617
+ if err := saveOrder(ctx, orderID); err != nil {
618
+ span.RecordError(err)
619
+ return err
620
+ }
621
+
622
+ return nil
623
+ }
624
+
625
+ func validateOrder(ctx context.Context, orderID string) error {
626
+ // 自動繼承 Parent Span
627
+ tracer := otel.Tracer("myservice")
628
+ ctx, span := tracer.Start(ctx, "validateOrder")
629
+ defer span.End()
630
+
631
+ // ...驗證邏輯
632
+ return nil
633
+ }
634
+ ```
635
+
636
+ ### 取得 Trace ID
637
+
638
+ ```go
639
+ func GetTraceID(ctx context.Context) string {
640
+ spanCtx := trace.SpanFromContext(ctx).SpanContext()
641
+ if spanCtx.HasTraceID() {
642
+ return spanCtx.TraceID().String()
643
+ }
644
+ return ""
645
+ }
646
+
647
+ // 注入到 Logger
648
+ func LogWithTrace(ctx context.Context, logger *zap.Logger, msg string) {
649
+ traceID := GetTraceID(ctx)
650
+ logger.Info(msg, zap.String("trace_id", traceID))
651
+ }
652
+ ```
653
+
654
+ ---
655
+
656
+ ## 檢查清單
657
+
658
+ **結構化日誌**
659
+ - [ ] 使用 Zap 或 Slog
660
+ - [ ] 固定欄位包含 `trace_id`, `span_id`, `req_id`
661
+ - [ ] 使用 `zap.Error(err)` 而非字串拼接
662
+ - [ ] 不在 library 使用 `logger.Fatal()`
663
+ - [ ] 生產環境使用 JSON 格式
664
+
665
+ **Prometheus Metrics**
666
+ - [ ] Counter 僅使用 `Inc()`,不減少
667
+ - [ ] Gauge 用於可增減的指標
668
+ - [ ] Histogram 用於延遲/大小分佈
669
+ - [ ] 命名使用蛇形命名法與單位後綴(`_seconds`, `_bytes`, `_total`)
670
+ - [ ] Label 避免高基數值(無 `user_id`, `trace_id`)
671
+ - [ ] Metrics Endpoint 暴露於獨立 Port(如 `:9090`)
672
+
673
+ **OpenTelemetry**
674
+ - [ ] 初始化 Tracer 並設定 Service Name
675
+ - [ ] HTTP/gRPC 整合 OTel Instrumentation
676
+ - [ ] 所有跨邊界呼叫傳遞 `ctx`
677
+ - [ ] 使用 `span.RecordError()` 記錄錯誤
678
+ - [ ] Logger 包含 Trace ID
679
+
680
+ **Context 傳遞**
681
+ - [ ] 所有對外 API 第一個參數為 `ctx context.Context`
682
+ - [ ] DB 查詢使用 `db.WithContext(ctx)`
683
+ - [ ] HTTP Client 使用 `req.WithContext(ctx)`
684
+ - [ ] gRPC Client 傳遞 `ctx`