參考項目: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中效果圖了,簡單展示一下: