爲何項目內須要鏈路追蹤?當一個請求中,請求了多個服務單元,若是請求出現了錯誤或異常,很難去定位是哪一個服務出了問題,這時就須要鏈路追蹤。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"` // 執行時長(單位秒) }
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"` // 執行時長(單位秒) }
這裏面的 Request
和 Response
結構與上面保持一致。緩存
細節來了,爲何 Responses
結構是 []*Response
?微信
是由於 HTTP 能夠進行重試請求,好比當請求對方接口的時候,HTTP 狀態碼爲 503 http.StatusServiceUnavailable
,這時須要重試,咱們也須要把重試的響應信息記錄下來。
Object
結構以下:
type Debug struct { Key string `json:"key"` // 標示 Value interface{} `json:"value"` // 值 CostSeconds float64 `json:"cost_seconds"` // 執行時間(單位秒) }
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"` // 執行時長(單位秒) }
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()), // 傳遞上下文 )
稍微複雜一丟丟,須要多傳遞一個參數,而後再寫一個 GORM
插件。
使用的 GORM V2
自帶的 Callbacks
和 Context
知識點,細節很少說,能夠看下這篇文章:基於 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 自帶的。 // 插件的代碼就不貼了,去上面的文章查看便可。
只需多傳遞一個參數便可。
調用示例代碼:
// cache 是基於 go-redis 封裝的包 d.cache.Get("name", cache.WithTrace(c.Trace()), )
在這沒關子可賣,看到這相信老鐵們都知道了,就兩個:一個是 攔截器,另外一個是 Context
。
將以上數據轉爲 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 } }
有對日誌收集感興趣的老鐵們能夠往下看,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 | 詳細的錯誤堆棧信息,當出現錯誤時纔有這字段。 |
日誌記錄能夠使用 zap
,logrus
,此次我使用的 zap
,簡單封裝一下便可,好比:
這個功能比較經常使用,使用起來也很爽,好比調用方發現接口出問題時,只須要提供 TRACE-ID
便可,咱們就能夠查到關於它整個鏈路的全部信息。
以上代碼的實現都在 go-gin-api 項目中,地址:https://github.com/xinliangnote/go-gin-api