经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Go语言 » 查看文章
Go - 实现项目内链路追踪
来源:cnblogs  作者:新亮笔记  时间:2021/2/18 15:35:50  对本文有异议

为什么项目内需要链路追踪?当一个请求中,请求了多个服务单元,如果请求出现了错误或异常,很难去定位是哪个服务出了问题,这时就需要链路追踪。

从图中可以清晰的看出他们之间的调用关系,通过一个例子说明下链路的重要性,比如对方调我们一个接口,反馈在某个时间段这接口太慢了,在排查代码发现逻辑比较复杂,不光调用了多个三方接口、操作了数据库,还操作了缓存,怎么快速定位是哪块执行时间很长?

不卖关子,先说下本篇文章最终实现了什么,如果感兴趣再继续往下看。

实现了通过记录如下参数,来进行问题定位,关于每个参数的结构在下面都有介绍。

  1. // Trace 记录的参数
  2. type Trace struct {
  3. mux sync.Mutex
  4. Identifier string `json:"trace_id"` // 链路 ID
  5. Request *Request `json:"request"` // 请求信息
  6. Response *Response `json:"response"` // 响应信息
  7. ThirdPartyRequests []*Dialog `json:"third_party_requests"` // 调用第三方接口的信息
  8. Debugs []*Debug `json:"debugs"` // 调试信息
  9. SQLs []*SQL `json:"sqls"` // 执行的 SQL 信息
  10. Redis []*Redis `json:"redis"` // 执行的 Redis 信息
  11. Success bool `json:"success"` // 请求结果 true or false
  12. CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒)
  13. }

参数结构

链路 ID

String 例如:4b4f81f015a4f2a01b00。如果请求 Header 中存在 TRACE-ID,就使用它,反之,重新创建一个。将 TRACE_ID 放到接口返回值中,这样就可以通过这个标示查到这一串的信息。

请求信息

Object,结构如下:

  1. type Request struct {
  2. TTL string `json:"ttl"` // 请求超时时间
  3. Method string `json:"method"` // 请求方式
  4. DecodedURL string `json:"decoded_url"` // 请求地址
  5. Header interface{} `json:"header"` // 请求 Header 信息
  6. Body interface{} `json:"body"` // 请求 Body 信息
  7. }

响应信息

Object,结构如下:

  1. type Response struct {
  2. Header interface{} `json:"header"` // Header 信息
  3. Body interface{} `json:"body"` // Body 信息
  4. BusinessCode int `json:"business_code,omitempty"` // 业务码
  5. BusinessCodeMsg string `json:"business_code_msg,omitempty"` // 提示信息
  6. HttpCode int `json:"http_code"` // HTTP 状态码
  7. HttpCodeMsg string `json:"http_code_msg"` // HTTP 状态码信息
  8. CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒)
  9. }

调用三方接口信息

Object,结构如下:

  1. type Dialog struct {
  2. mux sync.Mutex
  3. Request *Request `json:"request"` // 请求信息
  4. Responses []*Response `json:"responses"` // 返回信息
  5. Success bool `json:"success"` // 是否成功,true 或 false
  6. CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒)
  7. }

这里面的 RequestResponse 结构与上面保持一致。

细节来了,为什么 Responses 结构是 []*Response

是因为 HTTP 可以进行重试请求,比如当请求对方接口的时候,HTTP 状态码为 503 http.StatusServiceUnavailable,这时需要重试,我们也需要把重试的响应信息记录下来。

调试信息

Object 结构如下:

  1. type Debug struct {
  2. Key string `json:"key"` // 标示
  3. Value interface{} `json:"value"` // 值
  4. CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒)
  5. }

SQL 信息

Object,结构如下:

  1. type SQL struct {
  2. Timestamp string `json:"timestamp"` // 时间,格式:2006-01-02 15:04:05
  3. Stack string `json:"stack"` // 文件地址和行号
  4. SQL string `json:"sql"` // SQL 语句
  5. Rows int64 `json:"rows_affected"` // 影响行数
  6. CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒)
  7. }

Redis 信息

Object,结构如下:

  1. type Redis struct {
  2. Timestamp string `json:"timestamp"` // 时间,格式:2006-01-02 15:04:05
  3. Handle string `json:"handle"` // 操作,SET/GET 等
  4. Key string `json:"key"` // Key
  5. Value string `json:"value,omitempty"` // Value
  6. TTL float64 `json:"ttl,omitempty"` // 超时时长(单位分)
  7. CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒)
  8. }

请求结果

Bool,这个和统一定义返回值有点关系,看下代码:

  1. // 错误返回
  2. c.AbortWithError(code.ErrParamBind.WithErr(err))
  3. // 正确返回
  4. c.Payload(code.OK.WithData(data))

当错误返回时 且 ctx.Writer.Status() != http.StatusOK 时,为 false,反之为 true

执行时长

Float64,例如:0.041746869,记录的是从请求开始到请求结束所花费的时间。

如何收集参数?

这时有老铁会说了:“规划的稍微还行,使用的时候会不会很麻烦?”

