链路

链路传递

要想使用链路的特性,那么我们一定要会在代码中传递好链路的Context。一旦没有传递好Context,那么链路数据将会丢失。

以下举常用链路传递方式

HTTP服务

HTTP服务中的链路的Context指的是c.Request.Context()。大部分的新手第一次在使用gin框架,会将gin.Context作为链路Context传递下去,而这个用法是错误的用法。以下举例一个在HTTP服务里调用gRPC传递Context方式。

  1. func UserInfo(c *gin.Context) {
  2. reply, err := invoker.UserSvc.UserInfo(c.Request.Context(), &UserInfoReq{
  3. Uid: c.Query("uid"),
  4. })
  5. ...
  6. }

gRPC服务

gRPC服务中的链路的Context就是gRPC函数里的Context,他是可以直接传递的

  1. func (*Order) OrderInfo(ctx context.Context, req *OrderInfoReq) (*OrderInfoReply, error) {
  2. ...
  3. reply, err := invoker.UserSvc.UserInfo(ctx, &UserInfoReq{
  4. Uid: c.Query("uid"),
  5. })
  6. ...
  7. }

Gorm客户端

MySQL客户端中的链路传递Context,如下所示

  1. func (*Order) OrderInfo(ctx context.Context, req *OrderInfoReq) (*OrderInfoReply, error) {
  2. ...
  3. var user User
  4. invoker.Db.WithContext(ctx).Find(&user)
  5. ...
  6. }

Redis客户端

MySQL客户端中的链路传递Context,如下所示

  1. func (*Order) OrderInfo(ctx context.Context, req *OrderInfoReq) (*OrderInfoReply, error) {
  2. ...
  3. invoker.Redis.Get(ctx, "hello")
  4. ...
  5. }

HTTP客户端

resty没有很好的拦截器,不太好封装链路,所以只能自行调用

  1. tracer := etrace.NewTracer(trace.SpanKindClient)
  2. req := httpComp.R()
  3. ctx, span := tracer.Start(context.Background(), "callHTTP()", propagation.HeaderCarrier(req.Header))
  4. defer span.End()
  5. fmt.Println(span.SpanContext().TraceID())
  6. info, err := req.SetContext(ctx).Get("http://127.0.0.1:9007/hello")
  7. if err != nil {
  8. return err
  9. }
  10. fmt.Println(info)

业务记录链路id日志

在ego里面提供了链路id的函数,如下所示

  1. // FieldCtxTid 设置链路id
  2. func FieldCtxTid(ctx context.Context) Field {
  3. return String("tid", etrace.ExtractTraceID(ctx))
  4. }

业务记录日志只需要如下即可

HTTP服务

HTTP服务中的链路的Context指的是c.Request.Context()。大部分的新手第一次在使用gin框架,会将gin.Context作为链路Context传递下去,而这个用法是错误的用法。以下举例一个在HTTP服务里调用gRPC传递Context方式。

  1. func UserInfo(c *gin.Context) {
  2. reply, err := invoker.UserSvc.UserInfo(c.Request.Context(), &UserInfoReq{
  3. Uid: c.Query("uid"),
  4. })
  5. if err != nil {
  6. elog.Error("请求用户服务错误",elog.FieldCtxTid(c.Request.Context()),elog.FieldErr(err))
  7. return
  8. }
  9. ...
  10. }

grpc服务

  1. func (*Order) OrderInfo(ctx context.Context, req *OrderInfoReq) (*OrderInfoReply, error) {
  2. ...
  3. reply, err := invoker.UserSvc.UserInfo(ctx, &UserInfoReq{
  4. Uid: c.Query("uid"),
  5. })
  6. if err != nil {
  7. elog.Error("请求用户服务错误",elog.FieldCtxTid(ctx),elog.FieldErr(err))
  8. return
  9. }
  10. ...
  11. }

链路trace id

框架默认自动开启链路,所有服务端或者客户端access日志会自动加入trace id,用户无需关心。 结合你的前端框架,报错时候,可以弹出链路id。

链路 - 图1

自定义链路

在应用的环境变量里,配置你需要在日志里添加自定义属性的环境变量export EGO_LOG_EXTRA_KEYS=X-Ego-Uid,X-Order-Id,框架在读到这个环境变量里EGO_LOG_EXTRA_KEYS,会根据逗号分割,解析用户配置了多少个自定义的链路属性,后续的Ego组件会根据这个属性,自动将组件日志里追加这些用户自定义属性。

如何在HTTP服务中鉴权后,在链路中添加Uid信息

  1. import "github.com/gotomicro/ego/core/transport"
  2. func checkToken() gin.HandlerFunc {
  3. return func(c *gin.Context) {
  4. accessToken := c.GetHeader("Access-Token")
  5. reply := checkToken(accessToken)
  6. // 使用ego中的transport赋值,会将X-Ego-Uid转为ego中context key的结构体
  7. // 避免ego内置context key和业务方自己的context key冲突
  8. parentContext := transport.WithValue(c.Request.Context(), "X-Ego-Uid", reply.Uid)
  9. c.Request = c.Request.WithContext(parentContext)
  10. c.Next()
  11. }
  12. }

通用Context的赋值和取值

  1. import "github.com/gotomicro/ego/core/transport"
  2. func main() {
  3. // 因为这里是模拟的自定义属性,为了测试,手动写入X-Ego-Uid
  4. // 实际业务应用启动后,该数据会从环境变量里获取
  5. transport.Set([]string{"X-Ego-Uid"})
  6. // 赋值
  7. ctx := transport.WithValue(context.Background(), "X-Ego-Uid", 9527)
  8. // 取值
  9. value := transport.Value(ctx, "X-Ego-Uid")
  10. fmt.Println(value)
  11. }

最终达到效果

链路数据流向原理链接链路 - 图2 (opens new window)

例如MySQLaccess日志中记录了uidtrace id信息。 img.png

以上就是Ego将日志中实现了链路的数据流向。快来体验吧。

体验版本为:

  • github.com/gotomicro/ego@v0.6.2
  • github.com/gotomicro/ego-component/egorm@v0.2.2
  • github.com/gotomicro/ego-component/eredis@v0.2.3