最近在尝试用 LGTM 来实现 Go 微服务的可观测性,就顺便整理一下文档。
Tempo 会分为 4 篇文章:
- Tempo 的架构
- 官网测试实操跑通
- gin 框架发送 trace 数据到 tempo
- go-zero 微服务框架发送数据到 tempo
本文就是写一下如何在 go-zero 微服务框架里面配置 trace 及 trace 的 handler 在哪里
代码地址:lgtm/example/go-zero-lgtm at main · zxmfke/lgtm (github.com)
这个代码里面包含一个 order 的 api 服务和一个 user 的 rpc 服务
go-zero 的服务接入 trace,是比较简单的,只需要在配置项中,配置 Telemetry 里面的参数就可以了。
Telemetry 配置
在 /etc 下有配置文件,其中配置:
...
Telemetry:
  Name: "order"
  Endpoint: "127.0.0.1:4318"
  Batcher: "otlphttp"
...
源码(core/trace/config.go)中可以配置的参数:
type Config struct {
	Name     string  `json:",optional"`
	Endpoint string  `json:",optional"`
	Sampler  float64 `json:",default=1.0"`
	Batcher  string  `json:",default=jaeger,options=jaeger|zipkin|otlpgrpc|otlphttp|file"`
	// OtlpHeaders represents the headers for OTLP gRPC or HTTP transport.
	// For example:
	//  uptrace-dsn: 'http://project2_secret_token@localhost:14317/2'
	OtlpHeaders map[string]string `json:",optional"`
	// OtlpHttpPath represents the path for OTLP HTTP transport.
	// For example
	// /v1/traces
	OtlpHttpPath string `json:",optional"`
	// Disabled indicates whether StartAgent starts the agent.
	Disabled bool `json:",optional"`
}
主要就是配置:
- Name:服务名称
- Endpoint:Trace provider 的 IP 地址
- Batcher:用哪一种 provider 类型
注意,Disabled 如果没有设置为 true,就默认表示发送 trace 数据
Grafana Tempo Trace Data
请求接口后,trace 数据会发送到 Grafana 的 Tempo 里面,如下图:

可以看到分为三段:
- order-api 收到请求的时候
- order-api 请求 user-rpc 的时候
- user-rpc 收到请求的时候
接下来我们可以追踪一下源码看看是在哪里发送的 trace 数据
追踪代码
初始化
order
order 是 api 服务,Telemetry 初始化的逻辑:
-  order.go // L24 server := rest.MustNewServer(c.RestConf)
-  go-zero/rest/server.go // L35 func MustNewServer(c RestConf, opts ...RunOption) *Server { server, err := NewServer(c, opts...) ... } // L46 func NewServer(c RestConf, opts ...RunOption) (*Server, error) { if err := c.SetUp(); err != nil { return nil, err } ... return server, nil }
-  go-zero/core/sevice/serviceconf.go // L44 func (sc ServiceConf) SetUp() error { ... trace.StartAgent(sc.Telemetry) ... return nil }
-  go-zero/core/trace/agent.go // L39 func StartAgent(c Config) { ... // if error happens, let later calls run. if err := startAgent(c); err != nil { return } ... }
user
-  user.go // L29 s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { userclient.RegisterUserServer(grpcServer, srv) if c.Mode == service.DevMode || c.Mode == service.TestMode { reflection.Register(grpcServer) } }) defer s.Stop()
-  go-zero/zrpc/server.go // L23 func MustNewServer(c RpcServerConf, register internal.RegisterFn) *RpcServer { server, err := NewServer(c, register) logx.Must(err) return server } // L30 func NewServer(c RpcServerConf, register internal.RegisterFn) (*RpcServer, error) { ... if err = c.SetUp(); err != nil { return nil, err } return rpcServer, nil }
-  Setup() 跟 order 的一样了 
Order api 的 trace handler
之前看过源码,go-zero 的 api 和 rpc 是分开两个目录,所以 api 的代码一定在 go-zero/rest 下面。 要在请求的收到的时候发送一个 trace 数据,只有可能以中间件的形式加入,因此可以找到 rest 下的 handler 文件夹,里面就有一个 tracinghandler.go。
源码位置:go-zero/rest/handler/tracinghandler.go
这个就类似上一篇的 gin server 加 trace middleware 一样。
func TracingHandler(serviceName, path string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		propagator := otel.GetTextMapPropagator()
		tracer := otel.GetTracerProvider().Tracer(trace.TraceName)
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
			spanName := path
			if len(spanName) == 0 {
				spanName = r.URL.Path
			}
			spanCtx, span := tracer.Start(
				ctx,
				spanName,
				oteltrace.WithSpanKind(oteltrace.SpanKindServer),
				oteltrace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(
					serviceName, spanName, r)...),
			)
			defer span.End()
			// convenient for tracking error messages
			propagator.Inject(spanCtx, propagation.HeaderCarrier(w.Header()))
			next.ServeHTTP(w, r.WithContext(spanCtx))
		})
	}
}
User rpc 的 trace handler
同理,user 是 rpc 服务,所以就到 zrpc 下面找,按照惯性,找到 go-zero/zrpc/internal/serverinterceptors ,这个时候就可以看到 tracinginterceptor.go
源码位置:go-zero/zrpc/internal/serverinterceptors/tracinginterceptor.go
func UnaryTracingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler) (interface{}, error) {
    ctx, span := startSpan(ctx, info.FullMethod)
    defer span.End()
    ztrace.MessageReceived.Event(ctx, 1, req)
    resp, err := handler(ctx, req)
    if err != nil {
       s, ok := status.FromError(err)
       if ok {
          span.SetStatus(codes.Error, s.Message())
          span.SetAttributes(ztrace.StatusCodeAttr(s.Code()))
          ztrace.MessageSent.Event(ctx, 1, s.Proto())
       } else {
          span.SetStatus(codes.Error, err.Error())
       }
       return nil, err
    }
    span.SetAttributes(ztrace.StatusCodeAttr(gcodes.OK))
    ztrace.MessageSent.Event(ctx, 1, resp)
    return resp, nil
}
找到这个,但是没有发现 order-api 调用 user 的 handler。就很奇怪,又返回 rest 里面找,不过还是没找到,后面才发现 zrpc/internal 下面有一个 clientinterceptors ,原来在这边。
func UnaryTracingInterceptor(ctx context.Context, method string, req, reply interface{},
	cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	ctx, span := startSpan(ctx, method, cc.Target())
	defer span.End()
	ztrace.MessageSent.Event(ctx, 1, req)
	ztrace.MessageReceived.Event(ctx, 1, reply)
	if err := invoker(ctx, method, req, reply, cc, opts...); err != nil {
		s, ok := status.FromError(err)
		if ok {
			span.SetStatus(codes.Error, s.Message())
			span.SetAttributes(ztrace.StatusCodeAttr(s.Code()))
		} else {
			span.SetStatus(codes.Error, err.Error())
		}
		return err
	}
	span.SetAttributes(ztrace.StatusCodeAttr(gcodes.OK))
	return nil
}
ror(err)
 if ok {
 span.SetStatus(codes.Error, s.Message())
 span.SetAttributes(ztrace.StatusCodeAttr(s.Code()))
 } else {
 span.SetStatus(codes.Error, err.Error())
 }
 return err
 }
span.SetAttributes(ztrace.StatusCodeAttr(gcodes.OK))
return nil
}



















