Go - 實(shí)現(xiàn)項(xiàng)目內(nèi)鏈路追蹤
為什么項(xiàng)目內(nèi)需要鏈路追蹤?
當(dāng)一個(gè)請求中,請求了多個(gè)服務(wù)單元,如果請求出現(xiàn)了錯(cuò)誤或異常,很難去定位是哪個(gè)服務(wù)出了問題,這時(shí)就需要鏈路追蹤。

這個(gè)圖畫的比較簡單,從圖中可以清晰的看出他們之間的調(diào)用關(guān)系,通過一個(gè)例子說明下鏈路的重要性,比如對(duì)方調(diào)我們一個(gè)接口,反饋在某個(gè)時(shí)間段這接口太慢了,在排查代碼發(fā)現(xiàn)邏輯比較復(fù)雜,不光調(diào)用了多個(gè)三方接口、操作了數(shù)據(jù)庫,還操作了緩存,怎么快速定位是哪塊執(zhí)行時(shí)間很長?
不賣關(guān)子,先說下本篇文章最終實(shí)現(xiàn)了什么,如果感興趣再繼續(xù)往下看。
實(shí)現(xiàn)了通過記錄如下參數(shù),來進(jìn)行問題定位,關(guān)于每個(gè)參數(shù)的結(jié)構(gòu)在下面都有介紹。
//?Trace?記錄的參數(shù)
type?Trace?struct?{
????mux????????????????sync.Mutex
????Identifier?????????string????`json:"trace_id"`?????????????//?鏈路?ID
????Request????????????*Request??`json:"request"`??????????????//?請求信息
????Response???????????*Response?`json:"response"`?????????????//?響應(yīng)信息
????ThirdPartyRequests?[]*Dialog?`json:"third_party_requests"`?//?調(diào)用第三方接口的信息
????Debugs?????????????[]*Debug??`json:"debugs"`???????????????//?調(diào)試信息
????SQLs???????????????[]*SQL????`json:"sqls"`?????????????????//?執(zhí)行的?SQL?信息
????Redis??????????????[]*Redis??`json:"redis"`????????????????//?執(zhí)行的?Redis?信息
????Success????????????bool??????`json:"success"`??????????????//?請求結(jié)果?true?or?false
????CostSeconds????????float64???`json:"cost_seconds"`?????????//?執(zhí)行時(shí)長(單位秒)
}
參數(shù)結(jié)構(gòu)
鏈路 ID
String 例如:4b4f81f015a4f2a01b00。如果請求 Header 中存在 TRACE-ID,就使用它,反之,重新創(chuàng)建一個(gè)。將 TRACE_ID 放到接口返回值中,這樣就可以通過這個(gè)標(biāo)示查到這一串的信息。
請求信息
Object,結(jié)構(gòu)如下:
type?Request?struct?{
?TTL????????string??????`json:"ttl"`?????????//?請求超時(shí)時(shí)間
?Method?????string??????`json:"method"`??????//?請求方式
?DecodedURL?string??????`json:"decoded_url"`?//?請求地址
?Header?????interface{}?`json:"header"`??????//?請求?Header?信息
?Body???????interface{}?`json:"body"`????????//?請求?Body?信息
}
響應(yīng)信息
Object,結(jié)構(gòu)如下:
type?Response?struct?{
?Header??????????interface{}?`json:"header"`??????????????????????//?Header?信息
?Body????????????interface{}?`json:"body"`????????????????????????//?Body?信息
?BusinessCode????int?????????`json:"business_code,omitempty"`?????//?業(yè)務(wù)碼
?BusinessCodeMsg?string??????`json:"business_code_msg,omitempty"`?//?提示信息
?HttpCode????????int?????????`json:"http_code"`???????????????????//?HTTP?狀態(tài)碼
?HttpCodeMsg?????string??????`json:"http_code_msg"`???????????????//?HTTP?狀態(tài)碼信息
?CostSeconds?????float64?????`json:"cost_seconds"`????????????????//?執(zhí)行時(shí)間(單位秒)
}
調(diào)用三方接口信息
Object,結(jié)構(gòu)如下:
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"`?//?執(zhí)行時(shí)長(單位秒)
}
這里面的 Request 和 Response 結(jié)構(gòu)與上面保持一致。
細(xì)節(jié)來了,為什么 Responses 結(jié)構(gòu)是 []*Response ?
是因?yàn)?HTTP 可以進(jìn)行重試請求,比如當(dāng)請求對(duì)方接口的時(shí)候,HTTP 狀態(tài)碼為 503 http.StatusServiceUnavailable,這時(shí)需要重試,我們也需要把重試的響應(yīng)信息記錄下來。
調(diào)試信息
Object 結(jié)構(gòu)如下:
type?Debug?struct?{
?Key?????????string??????`json:"key"`??????????//?標(biāo)示
?Value???????interface{}?`json:"value"`????????//?值
?CostSeconds?float64?????`json:"cost_seconds"`?//?執(zhí)行時(shí)間(單位秒)
}
SQL 信息
Object,結(jié)構(gòu)如下:
type?SQL?struct?{
?Timestamp???string??`json:"timestamp"`?????//?時(shí)間,格式:2006-01-02 15:04:05
?Stack???????string??`json:"stack"`?????????//?文件地址和行號(hào)
?SQL?????????string??`json:"sql"`???????????//?SQL?語句
?Rows????????int64???`json:"rows_affected"`?//?影響行數(shù)
?CostSeconds?float64?`json:"cost_seconds"`??//?執(zhí)行時(shí)長(單位秒)
}
Redis 信息
Object,結(jié)構(gòu)如下:
type?Redis?struct?{
?Timestamp???string??`json:"timestamp"`???????//?時(shí)間,格式: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"`???//?超時(shí)時(shí)長(單位分)
?CostSeconds?float64?`json:"cost_seconds"`????//?執(zhí)行時(shí)間(單位秒)
}
請求結(jié)果
Bool,這個(gè)和統(tǒng)一定義返回值有點(diǎn)關(guān)系,看下代碼:
//?錯(cuò)誤返回
c.AbortWithError(code.ErrParamBind.WithErr(err))
//?正確返回
c.Payload(code.OK.WithData(data))
當(dāng)錯(cuò)誤返回時(shí) 且 ctx.Writer.Status() != http.StatusOK 時(shí),為 false,反之為 true。
執(zhí)行時(shí)長
Float64,例如:0.041746869,記錄的是從請求開始到請求結(jié)束所花費(fèi)的時(shí)間。
如何收集參數(shù)?
這時(shí)有老鐵會(huì)說了:“規(guī)劃的稍微還行,使用的時(shí)候會(huì)不會(huì)很麻煩?”
“No,No,使用起來一丟丟都不麻煩”,接著往下看。
無需關(guān)心的參數(shù)
鏈路 ID、請求信息、響應(yīng)信息、請求結(jié)果、執(zhí)行時(shí)長,這 5 個(gè)參數(shù),開發(fā)者無需關(guān)心,這些都在中間件封裝好了。
調(diào)用第三方接口的信息
只需多傳遞一個(gè)參數(shù)即可。
在這里厚臉皮自薦下 httpclient 包 。
支持設(shè)置失敗時(shí)重試,可以自定義重試次數(shù)、重試前延遲等待時(shí)間、重試的滿足條件; 支持設(shè)置失敗時(shí)告警,可以自定義告警渠道(郵件/微信)、告警的滿足條件; 支持設(shè)置調(diào)用鏈路;
調(diào)用示例代碼:
//?httpclient?是項(xiàng)目中封裝的包
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()),??//?傳遞上下文
)
調(diào)試信息
只需多傳遞一個(gè)參數(shù)即可。
調(diào)用示例代碼:
// p 是項(xiàng)目中封裝的包
p.Println("key",?"value",
?p.WithTrace(ctx.Trace()),?//?傳遞上下文
)
SQL 信息
稍微復(fù)雜一丟丟,需要多傳遞一個(gè)參數(shù),然后再寫一個(gè) GORM 插件。
使用的 GORM V2 自帶的 Callbacks 和 Context 知識(shí)點(diǎn),細(xì)節(jié)不多說,可以看下這篇文章:基于 GORM 獲取當(dāng)前請求所執(zhí)行的 SQL 信息。
調(diào)用示例代碼:
//?原來查詢這樣寫
err?:=?u.db.GetDbR().
????First(data,?id).
????Where("is_deleted?=??",?-1).
????Error
//?現(xiàn)在只需這樣寫
err?:=?u.db.GetDbR().
????WithContext(ctx.RequestContext()).
????First(data,?id).
????Where("is_deleted?=??",?-1).
????Error
????
// .WithContext 是 GORM V2 自帶的。????
//?插件的代碼就不貼了,去上面的文章查看即可。
Redis 信息
只需多傳遞一個(gè)參數(shù)即可。
調(diào)用示例代碼:
//?cache?是基于?go-redis?封裝的包
d.cache.Get("name",?
????cache.WithTrace(c.Trace()),
)
核心原理是啥?
在這沒關(guān)子可賣,看到這相信老鐵們都知道了,就兩個(gè):一個(gè)是 攔截器,另一個(gè)是 Context。

如何記錄參數(shù)?
將以上數(shù)據(jù)轉(zhuǎn)為 JSON 結(jié)構(gòu)記錄到日志中。
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 日志組件
有對(duì)日志收集感興趣的老鐵們可以往下看,trace_info 只是日志的一個(gè)參數(shù),具體日志參數(shù)包括:
| 參數(shù) | 數(shù)據(jù)類型 | 說明 |
|---|---|---|
| level | String | 日志級(jí)別,例如:info,warn,error,debug |
| time | String | 時(shí)間,例如:2021-01-30 16:05:44 |
| caller | String | 調(diào)用位置,文件+行號(hào),例如:core/core.go:443 |
| msg | String | 日志信息,例如:xx 錯(cuò)誤 |
| domain | String | 域名或服務(wù)名,例如:go-gin-api[fat] |
| method | String | 請求方式,例如:POST |
| path | String | 請求路徑,例如:/user/create |
| http_code | Int | HTTP 狀態(tài)碼,例如:200 |
| business_code | Int | 業(yè)務(wù)狀態(tài)碼,例如:10101 |
| success | Bool | 狀態(tài),true or false |
| cost_seconds | Float64 | 花費(fèi)時(shí)間,單位:秒,例如:0.01 |
| trace_id | String | 鏈路ID,例如:ec3c868c8dcccfe515ab |
| trace_info | Object | 鏈路信息,結(jié)構(gòu)化數(shù)據(jù)。 |
| error | String | 錯(cuò)誤信息,當(dāng)出現(xiàn)錯(cuò)誤時(shí)才有這字段。 |
| errorVerbose | String | 詳細(xì)的錯(cuò)誤堆棧信息,當(dāng)出現(xiàn)錯(cuò)誤時(shí)才有這字段。 |
日志記錄可以使用 zap,logrus ,這次我使用的 zap,簡單封裝一下即可,比如:
支持設(shè)置日志級(jí)別; 支持設(shè)置日志輸出到控制臺(tái); 支持設(shè)置日志輸出到文件; 支持設(shè)置日志輸出到文件(可自動(dòng)分割);
總結(jié)
這個(gè)功能比較常用,使用起來也很爽,比如調(diào)用方發(fā)現(xiàn)接口出問題時(shí),只需要提供 TRACE-ID 即可,我們就可以查到關(guān)于它整個(gè)鏈路的所有信息。
以上代碼都在 go-gin-api 項(xiàng)目中,地址:https://github.com/xinliangnote/go-gin-api
推薦閱讀
