在本章节中,我们演示两个示例,一个用于演示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)
}

客户端代码简要说明:

  1. 首先,客户端也是需要通过initTracer方法初始化Jaeger
  2. 随后,这里通过gtrace.SetBaggageValue(ctx, "name", "john")方法设置了一个baggage,该baggage将会在该请求的所有链路中传递。不过我们该示例也有两个节点,因此该baggage数据只会传递到服务端。该方法会返回一个新的context.Context上下文变量,在随后的调用链中我们将需要传递这个新的上下文变量。
  3. 其中,这里通过g.Client().Use(ghttp.MiddlewareClientTracing)创建一个HTTP客户端请求对象,并通过Use方法设置客户端请求的拦截器。随后该客户端对象所有的的请求都将会通过拦截器处理后再发出。这里的注册拦截器ghttp.MiddlewareClientTracing主要用于启用链路跟踪特性,否则客户端请求中不会自动增加链路信息。
  4. 最后,这里使用了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)
}

服务端代码简要说明:

  1. 当然,服务端也是需要通过initTracer方法初始化Jaeger
  2. 服务端通过group.Middleware(ghttp.MiddlewareServerTracing)注册一个分组路由中间件,该中间件的作用是启用链路跟踪特性,所有该分组路由下的请求都将会经过中间件的处理后再将请求转交给路由方法。我们在项目中也可以注册全局中间件的形式来启用链路跟踪特性,关于中间件的介绍请查看 路由管理-中间件/拦截器 章节。
  3. 服务端通过gtrace.GetBaggageVar(ctx, "name").String()方法获取客户端提交的baggage信息,并转换为字符串返回。

效果查看

启动服务端:

启动客户端:

可以看到,客户端提交的baggage已经被服务端成功接收到并打印返回。并且客户端在输出日志内容的时候也同时输出的TraceId信息。TraceId是一条链路的唯一ID,可以通过该ID检索该链路的所有日志信息,并且也可以通过该TraceIdJaeger系统上查询该调用链路详情。

Jaeger上查看链路信息:

可以看到在这里出现了两个服务名称:tracing-http-clienttracing-http-server,表示我们这次请求涉及到两个服务,分别是HTTP请求的客户端和服务端,并且每个服务中分别涉及到2span链路节点。

我们点击这个trace的详情,可以看得到调用链的层级关系。并且可以看得到客户端请求的地址、服务端接收的路由以及服务端路由函数名称。我们这里来介绍一下客户端的Atttributes信息和Events信息,也就是Jaeger中展示的Tags信息和Process信息。

HTTP Client Attributes

Attribute/Tag说明
otel.instrumentation_library.name当前仪表器名称,往往是当前span操作的组件名称
otel.instrumentation_library.version当前仪表器组件版本
span.kind

当前span的类型,一般由组件自动写入。常见span类型为:

类型说明

client 

客户端
server服务端
producer生产者,常用于MQ
consumer消费者,常用于MQ
internal内部方法,一般业务使用
undefined未定义,较少使用
status.code当前span状态,0为正常,非0表示失败
status.message当前span状态信息,往往在失败时会带有错误信息
hostname当前节点的主机名称
ip.intranet当前节点的主机内网地址列表
http.address.localHTTP通信的本地地址和端口
http.address.remoteHTTP通信的目标地址和端口
http.dns.start当请求的目标地址带有域名时,开始解析的域名地址
http.dns.done当请求的目标地址带有域名时,解析结束之后的IP地址
http.connect.start开始创建连接的类型和地址
http.connect.done创建连接成功后的类型和地址

HTTP Client Events

Event/Log说明
http.request.headersHTTP客户端请求提交的Header信息,可能会比较大。
http.request.baggageHTTP客户端请求提交的Baggage信息,用于服务间链路信息传递。
http.request.body

HTTP客户端请求提交的Body数据,可能会比较大,最大只记录512KB,如果超过该大小则忽略。

http.response.headersHTTP客户端请求接收返回的的Header信息,可能会比较大。
http.response.bodyHTTP客户端请求接收返回的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)
}

客户端代码简要说明:

  1. 首先,客户端也是需要通过initTracer方法初始化Jaeger
  2. 在本示例中,我们通过HTTP客户端向服务端发起了3次请求:
    1. /user/insert 用于新增一个用户信息,成功后返回用户的ID。
    2. /user/query 用于查询用户,使用前一个接口返回的用户ID。
    3. /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)
}

服务端代码简要说明:

  1. 首先,客户端也是需要通过initTracer方法初始化Jaeger
  2. 在本示例中,我们使用到了数据库和数据库缓存功能,以便于同时演示ormredis的链路跟踪记录。
  3. 我们在程序启动时通过g.DB().GetCache().SetAdapter(adapter.NewRedis(g.Redis()))设置当前数据库缓存管理的适配器为redis。关于缓存适配器的介绍感兴趣可以参考 缓存管理-缓存适配 章节。
  4. orm的操作中,需要通过Ctx方法将上下文变量传递到组件中,orm组件会自动识别当前上下文中是否包含Tracing链路信息,如果包含则自动启用链路跟踪特性。
  5. orm的操作中,这里使用Cache方法缓存查询结果到redis中,并在删除操作中也使用Cache方法清除redis中的缓存结果。关于orm的缓存管理介绍请参考 ORM链式操作-查询缓存 章节。在orm的内部实现中,也是会将该context.Context上下文变量传递给redis链式操作中,redis组件也有一个Ctx链式操作方法,这样redis组件也会自动识别链路信息并做自动开启。

效果查看

启动服务端:

启动客户端:

Jaeger上查看链路信息:

可以看到,这次请求总共产生了14span,其中客户端有4span,服务端有10span,每一个span代表一个链路节点。不过,我们注意到,这里产生了3errors。我们点击详情查看什么原因呢。

我们看到好像所有的redis操作都报错了,随便点击一个redis的相关span,查看一下详情呢:

原来是redis连接不上报错了,这样的话所有的orm缓存功能都失效了,但是可以看到并没有影响接口逻辑,只是所有的查询都走了数据库。这个报错是因为我本地忘了打开redis server,我赶紧启动一下本地的redis server,再看看效果:

再把上面的客户端运行一下,查看jaeger

现在就没有报错了。

HTTP Client&ServerLogging组件在之前已经介绍过,因此这里我们主要关注ormredis组件的链路跟踪信息。

ORM链路信息

Attributes/Tags

我们随便点开一个ORM链路Span,看看Attributes/Tags信息:

可以看到这里的span.kindinternal,也就是之前介绍过的方法内部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.ExecContextDB.QueryContext,分别代表写操作和读操作。
db.execution.cost

当前SQL语句执行耗时,单位为ms毫秒。

Redis链路信息

Attributes/Tags

Attribute/Tag说明
redis.hostRedis连接地址。
redis.portRedis连接端口。
redis.dbRedis操作db

Events/Process

Event/Log说明
redis.execution.commandRedis执行指令。
redis.execution.argumentsRedis执行指令参数。
redis.execution.cost

Redis执行指令执行耗时,单位为ms毫秒。




Content Menu

  • No labels