- Created by 郭强, last modified on May 25, 2021
在本章节中,我们演示两个示例,一个用于演示baggage
服务间上下文数据传递;一个用于演示较完整的HTTP+DB+Redis+Logging
组件的链路跟踪。
baggage
服务间链路数据传递
示例代码地址:https://github.com/gogf/gf-tracing/tree/master/examples/http
客户端
package main import ( "context" "gftracing/tracing" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/net/ghttp" "github.com/gogf/gf/net/gtrace" ) const ( ServiceName = "tracing-http-client" JaegerUdpEndpoint = "localhost:6831" ) func main() { flush, err := tracing.InitJaeger(ServiceName, JaegerUdpEndpoint) if err != nil { g.Log().Fatal(err) } defer flush() StartRequests() } func StartRequests() { ctx, span := gtrace.NewSpan(context.Background(), "StartRequests") defer span.End() ctx = gtrace.SetBaggageValue(ctx, "name", "john") client := g.Client().Use(ghttp.MiddlewareClientTracing) content := client.Ctx(ctx).GetContent("http://127.0.0.1:8199/hello") g.Log().Ctx(ctx).Print(content) }
客户端代码简要说明:
- 首先,客户端也是需要通过
initTracer
方法初始化Jaeger
。 - 随后,这里通过
gtrace.SetBaggageValue(ctx, "name", "john")
方法设置了一个baggage
,该baggage
将会在该请求的所有链路中传递。不过我们该示例也有两个节点,因此该baggage
数据只会传递到服务端。该方法会返回一个新的context.Context
上下文变量,在随后的调用链中我们将需要传递这个新的上下文变量。 - 其中,这里通过
g.Client().Use(ghttp.MiddlewareClientTracing)
创建一个HTTP客户端请求对象,并通过Use
方法设置客户端请求的拦截器。随后该客户端对象所有的的请求都将会通过拦截器处理后再发出。这里的注册拦截器ghttp.MiddlewareClientTracing
主要用于启用链路跟踪特性,否则客户端请求中不会自动增加链路信息。 - 最后,这里使用了
g.Log().Ctx(ctx).Print(content)
方法打印服务端的返回内容,其中的Ctx(ctx)
便是将链路信息传递给日志组件,如果ctx
上下文对象中存在链路信息时,日志组件会同时自动将TraceId
输出到日志内容中。
服务端
package main import ( "gftracing/tracing" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/net/ghttp" "github.com/gogf/gf/net/gtrace" ) const ( ServiceName = "tracing-http-server" JaegerUdpEndpoint = "localhost:6831" ) func main() { flush, err := tracing.InitJaeger(ServiceName, JaegerUdpEndpoint) if err != nil { g.Log().Fatal(err) } defer flush() s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(ghttp.MiddlewareServerTracing) group.GET("/hello", HelloHandler) }) s.SetPort(8199) s.Run() } func HelloHandler(r *ghttp.Request) { ctx, span := gtrace.NewSpan(r.Context(), "HelloHandler") defer span.End() value := gtrace.GetBaggageVar(ctx, "name").String() r.Response.Write("hello:", value) }
服务端代码简要说明:
- 当然,服务端也是需要通过
initTracer
方法初始化Jaeger
。 - 服务端通过
group.Middleware(ghttp.MiddlewareServerTracing)
注册一个分组路由中间件,该中间件的作用是启用链路跟踪特性,所有该分组路由下的请求都将会经过中间件的处理后再将请求转交给路由方法。我们在项目中也可以注册全局中间件的形式来启用链路跟踪特性,关于中间件的介绍请查看 路由管理-中间件/拦截器 章节。 - 服务端通过
gtrace.GetBaggageVar(ctx, "name").String()
方法获取客户端提交的baggage
信息,并转换为字符串返回。
效果查看
启动服务端:
启动客户端:
可以看到,客户端提交的baggage
已经被服务端成功接收到并打印返回。并且客户端在输出日志内容的时候也同时输出的TraceId
信息。TraceId
是一条链路的唯一ID,可以通过该ID检索该链路的所有日志信息,并且也可以通过该TraceId
在Jaeger
系统上查询该调用链路详情。
在Jaeger
上查看链路信息:
可以看到在这里出现了两个服务名称:tracing-http-client
和tracing-http-server
,表示我们这次请求涉及到两个服务,分别是HTTP请求的客户端和服务端,并且每个服务中分别涉及到2
个span
链路节点。
我们点击这个trace
的详情,可以看得到调用链的层级关系。并且可以看得到客户端请求的地址、服务端接收的路由以及服务端路由函数名称。我们这里来介绍一下客户端的Atttributes
信息和Events
信息,也就是Jaeger
中展示的Tags
信息和Process
信息。
HTTP Client Attributes
Attribute/Tag | 说明 | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
otel.instrumentation_library.name | 当前仪表器名称,往往是当前span 操作的组件名称 | ||||||||||||||
otel.instrumentation_library.version | 当前仪表器组件版本 | ||||||||||||||
span.kind | 当前
| ||||||||||||||
status.code | 当前span 状态,0 为正常,非0 表示失败 | ||||||||||||||
status.message | 当前span 状态信息,往往在失败时会带有错误信息 | ||||||||||||||
hostname | 当前节点的主机名称 | ||||||||||||||
ip.intranet | 当前节点的主机内网地址列表 | ||||||||||||||
http.address.local | HTTP通信的本地地址和端口 | ||||||||||||||
http.address.remote | HTTP通信的目标地址和端口 | ||||||||||||||
http.dns.start | 当请求的目标地址带有域名时,开始解析的域名地址 | ||||||||||||||
http.dns.done | 当请求的目标地址带有域名时,解析结束之后的IP地址 | ||||||||||||||
http.connect.start | 开始创建连接的类型和地址 | ||||||||||||||
http.connect.done | 创建连接成功后的类型和地址 |
HTTP Client Events
Event/Log | 说明 |
---|---|
http.request.headers | HTTP客户端请求提交的Header 信息,可能会比较大。 |
http.request.baggage | HTTP客户端请求提交的Baggage 信息,用于服务间链路信息传递。 |
http.request.body | HTTP客户端请求提交的 |
http.response.headers | HTTP客户端请求接收返回的的Header 信息,可能会比较大。 |
http.response.body | HTTP客户端请求接收返回的Body 数据,可能会比较大,最大只记录512KB ,如果超过该大小则忽略。 |
HTTP Server Attributes
HTTP Server
端的Attributes
含义同HTTP Client
,在同一请求中,打印的数据基本一致。
HTTP Server Events
HTTP Server
端的Events
含义同HTTP Client
,在同一请求中,打印的数据基本一致。
HTTP+DB+Redis+Logging
我们再来看一个相对完整一点的例子,包含几个常用核心组件的链路跟踪示例,示例代码地址:https://github.com/gogf/gf-tracing/tree/master/examples/http_db_redis_log
客户端
package main import ( "context" "gftracing/tracing" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/net/ghttp" "github.com/gogf/gf/net/gtrace" ) const ( ServiceName = "tracing-http-client" JaegerUdpEndpoint = "localhost:6831" ) func main() { flush, err := tracing.InitJaeger(ServiceName, JaegerUdpEndpoint) if err != nil { g.Log().Fatal(err) } defer flush() StartRequests() } func StartRequests() { ctx, span := gtrace.NewSpan(context.Background(), "StartRequests") defer span.End() client := g.Client().Use(ghttp.MiddlewareClientTracing) // Add user info. idStr := client.Ctx(ctx).PostContent( "http://127.0.0.1:8199/user/insert", g.Map{ "name": "john", }, ) if idStr == "" { g.Log().Ctx(ctx).Print("retrieve empty id string") return } g.Log().Ctx(ctx).Print("insert:", idStr) // Query user info. userJson := client.Ctx(ctx).GetContent( "http://127.0.0.1:8199/user/query", g.Map{ "id": idStr, }, ) g.Log().Ctx(ctx).Print("query:", idStr, userJson) // Delete user info. deleteResult := client.Ctx(ctx).PostContent( "http://127.0.0.1:8199/user/delete", g.Map{ "id": idStr, }, ) g.Log().Ctx(ctx).Print("delete:", idStr, deleteResult) }
客户端代码简要说明:
- 首先,客户端也是需要通过
initTracer
方法初始化Jaeger
。 - 在本示例中,我们通过HTTP客户端向服务端发起了
3
次请求:/user/insert
用于新增一个用户信息,成功后返回用户的ID。/user/query
用于查询用户,使用前一个接口返回的用户ID。/user/delete
用于删除用户,使用之前接口返回的用户ID。
服务端
package main import ( "fmt" "gftracing/tracing" "github.com/gogf/gcache-adapter/adapter" "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/net/ghttp" "time" ) type tracingApi struct{} const ( ServiceName = "tracing-http-server" JaegerUdpEndpoint = "localhost:6831" ) func main() { flush, err := tracing.InitJaeger(ServiceName, JaegerUdpEndpoint) if err != nil { g.Log().Fatal(err) } defer flush() g.DB().GetCache().SetAdapter(adapter.NewRedis(g.Redis())) s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(ghttp.MiddlewareServerTracing) group.ALL("/user", new(tracingApi)) }) s.SetPort(8199) s.Run() } type userApiInsert struct { Name string `v:"required#Please input user name."` } // Insert is a route handler for inserting user info into dtabase. func (api *tracingApi) Insert(r *ghttp.Request) { var ( dataReq *userApiInsert ) if err := r.Parse(&dataReq); err != nil { r.Response.WriteExit(gerror.Current(err)) } result, err := g.Table("user").Ctx(r.Context()).Insert(g.Map{ "name": dataReq.Name, }) if err != nil { r.Response.WriteExit(gerror.Current(err)) } id, _ := result.LastInsertId() r.Response.Write(id) } type userApiQuery struct { Id int `v:"min:1#User id is required for querying."` } // Query is a route handler for querying user info. It firstly retrieves the info from redis, // if there's nothing in the redis, it then does db select. func (api *tracingApi) Query(r *ghttp.Request) { var ( dataReq *userApiQuery ) if err := r.Parse(&dataReq); err != nil { r.Response.WriteExit(gerror.Current(err)) } one, err := g.Table("user"). Ctx(r.Context()). Cache(5*time.Second, api.userCacheKey(dataReq.Id)). FindOne(dataReq.Id) if err != nil { r.Response.WriteExit(gerror.Current(err)) } r.Response.WriteJson(one) } type userApiDelete struct { Id int `v:"min:1#User id is required for deleting."` } // Delete is a route handler for deleting specified user info. func (api *tracingApi) Delete(r *ghttp.Request) { var ( dataReq *userApiDelete ) if err := r.Parse(&dataReq); err != nil { r.Response.WriteExit(gerror.Current(err)) } _, err := g.Table("user"). Ctx(r.Context()). Cache(-1, api.userCacheKey(dataReq.Id)). WherePri(dataReq.Id). Delete() if err != nil { r.Response.WriteExit(gerror.Current(err)) } r.Response.Write("ok") } func (api *tracingApi) userCacheKey(id int) string { return fmt.Sprintf(`userInfo:%d`, id) }
服务端代码简要说明:
- 首先,客户端也是需要通过
initTracer
方法初始化Jaeger
。 - 在本示例中,我们使用到了数据库和数据库缓存功能,以便于同时演示
orm
和redis
的链路跟踪记录。 - 我们在程序启动时通过
g.DB().GetCache().SetAdapter(adapter.NewRedis(g.Redis()))
设置当前数据库缓存管理的适配器为redis
。关于缓存适配器的介绍感兴趣可以参考 缓存管理-缓存适配 章节。 - 在
orm
的操作中,需要通过Ctx
方法将上下文变量传递到组件中,orm
组件会自动识别当前上下文中是否包含Tracing链路信息,如果包含则自动启用链路跟踪特性。 - 在
orm
的操作中,这里使用Cache
方法缓存查询结果到redis
中,并在删除操作中也使用Cache
方法清除redis
中的缓存结果。关于orm
的缓存管理介绍请参考 ORM链式操作-查询缓存 章节。在orm
的内部实现中,也是会将该context.Context
上下文变量传递给redis
链式操作中,redis
组件也有一个Ctx
链式操作方法,这样redis
组件也会自动识别链路信息并做自动开启。
效果查看
启动服务端:
启动客户端:
在Jaeger
上查看链路信息:
可以看到,这次请求总共产生了14
个span
,其中客户端有4
个span
,服务端有10
个span
,每一个span
代表一个链路节点。不过,我们注意到,这里产生了3
个errors
。我们点击详情查看什么原因呢。
我们看到好像所有的redis
操作都报错了,随便点击一个redis
的相关span
,查看一下详情呢:
原来是redis
连接不上报错了,这样的话所有的orm
缓存功能都失效了,但是可以看到并没有影响接口逻辑,只是所有的查询都走了数据库。这个报错是因为我本地忘了打开redis server
,我赶紧启动一下本地的redis server
,再看看效果:
再把上面的客户端运行一下,查看jaeger
:
现在就没有报错了。
HTTP Client&Server
、Logging
组件在之前已经介绍过,因此这里我们主要关注orm
和redis
组件的链路跟踪信息。
ORM链路信息
Attributes/Tags
我们随便点开一个ORM
链路Span
,看看Attributes/Tags
信息:
可以看到这里的span.kind
是internal
,也就是之前介绍过的方法内部span
类型。这里很多Tags
在之前已经介绍过,因此这里主要介绍关于数据库相关的Tags
:
Attribute/Tag | 说明 |
---|---|
db.type
| 数据库连接类型。如mysql , mssql , pgsql 等等。 |
db.link | 数据库连接信息。其中密码字段被自动隐藏。 |
db.group | 在配置文件中的数据库分组名称。 |
Events/Process
Event/Log | 说明 |
---|---|
db.execution.sql | 执行的具体SQL 语句。由于ORM底层是预处理,该语句为方便查看自动拼接而成,仅供参考。 |
db.execution.type | 执行的SQL 语句类型。常见为DB.ExecContext 和DB.QueryContext ,分别代表写操作和读操作。 |
db.execution.cost | 当前 |
Redis链路信息
Attributes/Tags
Attribute/Tag | 说明 |
---|---|
redis.host
| Redis 连接地址。 |
redis.port | Redis 连接端口。 |
redis.db | Redis 操作db 。 |
Events/Process
Event/Log | 说明 |
---|---|
redis.execution.command | Redis 执行指令。 |
redis.execution.arguments | Redis 执行指令参数。 |
redis.execution.cost |
|
- No labels