opentracing: jaeger在grpc中的简单实现

参考项目:https://github.com/grpc-ecosystem/grpc-opentracing

之前用函数调用实现了简单jaeger-demo(https://blog.csdn.net/liyunlong41/article/details/87932953),函数之间利用context传递span信息。现在开始在grpc请求中实现简单的grpc-jaeger-demo,span的传递渠道也是利用context。

但是也稍有不同,我们之前是用StartSpanFromContext来模拟从context中启动一个子span,但是StartSpanFromContext或者SpanFromContext只能在同一个服务内使用,grpc中client的context和server的context并不是同一个context,无法使用这两个函数。(参考https://github.com/grpc/grpc-go/issues/130

如果想通过grpc的context传递span的信息,就需要使用grcp的metadata来传递(一个简单的例子:https://medium.com/@harlow/grpc-context-for-client-server-metadata-91cec8729424)。

同时grpc-client端提供了Inject函数,可以将span的context信息注入到carrier中,再将carrier写入到metadata中,即可完成span信息的传递。

grpc提供了拦截器,我们可以在dial函数里设置拦截器,这样每次请求都会经过拦截器,我们不需要在每个接口中去编写重复的代码。

client端示例代码:


func main() {
	//init jaeger
	tracer, closer, err := initJaeger("client", jaegerAgentHost)
	if err != nil {
		log.Fatal(err)
	}
	defer closer.Close()
	//dial
	conn, err := grpc.Dial(addr, grpc.WithInsecure(), clientDialOption(tracer))
	if err != nil {
		log.Fatalf("dial fail, %+v\n", err)
	}
	//发送请求
	req := &delayqueue.PingRequest{Msg:"ping~"}
	client := delayqueue.NewDelayQueueClient(conn)
	r, err := client.Ping(context.Background(), req)

	fmt.Println(r, err)
}

func clientDialOption(tracer opentracing.Tracer) grpc.DialOption {
	return grpc.WithUnaryInterceptor(jaegerGrpcClientInterceptor)
}
type TextMapWriter struct {
	metadata.MD
}
//重写TextMapWriter的Set方法,我们需要将carrier中的数据写入到metadata中,这样grpc才会携带。
func (t TextMapWriter) Set(key, val string) {
	//key = strings.ToLower(key)
	t.MD[key] = append(t.MD[key], val)
}

func jaegerGrpcClientInterceptor (ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) (err error) {
	var parentContext opentracing.SpanContext
	//先从context中获取原始的span
	parentSpan := opentracing.SpanFromContext(ctx)
	if parentSpan != nil {
		parentContext = parentSpan.Context()
	}
	tracer := opentracing.GlobalTracer()
	span := tracer.StartSpan(method, opentracing.ChildOf(parentContext))
	defer span.Finish()
	//从context中获取metadata。md.(type) == map[string][]string
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		md = metadata.New(nil)
	} else {
		//如果对metadata进行修改,那么需要用拷贝的副本进行修改。(FromIncomingContext的注释)
		md = md.Copy()
	}
	//定义一个carrier,下面的Inject注入数据需要用到。carrier.(type) == map[string]string
	//carrier := opentracing.TextMapCarrier{}
	carrier := TextMapWriter{md}
	//将span的context信息注入到carrier中
	e := tracer.Inject(span.Context(), opentracing.TextMap, carrier)
	if e != nil {
		fmt.Println("tracer Inject err,", e)
	}
	//创建一个新的context,把metadata附带上
	ctx = metadata.NewOutgoingContext(ctx, md)

	return invoker(ctx, method, req, reply, cc, opts...)
}

func initJaeger(service string, jaegerAgentHost string) (tracer opentracing.Tracer, closer io.Closer, err error) {
	cfg := &config.Configuration{
		Sampler: &config.SamplerConfig{
			Type:  "const",
			Param: 1,
		},
		Reporter: &config.ReporterConfig{
			LogSpans: true,
			LocalAgentHostPort:jaegerAgentHost,
		},
	}
	tracer, closer, err = cfg.New(service, config.Logger(jaeger.StdLogger))
	opentracing.SetGlobalTracer(tracer)
	return tracer, closer, err
}

在grpc-server端,我们使用Extract函数将carrier从metadata中提取出来,这样client端与server端就能建立span信息的关联。我们在server端同样只是修改拦截器,在grpc.NewServer时将我们的拦截器传进去。

server端代码:

func serverOption(tracer opentracing.Tracer) grpc.ServerOption {
	return grpc.UnaryInterceptor(jaegerGrpcServerInterceptor)
}

type TextMapReader struct {
	metadata.MD
}
//读取metadata中的span信息
func (t TextMapReader) ForeachKey(handler func(key, val string) error) error { //不能是指针
	for key, val := range t.MD {
		for _, v := range val {
			if err := handler(key, v); err != nil {
				return err
			}
		}
	}
	return nil
}

func jaegerGrpcServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
	//从context中获取metadata。md.(type) == map[string][]string
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		md = metadata.New(nil)
	} else {
		//如果对metadata进行修改,那么需要用拷贝的副本进行修改。(FromIncomingContext的注释)
		md = md.Copy()
	}
	carrier := TextMapReader{md}
	tracer := opentracing.GlobalTracer()
	spanContext, e := tracer.Extract(opentracing.TextMap, carrier)
	if e != nil {
		fmt.Println("Extract err:", e)
	}

	span := tracer.StartSpan(info.FullMethod, opentracing.ChildOf(spanContext))
	defer span.Finish()
	ctx = opentracing.ContextWithSpan(ctx, span)

	return handler(ctx, req)
}

我们可以在span finish之前利用SetTag添加一些额外的信息,例如request和reply,以及error信息,但是这些信息是不会在client和server中传递的,我们可以在UI中每个span中显示出他们的tag。

WebUI:

下面就是webUI中效果图了,简单展示一下: