参考项目: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中效果图了,简单展示一下: