Go - 實現項目內鏈路追蹤

爲何項目內須要鏈路追蹤?當一個請求中,請求了多個服務單元,若是請求出現了錯誤或異常,很難去定位是哪一個服務出了問題,這時就須要鏈路追蹤。html

從圖中能夠清晰的看出他們之間的調用關係,經過一個例子說明下鏈路的重要性,好比對方調咱們一個接口,反饋在某個時間段這接口太慢了,在排查代碼發現邏輯比較複雜,不光調用了多個三方接口、操做了數據庫,還操做了緩存,怎麼快速定位是哪塊執行時間很長?git

不賣關子,先說下本篇文章最終實現了什麼,若是感興趣再繼續往下看。github

實現了經過記錄以下參數,來進行問題定位,關於每一個參數的結構在下面都有介紹。redis

// Trace 記錄的參數
type Trace struct {
    mux                sync.Mutex
    Identifier         string    `json:"trace_id"`             // 鏈路 ID
    Request            *Request  `json:"request"`              // 請求信息
    Response           *Response `json:"response"`             // 響應信息
    ThirdPartyRequests []*Dialog `json:"third_party_requests"` // 調用第三方接口的信息
    Debugs             []*Debug  `json:"debugs"`               // 調試信息
    SQLs               []*SQL    `json:"sqls"`                 // 執行的 SQL 信息
    Redis              []*Redis  `json:"redis"`                // 執行的 Redis 信息
    Success            bool      `json:"success"`              // 請求結果 true or false
    CostSeconds        float64   `json:"cost_seconds"`         // 執行時長(單位秒)
}

參數結構

鏈路 ID

String 例如:4b4f81f015a4f2a01b00。若是請求 Header 中存在 TRACE-ID,就使用它,反之,從新建立一個。將 TRACE_ID 放到接口返回值中,這樣就能夠經過這個標示查到這一串的信息。sql

請求信息

Object,結構以下:數據庫

type Request struct {
	TTL        string      `json:"ttl"`         // 請求超時時間
	Method     string      `json:"method"`      // 請求方式
	DecodedURL string      `json:"decoded_url"` // 請求地址
	Header     interface{} `json:"header"`      // 請求 Header 信息
	Body       interface{} `json:"body"`        // 請求 Body 信息
}

響應信息

Object,結構以下:json

type Response struct {
	Header          interface{} `json:"header"`                      // Header 信息
	Body            interface{} `json:"body"`                        // Body 信息
	BusinessCode    int         `json:"business_code,omitempty"`     // 業務碼
	BusinessCodeMsg string      `json:"business_code_msg,omitempty"` // 提示信息
	HttpCode        int         `json:"http_code"`                   // HTTP 狀態碼
	HttpCodeMsg     string      `json:"http_code_msg"`               // HTTP 狀態碼信息
	CostSeconds     float64     `json:"cost_seconds"`                // 執行時間(單位秒)
}

調用三方接口信息

Object,結構以下:api

type Dialog struct {
	mux         sync.Mutex
	Request     *Request    `json:"request"`      // 請求信息
	Responses   []*Response `json:"responses"`    // 返回信息
	Success     bool        `json:"success"`      // 是否成功,true 或 false
	CostSeconds float64     `json:"cost_seconds"` // 執行時長(單位秒)
}

這裏面的 RequestResponse 結構與上面保持一致。緩存

細節來了,爲何 Responses 結構是 []*Response微信

是由於 HTTP 能夠進行重試請求,好比當請求對方接口的時候,HTTP 狀態碼爲 503 http.StatusServiceUnavailable,這時須要重試,咱們也須要把重試的響應信息記錄下來。

調試信息

Object 結構以下:

type Debug struct {
	Key         string      `json:"key"`          // 標示
	Value       interface{} `json:"value"`        // 值
	CostSeconds float64     `json:"cost_seconds"` // 執行時間(單位秒)
}

SQL 信息

Object,結構以下:

type SQL struct {
	Timestamp   string  `json:"timestamp"`     // 時間,格式:2006-01-02 15:04:05
	Stack       string  `json:"stack"`         // 文件地址和行號
	SQL         string  `json:"sql"`           // SQL 語句
	Rows        int64   `json:"rows_affected"` // 影響行數
	CostSeconds float64 `json:"cost_seconds"`  // 執行時長(單位秒)
}

Redis 信息

Object,結構以下:

type Redis struct {
	Timestamp   string  `json:"timestamp"`       // 時間,格式:2006-01-02 15:04:05
	Handle      string  `json:"handle"`          // 操做,SET/GET 等
	Key         string  `json:"key"`             // Key
	Value       string  `json:"value,omitempty"` // Value
	TTL         float64 `json:"ttl,omitempty"`   // 超時時長(單位分)
	CostSeconds float64 `json:"cost_seconds"`    // 執行時間(單位秒)
}

請求結果

Bool,這個和統必定義返回值有點關係,看下代碼:

// 錯誤返回
c.AbortWithError(code.ErrParamBind.WithErr(err))

// 正確返回
c.Payload(code.OK.WithData(data))

當錯誤返回時 且 ctx.Writer.Status() != http.StatusOK 時,爲 false,反之爲 true

執行時長

Float64,例如:0.041746869,記錄的是從請求開始到請求結束所花費的時間。

如何收集參數?

這時有老鐵會說了:「規劃的稍微還行,使用的時候會不會很麻煩?」

「No,No,使用起來一丟丟都不麻煩」,接着往下看。

無需關心的參數

鏈路 ID、請求信息、響應信息、請求結果、執行時長,這 5 個參數,開發者無需關心,這些都在中間件封裝好了。

調用第三方接口的信息

只需多傳遞一個參數便可。

在這裏厚臉皮自薦下 httpclient 包

  • 支持設置失敗時重試,能夠自定義重試次數、重試前延遲等待時間、重試的知足條件;
  • 支持設置失敗時告警,能夠自定義告警渠道(郵件/微信)、告警的知足條件;
  • 支持設置調用鏈路;

調用示例代碼:

// httpclient 是項目中封裝的包
api := "http://127.0.0.1:9999/demo/post"
params := url.Values{}
params.Set("name", name)
body, err := httpclient.PostForm(api, params,
    httpclient.WithTrace(ctx.Trace()),  // 傳遞上下文
)

調試信息

只需多傳遞一個參數便可。

調用示例代碼:

// p 是項目中封裝的包
p.Println("key", "value",
	p.WithTrace(ctx.Trace()), // 傳遞上下文
)

SQL 信息

稍微複雜一丟丟,須要多傳遞一個參數,而後再寫一個 GORM 插件。

使用的 GORM V2 自帶的 CallbacksContext 知識點,細節很少說,能夠看下這篇文章:基於 GORM 獲取當前請求所執行的 SQL 信息

調用示例代碼:

// 原來查詢這樣寫
err := u.db.GetDbR().
    First(data, id).
    Where("is_deleted = ?", -1).
    Error

// 如今只需這樣寫
err := u.db.GetDbR().
    WithContext(ctx.RequestContext()).
    First(data, id).
    Where("is_deleted = ?", -1).
    Error
    
// .WithContext 是 GORM V2 自帶的。    
// 插件的代碼就不貼了,去上面的文章查看便可。

Redis 信息

只需多傳遞一個參數便可。

調用示例代碼:

// cache 是基於 go-redis 封裝的包
d.cache.Get("name", 
    cache.WithTrace(c.Trace()),
)

核心原理是啥?

在這沒關子可賣,看到這相信老鐵們都知道了,就兩個:一個是 攔截器,另外一個是 Context

如何記錄參數?

將以上數據轉爲 JSON 結構記錄到日誌中。

JSON 示例

{
    "level":"info",
    "time":"2021-01-30 22:32:48",
    "caller":"core/core.go:444",
    "msg":"core-interceptor",
    "domain":"go-gin-api[fat]",
    "method":"GET",
    "path":"/demo/trace",
    "http_code":200,
    "business_code":1,
    "success":true,
    "cost_seconds":0.054025302,
    "trace_id":"2cdb2f96934f573af391",
    "trace_info":{
        "trace_id":"2cdb2f96934f573af391",
        "request":{
            "ttl":"un-limit",
            "method":"GET",
            "decoded_url":"/demo/trace",
            "header":{
                "Accept":[
                    "application/json"
                ],
                "Accept-Encoding":[
                    "gzip, deflate, br"
                ],
                "Accept-Language":[
                    "zh-CN,zh;q=0.9,en;q=0.8"
                ],
                "Authorization":[
                    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEsIlVzZXJOYW1lIjoieGlubGlhbmdub3RlIiwiZXhwIjoxNjEyMTAzNTQwLCJpYXQiOjE2MTIwMTcxNDAsIm5iZiI6MTYxMjAxNzE0MH0.2yHDdP7cNT5uL5xA0-j_NgTK4GrW-HGn0KUxcbZfpKg"
                ],
                "Connection":[
                    "keep-alive"
                ],
                "Referer":[
                    "http://127.0.0.1:9999/swagger/index.html"
                ],
                "Sec-Fetch-Dest":[
                    "empty"
                ],
                "Sec-Fetch-Mode":[
                    "cors"
                ],
                "Sec-Fetch-Site":[
                    "same-origin"
                ],
                "User-Agent":[
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36"
                ]
            },
            "body":""
        },
        "response":{
            "header":{
                "Content-Type":[
                    "application/json; charset=utf-8"
                ],
                "Trace-Id":[
                    "2cdb2f96934f573af391"
                ],
                "Vary":[
                    "Origin"
                ]
            },
            "body":{
                "code":1,
                "msg":"OK",
                "data":[
                    {
                        "name":"Tom",
                        "job":"Student"
                    },
                    {
                        "name":"Jack",
                        "job":"Teacher"
                    }
                ],
                "id":"2cdb2f96934f573af391"
            },
            "business_code":1,
            "business_code_msg":"OK",
            "http_code":200,
            "http_code_msg":"OK",
            "cost_seconds":0.054024874
        },
        "third_party_requests":[
            {
                "request":{
                    "ttl":"5s",
                    "method":"GET",
                    "decoded_url":"http://127.0.0.1:9999/demo/get/Tom",
                    "header":{
                        "Authorization":[
                            "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEsIlVzZXJOYW1lIjoieGlubGlhbmdub3RlIiwiZXhwIjoxNjEyMTAzNTQwLCJpYXQiOjE2MTIwMTcxNDAsIm5iZiI6MTYxMjAxNzE0MH0.2yHDdP7cNT5uL5xA0-j_NgTK4GrW-HGn0KUxcbZfpKg"
                        ],
                        "Content-Type":[
                            "application/x-www-form-urlencoded; charset=utf-8"
                        ],
                        "TRACE-ID":[
                            "2cdb2f96934f573af391"
                        ]
                    },
                    "body":null
                },
                "responses":[
                    {
                        "header":{
                            "Content-Length":[
                                "87"
                            ],
                            "Content-Type":[
                                "application/json; charset=utf-8"
                            ],
                            "Date":[
                                "Sat, 30 Jan 2021 14:32:48 GMT"
                            ],
                            "Trace-Id":[
                                "2cdb2f96934f573af391"
                            ],
                            "Vary":[
                                "Origin"
                            ]
                        },
                        "body":"{"code":1,"msg":"OK","data":{"name":"Tom","job":"Student"},"id":"2cdb2f96934f573af391"}",
                        "http_code":200,
                        "http_code_msg":"200 OK",
                        "cost_seconds":0.000555089
                    }
                ],
                "success":true,
                "cost_seconds":0.000580202
            },
            {
                "request":{
                    "ttl":"5s",
                    "method":"POST",
                    "decoded_url":"http://127.0.0.1:9999/demo/post",
                    "header":{
                        "Authorization":[
                            "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEsIlVzZXJOYW1lIjoieGlubGlhbmdub3RlIiwiZXhwIjoxNjEyMTAzNTQwLCJpYXQiOjE2MTIwMTcxNDAsIm5iZiI6MTYxMjAxNzE0MH0.2yHDdP7cNT5uL5xA0-j_NgTK4GrW-HGn0KUxcbZfpKg"
                        ],
                        "Content-Type":[
                            "application/x-www-form-urlencoded; charset=utf-8"
                        ],
                        "TRACE-ID":[
                            "2cdb2f96934f573af391"
                        ]
                    },
                    "body":"name=Jack"
                },
                "responses":[
                    {
                        "header":{
                            "Content-Length":[
                                "88"
                            ],
                            "Content-Type":[
                                "application/json; charset=utf-8"
                            ],
                            "Date":[
                                "Sat, 30 Jan 2021 14:32:48 GMT"
                            ],
                            "Trace-Id":[
                                "2cdb2f96934f573af391"
                            ],
                            "Vary":[
                                "Origin"
                            ]
                        },
                        "body":"{"code":1,"msg":"OK","data":{"name":"Jack","job":"Teacher"},"id":"2cdb2f96934f573af391"}",
                        "http_code":200,
                        "http_code_msg":"200 OK",
                        "cost_seconds":0.000450153
                    }
                ],
                "success":true,
                "cost_seconds":0.000468387
            }
        ],
        "debugs":[
            {
                "key":"res1.Data.Name",
                "value":"Tom",
                "cost_seconds":0.000005193
            },
            {
                "key":"res2.Data.Name",
                "value":"Jack",
                "cost_seconds":0.000003907
            },
            {
                "key":"redis-name",
                "value":"tom",
                "cost_seconds":0.000009816
            }
        ],
        "sqls":[
            {
                "timestamp":"2021-01-30 22:32:48",
                "stack":"/Users/xinliang/github/go-gin-api/internal/api/repository/db_repo/user_demo_repo/user_demo.go:76",
                "sql":"SELECT `id`,`user_name`,`nick_name`,`mobile` FROM `user_demo` WHERE user_name = 'test_user' and is_deleted = -1 ORDER BY `user_demo`.`id` LIMIT 1",
                "rows_affected":1,
                "cost_seconds":0.031969072
            }
        ],
        "redis":[
            {
                "timestamp":"2021-01-30 22:32:48",
                "handle":"set",
                "key":"name",
                "value":"tom",
                "ttl":10,
                "cost_seconds":0.009982091
            },
            {
                "timestamp":"2021-01-30 22:32:48",
                "handle":"get",
                "key":"name",
                "cost_seconds":0.010681579
            }
        ],
        "success":true,
        "cost_seconds":0.054025302
    }
}

