@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.
- package/.agent_agy/INSTALLATION.md +786 -0
- package/.agent_agy/README.md +243 -0
- package/.agent_agy/SKILLS_INDEX.md +516 -0
- package/.agent_agy/rules/go-core.copilot-instructions.md +251 -0
- package/.agent_agy/skills/go-api-design/SKILL.md +535 -0
- package/.agent_agy/skills/go-ci-tooling/SKILL.md +533 -0
- package/.agent_agy/skills/go-configuration/SKILL.md +609 -0
- package/.agent_agy/skills/go-database/SKILL.md +412 -0
- package/.agent_agy/skills/go-ddd/SKILL.md +374 -0
- package/.agent_agy/skills/go-dependency-injection/SKILL.md +546 -0
- package/.agent_agy/skills/go-domain-events/SKILL.md +525 -0
- package/.agent_agy/skills/go-examples/SKILL.md +690 -0
- package/.agent_agy/skills/go-graceful-shutdown/SKILL.md +708 -0
- package/.agent_agy/skills/go-grpc/SKILL.md +484 -0
- package/.agent_agy/skills/go-http-advanced/SKILL.md +494 -0
- package/.agent_agy/skills/go-observability/SKILL.md +684 -0
- package/.agent_agy/skills/go-testing-advanced/SKILL.md +573 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/cli/install.js +344 -0
- package/package.json +47 -0
|
@@ -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`
|