Go - 实现项目内链路追踪

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
简介: Go - 实现项目内链路追踪

为什么项目内需要链路追踪?

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

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

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

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

// 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 放到接口返回值中,这样就可以通过这个标示查到这一串的信息。

请求信息

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,结构如下:

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,结构如下:

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

dfe8ac7691cf2c1b9d43c4d2bde3096b.png

如何记录参数?

将以上数据转为 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

相关实践学习
基于OpenTelemetry构建全链路追踪与监控
本实验将带领您快速上手可观测链路OpenTelemetry版,包括部署并接入多语言应用、体验TraceId自动注入至日志以实现调用链与日志的关联查询、以及切换调用链透传协议以满足全链路打通的需求。
分布式链路追踪Skywalking
Skywalking是一个基于分布式跟踪的应用程序性能监控系统,用于从服务和云原生等基础设施中收集、分析、聚合以及可视化数据,提供了一种简便的方式来清晰地观测分布式系统,具有分布式追踪、性能指标分析、应用和服务依赖分析等功能。 分布式追踪系统发展很快,种类繁多,给我们带来很大的方便。但在数据采集过程中,有时需要侵入用户代码,并且不同系统的 API 并不兼容,这就导致了如果希望切换追踪系统,往往会带来较大改动。OpenTracing为了解决不同的分布式追踪系统 API 不兼容的问题,诞生了 OpenTracing 规范。OpenTracing 是一个轻量级的标准化层,它位于应用程序/类库和追踪或日志分析程序之间。Skywalking基于OpenTracing规范开发,具有性能好,支持多语言探针,无侵入性等优势,可以帮助我们准确快速的定位到线上故障和性能瓶颈。 在本套课程中,我们将全面的讲解Skywalking相关的知识。从APM系统、分布式调用链等基础概念的学习加深对Skywalking的理解,从0开始搭建一套完整的Skywalking环境,学会对各类应用进行监控,学习Skywalking常用插件。Skywalking原理章节中,将会对Skywalking使用的agent探针技术进行深度剖析,除此之外还会对OpenTracing规范作整体上的介绍。通过对本套课程的学习,不止能学会如何使用Skywalking,还将对其底层原理和分布式架构有更深的理解。本课程由黑马程序员提供。
目录
相关文章
|
5月前
|
存储 监控 Go
带你十天轻松搞定 Go 微服务系列(九、链路追踪)
带你十天轻松搞定 Go 微服务系列(九、链路追踪)
|
5月前
|
JSON 运维 Go
Go 项目配置文件的定义和读取
Go 项目配置文件的定义和读取
|
2月前
|
Go API 数据库
Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
本文介绍了 Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
140 4
|
2月前
|
存储 JSON Go
如何在 Go 项目中隐藏敏感信息,比如避免暴露用户密码?
在Go语言开发中,用户信息管理常涉及敏感数据如密码的处理。为防止这些数据暴露给客户端,本文介绍了三种方法:使用JSON标签忽略字段、自定义序列化逻辑、使用数据传输对象(DTO),以确保用户数据的安全性。通过这些方法,可以有效控制数据输出,避免敏感信息泄露。
42 1
|
2月前
|
中间件 Go API
Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架
本文概述了Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架。
139 1
|
3月前
|
SQL 关系型数据库 MySQL
Go语言项目高效对接SQL数据库:实践技巧与方法
在Go语言项目中,与SQL数据库进行对接是一项基础且重要的任务
107 11
|
3月前
|
Go
使用go语言将A助手加入项目中
使用go语言将A助手加入项目中
28 2
|
3月前
|
存储 Kubernetes Go
Go语言项目组织架构
Go语言项目组织架构
|
5月前
|
API
企业项目迁移go-zero实战(二)
企业项目迁移go-zero实战(二)
|
5月前
|
消息中间件 中间件 API
玩转 Go 链路追踪
玩转 Go 链路追踪