Gin 框架:實現分散式日誌追蹤

語言: CN / TW / HK

theme: github highlight: a11y-dark


這是我參與11月更文挑戰的第11天,活動詳情檢視:2021最後一次更文挑戰

介紹

通過一個完整例子,在基於 Gin 框架中實現分散式日誌追蹤。

什麼是 API 日誌追蹤?

一個 API 請求會跨多個微服務,我們希望通過一個唯一的 ID 檢索到整個鏈路的日誌。

我們將會使用 rk-boot 來啟動 Gin 框架的微服務。

請訪問如下地址獲取完整教程:

  • http://rkdocs.netlify.app/cn

安裝

go go get github.com/rookie-ninja/rk-boot go get github.com/rookie-ninja/rk-gin

快速開始

我們會建立 /v1/greeter API 進行驗證,同時開啟 logging, meta 和 tracing 中介軟體以達到目的。

1. 建立 bootA.yaml & serverA.go

ServerA 監聽 1949 埠,並且傳送請求給 ServerB。

我們通過 rkginctx.InjectSpanToNewContext() 方法把 Tracing 資訊注入到 Context 中,傳送給 ServerB。

```yaml

gin: - name: greeter # Required port: 1949 # Required enabled: true # Required interceptors: loggingZap: enabled: true # Optional, enable logging interceptor meta: enabled: true # Optional, enable meta interceptor tracingTelemetry: enabled: true # Optional, enable tracing interceptor ```

```go // Copyright (c) 2021 rookie-ninja // // Use of this source code is governed by an Apache-style // license that can be found in the LICENSE file. package main

import ( "context" "github.com/gin-gonic/gin" "github.com/rookie-ninja/rk-boot" "github.com/rookie-ninja/rk-gin/boot" "github.com/rookie-ninja/rk-gin/interceptor/context" "net/http" )

// Application entrance. func main() { // Create a new boot instance. boot := rkboot.NewBoot(rkboot.WithBootConfigPath("bootA.yaml"))

// Register handler
boot.GetEntry("greeter").(*rkgin.GinEntry).Router.GET("/v1/greeter", GreeterA)

// Bootstrap
boot.Bootstrap(context.Background())

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())

}

// GreeterA will add trace info into context and call serverB func GreeterA(ctx *gin.Context) { // Call serverB at 2008 req, _ := http.NewRequest(http.MethodGet, "http://localhost:2008/v1/greeter", nil)

// Inject current trace information into context
rkginctx.InjectSpanToHttpRequest(ctx, req)

// Call server
http.DefaultClient.Do(req)

// Respond to request
ctx.String(http.StatusOK, "Hello from serverA!")

} ```

2. 建立 bootB.yaml & serverB.go

ServerB 監聽 2008 埠。

```yaml

gin: - name: greeter # Required port: 2008 # Required enabled: true # Required interceptors: loggingZap: enabled: true # Optional, enable logging interceptor meta: enabled: true # Optional, enable meta interceptor tracingTelemetry: enabled: true # Optional, enable tracing interceptor ```

```go // Copyright (c) 2021 rookie-ninja // // Use of this source code is governed by an Apache-style // license that can be found in the LICENSE file. package main

import ( "context" "github.com/gin-gonic/gin" "github.com/rookie-ninja/rk-boot" "github.com/rookie-ninja/rk-gin/boot" "net/http" )

// Application entrance. func main() { // Create a new boot instance. boot := rkboot.NewBoot(rkboot.WithBootConfigPath("bootB.yaml"))

// Register handler
boot.GetEntry("greeter").(*rkgin.GinEntry).Router.GET("/v1/greeter", GreeterB)

// Bootstrap
boot.Bootstrap(context.Background())

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())

}

// GreeterB receive request from serverA and respond. func GreeterB(ctx *gin.Context) { ctx.String(http.StatusOK, "Hello from serverB!") } ```

3. 資料夾結構

``` . ├── bootA.yaml ├── bootB.yaml ├── go.mod ├── go.sum ├── serverA.go └── serverB.go

0 directories, 6 files ```

4. 啟動 ServerA & ServerB

$ go run serverA.go $ go run serverB.go

5. 往 ServerA 傳送請求

$ curl localhost:1949/v1/greeter Hello from serverA!

6. 驗證日誌

兩個服務的日誌中,會有同樣的 traceId,不同的 requestId。

我們可以通過 grep traceId 來追蹤 RPC。

  • ServerA

```

endTime=2021-11-18T01:29:56.698997+08:00 ... ids={"eventId":"f5878390-1a5a-4bb9-8b39-bf4261864c0f","requestId":"f5878390-1a5a-4bb9-8b39-bf4261864c0f","traceId":"b2d70ab9f8207ef4a9f0c3fb1be5c22c"} ... operation=/v1/greeter resCode=200 eventStatus=Ended EOE ```

  • ServerB

```

endTime=2021-11-18T01:29:56.698606+08:00 ... ids={"eventId":"273c97d2-e11a-46f5-a044-bb9c0cf64540","requestId":"273c97d2-e11a-46f5-a044-bb9c0cf64540","traceId":"b2d70ab9f8207ef4a9f0c3fb1be5c22c"} ... operation=/v1/greeter resCode=200 eventStatus=Ended EOE ```

概念

當我們沒有使用例如 jaeger 呼叫鏈服務的時候,我們希望通過日誌來追蹤分散式系統裡的 RPC 請求。

rk-boot 的中介軟體會通過 openTelemetry 庫來向日志寫入 traceId 來追蹤 RPC。

當啟動了日誌中介軟體,原資料中介軟體,呼叫鏈中介軟體的時候,中介軟體會往日誌裡寫入如下三種 ID。

EventId

當啟動了日誌中介軟體,EventId 會自動生成。

```yaml

gin: - name: greeter # Required port: 1949 # Required enabled: true # Required interceptors: loggingZap: enabled: true ```

```

... ids={"eventId":"cd617f0c-2d93-45e1-bef0-95c89972530d"} ... ```

RequestId

當啟動了日誌中介軟體和原資料中介軟體,RequestId 和 EventId 會自動生成,並且這兩個 ID 會一致。

```yaml

gin: - name: greeter # Required port: 1949 # Required enabled: true # Required interceptors: loggingZap: enabled: true meta: enabled: true ```

```

... ids={"eventId":"8226ba9b-424e-4e19-ba63-d37ca69028b3","requestId":"8226ba9b-424e-4e19-ba63-d37ca69028b3"} ... ```

即使使用者覆蓋了 RequestId,EventId 也會保持一致。

rkginctx.AddHeaderToClient(ctx, rkginctx.RequestIdKey, "overridden-request-id")

```

... ids={"eventId":"overridden-request-id","requestId":"overridden-request-id"} ... ```

TraceId

當啟動了呼叫鏈中介軟體,traceId 會自動生成。

```yaml

gin: - name: greeter # Required port: 1949 # Required enabled: true # Required interceptors: loggingZap: enabled: true meta: enabled: true tracingTelemetry: enabled: true ```

```

... ids={"eventId":"dd19cf9a-c7be-486c-b29d-7af777a78ebe","requestId":"dd19cf9a-c7be-486c-b29d-7af777a78ebe","traceId":"316a7b475ff500a76bfcd6147036951c"} ... ```