“No,No,使用起来一丢丢都不麻烦”,接着往下看。

无需关心的参数

链路 ID、请求信息、响应信息、请求结果、执行时长,这 5 个参数,开发者无需关心,这些都在中间件封装好了。

调用第三方接口的信息

只需多传递一个参数即可。

在这里厚脸皮自荐下 httpclient 包

  • 支持设置失败时重试,可以自定义重试次数、重试前延迟等待时间、重试的满足条件;
  • 支持设置失败时告警,可以自定义告警渠道(邮件/微信)、告警的满足条件;
  • 支持设置调用链路;

调用示例代码:

  1. // httpclient 是项目中封装的包
  2. api := "http://127.0.0.1:9999/demo/post"
  3. params := url.Values{}
  4. params.Set("name", name)
  5. body, err := httpclient.PostForm(api, params,
  6. httpclient.WithTrace(ctx.Trace()), // 传递上下文
  7. )

调试信息

只需多传递一个参数即可。

调用示例代码:

  1. // p 是项目中封装的包
  2. p.Println("key", "value",
  3. p.WithTrace(ctx.Trace()), // 传递上下文
  4. )

SQL 信息

稍微复杂一丢丢,需要多传递一个参数,然后再写一个 GORM 插件。

使用的 GORM V2 自带的 CallbacksContext 知识点,细节不多说,可以看下这篇文章:基于 GORM 获取当前请求所执行的 SQL 信息

调用示例代码:

  1. // 原来查询这样写
  2. err := u.db.GetDbR().
  3. First(data, id).
  4. Where("is_deleted = ?", -1).
  5. Error
  6. // 现在只需这样写
  7. err := u.db.GetDbR().
  8. WithContext(ctx.RequestContext()).
  9. First(data, id).
  10. Where("is_deleted = ?", -1).
  11. Error
  12. // .WithContext 是 GORM V2 自带的。
  13. // 插件的代码就不贴了,去上面的文章查看即可。

Redis 信息

只需多传递一个参数即可。

调用示例代码:

  1. // cache 是基于 go-redis 封装的包
  2. d.cache.Get("name",
  3. cache.WithTrace(c.Trace()),
  4. )

核心原理是啥?

在这没关子可卖,看到这相信老铁们都知道了,就两个:一个是 拦截器,另一个是 Context

如何记录参数?

将以上数据转为 JSON 结构记录到日志中。