zap 日誌組件

有對日誌收集感興趣的老鐵們能夠往下看,trace_info 只是日誌的一個參數,具體日誌參數包括:

參數 數據類型 說明
level String 日誌級別,例如:info,warn,error,debug
time String 時間,例如:2021-01-30 16:05:44
caller String 調用位置,文件+行號,例如:core/core.go:443
msg String 日誌信息,例如:xx 錯誤
domain String 域名或服務名,例如:go-gin-api[fat]
method String 請求方式,例如:POST
path String 請求路徑,例如:/user/create
http_code Int HTTP 狀態碼,例如:200
business_code Int 業務狀態碼,例如:10101
success Bool 狀態,true or false
cost_seconds Float64 花費時間,單位:秒,例如:0.01
trace_id String 鏈路ID,例如:ec3c868c8dcccfe515ab
trace_info Object 鏈路信息,結構化數據。
error String 錯誤信息,當出現錯誤時纔有這字段。
errorVerbose String 詳細的錯誤堆棧信息,當出現錯誤時纔有這字段。

日誌記錄能夠使用 zaplogrus ,此次我使用的 zap,簡單封裝一下便可,好比:

  • 支持設置日誌級別;
  • 支持設置日誌輸出到控制檯;
  • 支持設置日誌輸出到文件;
  • 支持設置日誌輸出到文件(可自動分割);

總結

這個功能比較經常使用,使用起來也很爽,好比調用方發現接口出問題時,只須要提供 TRACE-ID 便可,咱們就能夠查到關於它整個鏈路的全部信息。

以上代碼的實現都在 go-gin-api 項目中,地址:https://github.com/xinliangnote/go-gin-api

相關文章
相關標籤/搜索