JSON 示例

  1. {
  2. "level":"info",
  3. "time":"2021-01-30 22:32:48",
  4. "caller":"core/core.go:444",
  5. "msg":"core-interceptor",
  6. "domain":"go-gin-api[fat]",
  7. "method":"GET",
  8. "path":"/demo/trace",
  9. "http_code":200,
  10. "business_code":1,
  11. "success":true,
  12. "cost_seconds":0.054025302,
  13. "trace_id":"2cdb2f96934f573af391",
  14. "trace_info":{
  15. "trace_id":"2cdb2f96934f573af391",
  16. "request":{
  17. "ttl":"un-limit",
  18. "method":"GET",
  19. "decoded_url":"/demo/trace",
  20. "header":{
  21. "Accept":[
  22. "application/json"
  23. ],
  24. "Accept-Encoding":[
  25. "gzip, deflate, br"
  26. ],
  27. "Accept-Language":[
  28. "zh-CN,zh;q=0.9,en;q=0.8"
  29. ],
  30. "Authorization":[
  31. "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEsIlVzZXJOYW1lIjoieGlubGlhbmdub3RlIiwiZXhwIjoxNjEyMTAzNTQwLCJpYXQiOjE2MTIwMTcxNDAsIm5iZiI6MTYxMjAxNzE0MH0.2yHDdP7cNT5uL5xA0-j_NgTK4GrW-HGn0KUxcbZfpKg"
  32. ],
  33. "Connection":[
  34. "keep-alive"
  35. ],
  36. "Referer":[
  37. "http://127.0.0.1:9999/swagger/index.html"
  38. ],
  39. "Sec-Fetch-Dest":[
  40. "empty"
  41. ],
  42. "Sec-Fetch-Mode":[
  43. "cors"
  44. ],
  45. "Sec-Fetch-Site":[
  46. "same-origin"
  47. ],
  48. "User-Agent":[
  49. "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"
  50. ]
  51. },
  52. "body":""
  53. },
  54. "response":{
  55. "header":{
  56. "Content-Type":[
  57. "application/json; charset=utf-8"
  58. ],
  59. "Trace-Id":[
  60. "2cdb2f96934f573af391"
  61. ],
  62. "Vary":[
  63. "Origin"
  64. ]
  65. },
  66. "body":{
  67. "code":1,
  68. "msg":"OK",
  69. "data":[
  70. {
  71. "name":"Tom",
  72. "job":"Student"
  73. },
  74. {
  75. "name":"Jack",
  76. "job":"Teacher"
  77. }
  78. ],
  79. "id":"2cdb2f96934f573af391"
  80. },
  81. "business_code":1,
  82. "business_code_msg":"OK",
  83. "http_code":200,
  84. "http_code_msg":"OK",
  85. "cost_seconds":0.054024874
  86. },
  87. "third_party_requests":[
  88. {
  89. "request":{
  90. "ttl":"5s",
  91. "method":"GET",
  92. "decoded_url":"http://127.0.0.1:9999/demo/get/Tom",
  93. "header":{
  94. "Authorization":[
  95. "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEsIlVzZXJOYW1lIjoieGlubGlhbmdub3RlIiwiZXhwIjoxNjEyMTAzNTQwLCJpYXQiOjE2MTIwMTcxNDAsIm5iZiI6MTYxMjAxNzE0MH0.2yHDdP7cNT5uL5xA0-j_NgTK4GrW-HGn0KUxcbZfpKg"
  96. ],
  97. "Content-Type":[
  98. "application/x-www-form-urlencoded; charset=utf-8"
  99. ],
  100. "TRACE-ID":[
  101. "2cdb2f96934f573af391"
  102. ]
  103. },
  104. "body":null
  105. },
  106. "responses":[
  107. {
  108. "header":{
  109. "Content-Length":[
  110. "87"
  111. ],
  112. "Content-Type":[
  113. "application/json; charset=utf-8"
  114. ],
  115. "Date":[
  116. "Sat, 30 Jan 2021 14:32:48 GMT"
  117. ],
  118. "Trace-Id":[
  119. "2cdb2f96934f573af391"
  120. ],
  121. "Vary":[
  122. "Origin"
  123. ]
  124. },
  125. "body":"{"code":1,"msg":"OK","data":{"name":"Tom","job":"Student"},"id":"2cdb2f96934f573af391"}",
  126. "http_code":200,
  127. "http_code_msg":"200 OK",
  128. "cost_seconds":0.000555089
  129. }
  130. ],
  131. "success":true,
  132. "cost_seconds":0.000580202
  133. },
  134. {
  135. "request":{
  136. "ttl":"5s",
  137. "method":"POST",
  138. "decoded_url":"http://127.0.0.1:9999/demo/post",
  139. "header":{
  140. "Authorization":[
  141. "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEsIlVzZXJOYW1lIjoieGlubGlhbmdub3RlIiwiZXhwIjoxNjEyMTAzNTQwLCJpYXQiOjE2MTIwMTcxNDAsIm5iZiI6MTYxMjAxNzE0MH0.2yHDdP7cNT5uL5xA0-j_NgTK4GrW-HGn0KUxcbZfpKg"
  142. ],
  143. "Content-Type":[
  144. "application/x-www-form-urlencoded; charset=utf-8"
  145. ],
  146. "TRACE-ID":[
  147. "2cdb2f96934f573af391"
  148. ]
  149. },
  150. "body":"name=Jack"
  151. },
  152. "responses":[
  153. {
  154. "header":{
  155. "Content-Length":[
  156. "88"
  157. ],
  158. "Content-Type":[
  159. "application/json; charset=utf-8"
  160. ],
  161. "Date":[
  162. "Sat, 30 Jan 2021 14:32:48 GMT"
  163. ],
  164. "Trace-Id":[
  165. "2cdb2f96934f573af391"
  166. ],
  167. "Vary":[
  168. "Origin"
  169. ]
  170. },
  171. "body":"{"code":1,"msg":"OK","data":{"name":"Jack","job":"Teacher"},"id":"2cdb2f96934f573af391"}",
  172. "http_code":200,
  173. "http_code_msg":"200 OK",
  174. "cost_seconds":0.000450153
  175. }
  176. ],
  177. "success":true,
  178. "cost_seconds":0.000468387
  179. }
  180. ],
  181. "debugs":[
  182. {
  183. "key":"res1.Data.Name",
  184. "value":"Tom",
  185. "cost_seconds":0.000005193
  186. },
  187. {
  188. "key":"res2.Data.Name",
  189. "value":"Jack",
  190. "cost_seconds":0.000003907
  191. },
  192. {
  193. "key":"redis-name",
  194. "value":"tom",
  195. "cost_seconds":0.000009816
  196. }
  197. ],
  198. "sqls":[
  199. {
  200. "timestamp":"2021-01-30 22:32:48",
  201. "stack":"/Users/xinliang/github/go-gin-api/internal/api/repository/db_repo/user_demo_repo/user_demo.go:76",
  202. "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",
  203. "rows_affected":1,
  204. "cost_seconds":0.031969072
  205. }
  206. ],
  207. "redis":[
  208. {
  209. "timestamp":"2021-01-30 22:32:48",
  210. "handle":"set",
  211. "key":"name",
  212. "value":"tom",
  213. "ttl":10,
  214. "cost_seconds":0.009982091
  215. },
  216. {
  217. "timestamp":"2021-01-30 22:32:48",
  218. "handle":"get",
  219. "key":"name",
  220. "cost_seconds":0.010681579
  221. }
  222. ],
  223. "success":true,
  224. "cost_seconds":0.054025302
  225. }
  226. }

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

原文链接:http://www.cnblogs.com/xinliangcoder/p/14358902.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号