diff --git a/providers/opentracing/client.go b/providers/opentracing/client.go new file mode 100644 index 000000000..be403bb9d --- /dev/null +++ b/providers/opentracing/client.go @@ -0,0 +1,49 @@ +package opentracing + +import ( + "context" + + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + "google.golang.org/grpc/grpclog" + + "github.com/grpc-ecosystem/go-grpc-middleware/v2/util/metautils" +) + +var ( + grpcTag = opentracing.Tag{Key: string(ext.Component), Value: "gRPC"} +) + +// ClientAddContextTags returns a context with specified opentracing tags, which +// are used by UnaryClientInterceptor/StreamClientInterceptor when creating a +// new span. +func ClientAddContextTags(ctx context.Context, tags opentracing.Tags) context.Context { + return context.WithValue(ctx, clientSpanTagKey{}, tags) +} + +type clientSpanTagKey struct{} + +func newClientSpanFromContext(ctx context.Context, tracer opentracing.Tracer, fullMethodName string) (context.Context, opentracing.Span) { + var parentSpanCtx opentracing.SpanContext + if parent := opentracing.SpanFromContext(ctx); parent != nil { + parentSpanCtx = parent.Context() + } + opts := []opentracing.StartSpanOption{ + opentracing.ChildOf(parentSpanCtx), + ext.SpanKindRPCClient, + grpcTag, + } + if tagx := ctx.Value(clientSpanTagKey{}); tagx != nil { + if opt, ok := tagx.(opentracing.StartSpanOption); ok { + opts = append(opts, opt) + } + } + clientSpan := tracer.StartSpan(fullMethodName, opts...) + // Make sure we add this to the metadata of the call, so it gets propagated: + md := metautils.ExtractOutgoing(ctx).Clone() + if err := tracer.Inject(clientSpan.Context(), opentracing.HTTPHeaders, metadataTextMap(md)); err != nil { + grpclog.Infof("grpc_opentracing: failed serializing trace information: %v", err) + } + ctxWithMetadata := md.ToOutgoing(ctx) + return opentracing.ContextWithSpan(ctxWithMetadata, clientSpan), clientSpan +} diff --git a/providers/opentracing/examples_test.go b/providers/opentracing/examples_test.go new file mode 100644 index 000000000..84030662d --- /dev/null +++ b/providers/opentracing/examples_test.go @@ -0,0 +1,33 @@ +package opentracing_test + +import ( + "testing" + + "google.golang.org/grpc" + + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tags" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tracing" + grpcopentracing "github.com/grpc-ecosystem/go-grpc-middleware/v2/providers/opentracing" +) + +func Example() { + _ = grpc.NewServer( + grpc.ChainUnaryInterceptor( + tags.UnaryServerInterceptor(tags.WithFieldExtractor(tags.CodeGenRequestFieldExtractor)), + tracing.UnaryServerInterceptor(grpcopentracing.InterceptorTracer()), + ), + grpc.ChainStreamInterceptor( + tags.StreamServerInterceptor(tags.WithFieldExtractor(tags.CodeGenRequestFieldExtractor)), + tracing.StreamServerInterceptor(grpcopentracing.InterceptorTracer()), + ), + ) + + _, _ = grpc.Dial("", + grpc.WithUnaryInterceptor(tracing.UnaryClientInterceptor(grpcopentracing.InterceptorTracer())), + grpc.WithStreamInterceptor(tracing.StreamClientInterceptor(grpcopentracing.InterceptorTracer())), + ) +} + +func TestExamplesBuildable(t *testing.T) { + Example() +} diff --git a/providers/opentracing/go.mod b/providers/opentracing/go.mod new file mode 100644 index 000000000..1512fef44 --- /dev/null +++ b/providers/opentracing/go.mod @@ -0,0 +1,13 @@ +module github.com/grpc-ecosystem/go-grpc-middleware/providers/opentracing/v2 + +go 1.14 + +replace github.com/grpc-ecosystem/go-grpc-middleware/v2 => ../.. + +require ( + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.2 + github.com/opentracing/opentracing-go v1.1.0 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.7.0 + google.golang.org/grpc v1.30.1 +) diff --git a/providers/opentracing/go.sum b/providers/opentracing/go.sum new file mode 100644 index 000000000..dde461bcc --- /dev/null +++ b/providers/opentracing/go.sum @@ -0,0 +1,99 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200624020401-64a14ca9d1ad h1:uAwc13+y0Y8QZLTYhLCu6lHhnG99ecQU5FYTj8zxAng= +google.golang.org/genproto v0.0.0-20200624020401-64a14ca9d1ad/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.30.1 h1:oJTcovwKSu7V3TaBKd0/AXOuJVHjTdGTutbMHIOgVEQ= +google.golang.org/grpc v1.30.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc/examples v0.0.0-20200723182653-9106c3fff523/go.mod h1:5j1uub0jRGhRiSghIlrThmBUgcgLXOVJQ/l1getT4uo= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/providers/opentracing/id_extract.go b/providers/opentracing/id_extract.go new file mode 100644 index 000000000..8216c6641 --- /dev/null +++ b/providers/opentracing/id_extract.go @@ -0,0 +1,80 @@ +package opentracing + +import ( + "strings" + + "github.com/opentracing/opentracing-go" + "google.golang.org/grpc/grpclog" + + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tags" +) + +const ( + TagTraceId = "trace.traceid" + TagSpanId = "trace.spanid" + TagSampled = "trace.sampled" + jaegerNotSampledFlag = "0" +) + +// injectOpentracingIdsToTags writes trace data to tags. +// This is done in an incredibly hacky way, because the public-facing interface of opentracing doesn't give access to +// the TraceId and SpanId of the SpanContext. Only the Tracer's Inject/Extract methods know what these are. +// Most tracers have them encoded as keys with 'traceid' and 'spanid': +// https://github.com/openzipkin/zipkin-go-opentracing/blob/594640b9ef7e5c994e8d9499359d693c032d738c/propagation_ot.go#L29 +// https://github.com/opentracing/basictracer-go/blob/1b32af207119a14b1b231d451df3ed04a72efebf/propagation_ot.go#L26 +// Jaeger from Uber use one-key schema with next format '{trace-id}:{span-id}:{parent-span-id}:{flags}' +// https://www.jaegertracing.io/docs/client-libraries/#trace-span-identity +// Datadog uses keys ending with 'trace-id' and 'parent-id' (for span) by default: +// https://github.com/DataDog/dd-trace-go/blob/v1/ddtrace/tracer/textmap.go#L77 +func injectOpentracingIdsToTags(traceHeaderName string, span opentracing.Span, tags tags.Tags) { + if err := span.Tracer().Inject(span.Context(), opentracing.HTTPHeaders, + &tagsCarrier{Tags: tags, traceHeaderName: traceHeaderName}); err != nil { + grpclog.Infof("grpc_opentracing: failed extracting trace info into ctx %v", err) + } +} + +// tagsCarrier is a really hacky way of +type tagsCarrier struct { + tags.Tags + traceHeaderName string +} + +func (t *tagsCarrier) Set(key, val string) { + key = strings.ToLower(key) + if strings.Contains(key, "traceid") { + t.Tags.Set(TagTraceId, val) // this will most likely be base-16 (hex) encoded + } + + if strings.Contains(key, "spanid") && !strings.Contains(strings.ToLower(key), "parent") { + t.Tags.Set(TagSpanId, val) // this will most likely be base-16 (hex) encoded + } + + if strings.Contains(key, "sampled") { + switch val { + case "true", "false": + t.Tags.Set(TagSampled, val) + } + } + + if key == t.traceHeaderName { + parts := strings.Split(val, ":") + if len(parts) == 4 { + t.Tags.Set(TagTraceId, parts[0]) + t.Tags.Set(TagSpanId, parts[1]) + + if parts[3] != jaegerNotSampledFlag { + t.Tags.Set(TagSampled, "true") + } else { + t.Tags.Set(TagSampled, "false") + } + } + } + + if strings.HasSuffix(key, "trace-id") { + t.Tags.Set(TagTraceId, val) + } + + if strings.HasSuffix(key, "parent-id") { + t.Tags.Set(TagSpanId, val) + } +} diff --git a/providers/opentracing/interceptors_test.go b/providers/opentracing/interceptors_test.go new file mode 100644 index 000000000..8a712b867 --- /dev/null +++ b/providers/opentracing/interceptors_test.go @@ -0,0 +1,294 @@ +// Copyright 2017 Michal Witkowski. All Rights Reserved. +// See LICENSE for licensing terms. + +package opentracing_test + +import ( + "context" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "testing" + + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/mocktracer" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + + grpcopentracing "github.com/grpc-ecosystem/go-grpc-middleware/providers/opentracing/v2" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tags" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tracing" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/testing/testpb" +) + +var ( + fakeInboundTraceId = 1337 + fakeInboundSpanId = 999 + traceHeaderName = "uber-trace-id" +) + +type tracingAssertService struct { + testpb.TestServiceServer + T *testing.T +} + +func (s *tracingAssertService) Ping(ctx context.Context, ping *testpb.PingRequest) (*testpb.PingResponse, error) { + assert.NotNil(s.T, opentracing.SpanFromContext(ctx), "handlers must have the spancontext in their context, otherwise propagation will fail") + tags := tags.Extract(ctx) + assert.True(s.T, tags.Has(grpcopentracing.TagTraceId), "tags must contain traceid") + assert.True(s.T, tags.Has(grpcopentracing.TagSpanId), "tags must contain spanid") + assert.True(s.T, tags.Has(grpcopentracing.TagSampled), "tags must contain sampled") + assert.Equal(s.T, tags.Values()[grpcopentracing.TagSampled], "true", "sampled must be set to true") + return s.TestServiceServer.Ping(ctx, ping) +} + +func (s *tracingAssertService) PingError(ctx context.Context, ping *testpb.PingErrorRequest) (*testpb.PingErrorResponse, error) { + assert.NotNil(s.T, opentracing.SpanFromContext(ctx), "handlers must have the spancontext in their context, otherwise propagation will fail") + return s.TestServiceServer.PingError(ctx, ping) +} + +func (s *tracingAssertService) PingList(ping *testpb.PingListRequest, stream testpb.TestService_PingListServer) error { + assert.NotNil(s.T, opentracing.SpanFromContext(stream.Context()), "handlers must have the spancontext in their context, otherwise propagation will fail") + tags := tags.Extract(stream.Context()) + assert.True(s.T, tags.Has(grpcopentracing.TagTraceId), "tags must contain traceid") + assert.True(s.T, tags.Has(grpcopentracing.TagSpanId), "tags must contain spanid") + assert.True(s.T, tags.Has(grpcopentracing.TagSampled), "tags must contain sampled") + assert.Equal(s.T, tags.Values()[grpcopentracing.TagSampled], "true", "sampled must be set to true") + return s.TestServiceServer.PingList(ping, stream) +} + +func (s *tracingAssertService) PingEmpty(ctx context.Context, empty *testpb.PingEmptyRequest) (*testpb.PingEmptyResponse, error) { + assert.NotNil(s.T, opentracing.SpanFromContext(ctx), "handlers must have the spancontext in their context, otherwise propagation will fail") + tags := tags.Extract(ctx) + assert.True(s.T, tags.Has(grpcopentracing.TagTraceId), "tags must contain traceid") + assert.True(s.T, tags.Has(grpcopentracing.TagSpanId), "tags must contain spanid") + assert.True(s.T, tags.Has(grpcopentracing.TagSampled), "tags must contain sampled") + assert.Equal(s.T, tags.Values()[grpcopentracing.TagSampled], "false", "sampled must be set to false") + return s.TestServiceServer.PingEmpty(ctx, empty) +} + +func TestTaggingSuite(t *testing.T) { + mockTracer := mocktracer.New() + opts := []grpcopentracing.Option{ + grpcopentracing.WithTracer(mockTracer), + grpcopentracing.WithTraceHeaderName(traceHeaderName), + } + s := &OpentracingSuite{ + mockTracer: mockTracer, + InterceptorTestSuite: makeInterceptorTestSuite(t, opts), + } + suite.Run(t, s) +} + +func TestTaggingSuiteJaeger(t *testing.T) { + mockTracer := mocktracer.New() + mockTracer.RegisterInjector(opentracing.HTTPHeaders, jaegerFormatInjector{}) + mockTracer.RegisterExtractor(opentracing.HTTPHeaders, jaegerFormatExtractor{}) + opts := []grpcopentracing.Option{ + grpcopentracing.WithTracer(mockTracer), + } + s := &OpentracingSuite{ + mockTracer: mockTracer, + InterceptorTestSuite: makeInterceptorTestSuite(t, opts), + } + suite.Run(t, s) +} + +func makeInterceptorTestSuite(t *testing.T, opts []grpcopentracing.Option) *testpb.InterceptorTestSuite { + return &testpb.InterceptorTestSuite{ + TestService: &tracingAssertService{TestServiceServer: &testpb.TestPingService{T: t}, T: t}, + ClientOpts: []grpc.DialOption{ + grpc.WithUnaryInterceptor(tracing.UnaryClientInterceptor(grpcopentracing.InterceptorTracer(opts...))), + grpc.WithStreamInterceptor(tracing.StreamClientInterceptor(grpcopentracing.InterceptorTracer(opts...))), + }, + ServerOpts: []grpc.ServerOption{ + grpc.ChainUnaryInterceptor( + tags.UnaryServerInterceptor(tags.WithFieldExtractor(tags.CodeGenRequestFieldExtractor)), + tracing.UnaryServerInterceptor(grpcopentracing.InterceptorTracer(opts...)), + ), + grpc.ChainStreamInterceptor( + tags.StreamServerInterceptor(tags.WithFieldExtractor(tags.CodeGenRequestFieldExtractor)), + tracing.StreamServerInterceptor(grpcopentracing.InterceptorTracer(opts...)), + ), + }, + } +} + +type OpentracingSuite struct { + *testpb.InterceptorTestSuite + mockTracer *mocktracer.MockTracer +} + +func (s *OpentracingSuite) SetupTest() { + s.mockTracer.Reset() +} + +func (s *OpentracingSuite) createContextFromFakeHttpRequestParent(ctx context.Context, sampled bool) context.Context { + jFlag := 0 + if sampled { + jFlag = 1 + } + + hdr := http.Header{} + hdr.Set(traceHeaderName, fmt.Sprintf("%d:%d:%d:%d", fakeInboundTraceId, fakeInboundSpanId, fakeInboundSpanId, jFlag)) + hdr.Set("mockpfx-ids-traceid", fmt.Sprint(fakeInboundTraceId)) + hdr.Set("mockpfx-ids-spanid", fmt.Sprint(fakeInboundSpanId)) + hdr.Set("mockpfx-ids-sampled", fmt.Sprint(sampled)) + + parentSpanContext, err := s.mockTracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(hdr)) + require.NoError(s.T(), err, "parsing a fake HTTP request headers shouldn't fail, ever") + fakeSpan := s.mockTracer.StartSpan( + "/fake/parent/http/request", + // this is magical, it attaches the new span to the parent parentSpanContext, and creates an unparented one if empty. + opentracing.ChildOf(parentSpanContext), + ) + fakeSpan.Finish() + return opentracing.ContextWithSpan(ctx, fakeSpan) +} + +func (s *OpentracingSuite) assertTracesCreated(methodName string) (clientSpan *mocktracer.MockSpan, serverSpan *mocktracer.MockSpan) { + spans := s.mockTracer.FinishedSpans() + for _, span := range spans { + s.T().Logf("span: %v, tags: %v", span, span.Tags()) + } + require.Len(s.T(), spans, 3, "should record 3 spans: one fake inbound, one client, one server") + traceIdAssert := fmt.Sprintf("traceId=%d", fakeInboundTraceId) + for _, span := range spans { + assert.Contains(s.T(), span.String(), traceIdAssert, "not part of the fake parent trace: %v", span) + if span.OperationName == methodName { + kind := fmt.Sprintf("%v", span.Tag("span.kind")) + if kind == "client" { + clientSpan = span + } else if kind == "server" { + serverSpan = span + } + assert.EqualValues(s.T(), span.Tag("component"), "gRPC", "span must be tagged with gRPC component") + } + } + require.NotNil(s.T(), clientSpan, "client span must be there") + require.NotNil(s.T(), serverSpan, "server span must be there") + assert.EqualValues(s.T(), "something", serverSpan.Tag("grpc.request.value"), "tags must be propagated, in this case ones from request fields") + return clientSpan, serverSpan +} + +func (s *OpentracingSuite) TestPing_PropagatesTraces() { + ctx := s.createContextFromFakeHttpRequestParent(s.SimpleCtx(), true) + goodPing := testpb.PingRequest{Value: "something", SleepTimeMs: 9999} + _, err := s.Client.Ping(ctx, &goodPing) + require.NoError(s.T(), err, "there must be not be an on a successful call") + s.assertTracesCreated("/" + testpb.TestServiceFullName + "/Ping") +} + +func (s *OpentracingSuite) TestPing_ClientContextTags() { + const name = "opentracing.custom" + ctx := grpcopentracing.ClientAddContextTags( + s.createContextFromFakeHttpRequestParent(s.SimpleCtx(), true), + opentracing.Tags{name: ""}, + ) + + goodPing := testpb.PingRequest{Value: "something", SleepTimeMs: 9999} + _, err := s.Client.Ping(ctx, &goodPing) + require.NoError(s.T(), err, "there must be not be an on a successful call") + + for _, span := range s.mockTracer.FinishedSpans() { + if span.OperationName == "/"+testpb.TestServiceFullName+"/Ping" { + kind := fmt.Sprintf("%v", span.Tag("span.kind")) + if kind == "client" { + assert.Contains(s.T(), span.Tags(), name, "custom opentracing.Tags must be included in context") + } + } + } +} + +func (s *OpentracingSuite) TestPingList_PropagatesTraces() { + ctx := s.createContextFromFakeHttpRequestParent(s.SimpleCtx(), true) + goodPing := testpb.PingListRequest{Value: "something", SleepTimeMs: 9999} + stream, err := s.Client.PingList(ctx, &goodPing) + require.NoError(s.T(), err, "should not fail on establishing the stream") + for { + _, err := stream.Recv() + if err == io.EOF { + break + } + require.NoError(s.T(), err, "reading stream should not fail") + } + s.assertTracesCreated("/" + testpb.TestServiceFullName + "/PingList") +} + +func (s *OpentracingSuite) TestPingError_PropagatesTraces() { + ctx := s.createContextFromFakeHttpRequestParent(s.SimpleCtx(), true) + erroringPing := testpb.PingErrorRequest{Value: "something", ErrorCodeReturned: uint32(codes.OutOfRange)} + _, err := s.Client.PingError(ctx, &erroringPing) + require.Error(s.T(), err, "there must be an error returned here") + clientSpan, serverSpan := s.assertTracesCreated("/" + testpb.TestServiceFullName + "/PingError") + assert.Equal(s.T(), true, clientSpan.Tag("error"), "client span needs to be marked as an error") + assert.Equal(s.T(), true, serverSpan.Tag("error"), "server span needs to be marked as an error") +} + +func (s *OpentracingSuite) TestPingEmpty_NotSampleTraces() { + ctx := s.createContextFromFakeHttpRequestParent(s.SimpleCtx(), false) + _, err := s.Client.PingEmpty(ctx, &testpb.PingEmptyRequest{}) + require.NoError(s.T(), err, "there must be not be an on a successful call") +} + +type jaegerFormatInjector struct{} + +func (jaegerFormatInjector) Inject(ctx mocktracer.MockSpanContext, carrier interface{}) error { + w := carrier.(opentracing.TextMapWriter) + flags := 0 + if ctx.Sampled { + flags = 1 + } + w.Set(traceHeaderName, fmt.Sprintf("%d:%d::%d", ctx.TraceID, ctx.SpanID, flags)) + + return nil +} + +type jaegerFormatExtractor struct{} + +func (jaegerFormatExtractor) Extract(carrier interface{}) (mocktracer.MockSpanContext, error) { + rval := mocktracer.MockSpanContext{Sampled: true} + reader, ok := carrier.(opentracing.TextMapReader) + if !ok { + return rval, opentracing.ErrInvalidCarrier + } + err := reader.ForeachKey(func(key, val string) error { + lowerKey := strings.ToLower(key) + switch { + case lowerKey == traceHeaderName: + parts := strings.Split(val, ":") + if len(parts) != 4 { + return errors.New("invalid trace id format") + } + traceId, err := strconv.Atoi(parts[0]) + if err != nil { + return err + } + rval.TraceID = traceId + spanId, err := strconv.Atoi(parts[1]) + if err != nil { + return err + } + rval.SpanID = spanId + flags, err := strconv.Atoi(parts[3]) + if err != nil { + return err + } + rval.Sampled = flags%2 == 1 + } + return nil + }) + if rval.TraceID == 0 || rval.SpanID == 0 { + return rval, opentracing.ErrSpanContextNotFound + } + if err != nil { + return rval, err + } + return rval, nil +} diff --git a/providers/opentracing/metadata.go b/providers/opentracing/metadata.go new file mode 100644 index 000000000..f42c3c370 --- /dev/null +++ b/providers/opentracing/metadata.go @@ -0,0 +1,49 @@ +// Copyright 2017 Michal Witkowski. All Rights Reserved. +// See LICENSE for licensing terms. + +package opentracing + +import ( + "encoding/base64" + "strings" + + "google.golang.org/grpc/metadata" +) + +const ( + binHdrSuffix = "-bin" +) + +// metadataTextMap extends a metadata.MD to be an opentracing textmap +type metadataTextMap metadata.MD + +// Set is a opentracing.TextMapReader interface that extracts values. +func (m metadataTextMap) Set(key, val string) { + // gRPC allows for complex binary values to be written. + encodedKey, encodedVal := encodeKeyValue(key, val) + // The metadata object is a multimap, and previous values may exist, but for opentracing headers, we do not append + // we just override. + m[encodedKey] = []string{encodedVal} +} + +// ForeachKey is a opentracing.TextMapReader interface that extracts values. +func (m metadataTextMap) ForeachKey(callback func(key, val string) error) error { + for k, vv := range m { + for _, v := range vv { + if err := callback(k, v); err != nil { + return err + } + } + } + return nil +} + +// encodeKeyValue encodes key and value qualified for transmission via gRPC. +// note: copy pasted from private values of grpc.metadata +func encodeKeyValue(k, v string) (string, string) { + k = strings.ToLower(k) + if strings.HasSuffix(k, binHdrSuffix) { + v = base64.StdEncoding.EncodeToString([]byte(v)) + } + return k, v +} diff --git a/providers/opentracing/options.go b/providers/opentracing/options.go new file mode 100644 index 000000000..648bf2b9b --- /dev/null +++ b/providers/opentracing/options.go @@ -0,0 +1,46 @@ +package opentracing + +import "github.com/opentracing/opentracing-go" + +var ( + defaultOptions = &options{ + tracer: nil, + } +) + +type options struct { + tracer opentracing.Tracer + traceHeaderName string +} + +func evaluateOptions(opts []Option) *options { + optCopy := &options{} + *optCopy = *defaultOptions + for _, o := range opts { + o(optCopy) + } + if optCopy.tracer == nil { + optCopy.tracer = opentracing.GlobalTracer() + } + if optCopy.traceHeaderName == "" { + optCopy.traceHeaderName = "uber-trace-id" + } + return optCopy +} + +type Option func(*options) + +// WithTraceHeaderName customizes the trace header name where trace metadata passed with requests. +// Default one is `uber-trace-id` +func WithTraceHeaderName(name string) Option { + return func(o *options) { + o.traceHeaderName = name + } +} + +// WithTracer sets a custom tracer to be used for this middleware, otherwise the opentracing.GlobalTracer is used. +func WithTracer(tracer opentracing.Tracer) Option { + return func(o *options) { + o.tracer = tracer + } +} diff --git a/providers/opentracing/server.go b/providers/opentracing/server.go new file mode 100644 index 000000000..09e4cadf1 --- /dev/null +++ b/providers/opentracing/server.go @@ -0,0 +1,36 @@ +package opentracing + +import ( + "context" + + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + "google.golang.org/grpc/grpclog" + + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tags" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/util/metautils" +) + +func newServerSpanFromInbound(ctx context.Context, tracer opentracing.Tracer, traceHeaderName, fullMethodName string) (context.Context, opentracing.Span) { + md := metautils.ExtractIncoming(ctx) + parentSpanContext, err := tracer.Extract(opentracing.HTTPHeaders, metadataTextMap(md)) + if err != nil && err != opentracing.ErrSpanContextNotFound { + grpclog.Infof("grpc_opentracing: failed parsing trace information: %v", err) + } + + serverSpan := tracer.StartSpan( + fullMethodName, + // this is magical, it attaches the new span to the parent parentSpanContext, and creates an unparented one if empty. + ext.RPCServerOption(parentSpanContext), + grpcTag, + ) + + // Log context information. + t := tags.Extract(ctx) + for k, v := range t.Values() { + serverSpan.SetTag(k, v) + } + + injectOpentracingIdsToTags(traceHeaderName, serverSpan, tags.Extract(ctx)) + return opentracing.ContextWithSpan(ctx, serverSpan), serverSpan +} diff --git a/providers/opentracing/span.go b/providers/opentracing/span.go new file mode 100644 index 000000000..ebceeb4c7 --- /dev/null +++ b/providers/opentracing/span.go @@ -0,0 +1,99 @@ +package opentracing + +import ( + "context" + + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + "github.com/opentracing/opentracing-go/log" + "google.golang.org/grpc/codes" + + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tags" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tracing" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tracing/kv" +) + +type span struct { + span opentracing.Span + ctx context.Context + + initial bool +} + +// Compatibility check. +var _ tracing.Span = &span{} + +func newSpan(rawSpan opentracing.Span, ctx context.Context) *span { + return &span{ + span: rawSpan, + ctx: ctx, + initial: true, + } +} + +func (s *span) End() { + // Middleware tags only record once + if s.initial { + s.initial = false + t := tags.Extract(s.ctx) + for k, v := range t.Values() { + s.span.SetTag(k, v) + } + } + + s.span.Finish() +} + +func (s *span) SetStatus(code codes.Code, msg string) { + if code != codes.OK { + ext.Error.Set(s.span, true) + s.span.LogFields(log.String("event", "error"), log.String("message", msg)) + } +} + +func (s *span) AddEvent(name string, attrs ...kv.KeyValue) { + fields := make([]log.Field, 0, len(attrs) +1) + + fields = append(fields, log.String("event", name)) + + for _, attr := range attrs { + switch attr.Value.Type() { + case kv.BOOL: + fields = append(fields, log.Bool(string(attr.Key), attr.Value.AsBool())) + case kv.INT64: + fields = append(fields, log.Int64(string(attr.Key), attr.Value.AsInt64())) + case kv.FLOAT64: + fields = append(fields, log.Float64(string(attr.Key), attr.Value.AsFloat64())) + case kv.STRING: + fields = append(fields, log.String(string(attr.Key), attr.Value.AsString())) + default: + continue + } + } + + s.span.LogFields(fields...) +} + + +func (s *span) SetAttributes(attrs ...kv.KeyValue) { + for _, attr := range attrs { + var v interface{} + switch attr.Value.Type() { + case kv.BOOL: + v = attr.Value.AsBool() + case kv.INT64: + v = attr.Value.AsInt64() + case kv.FLOAT64: + v = attr.Value.AsFloat64() + case kv.STRING: + v = attr.Value.AsString() + default: + continue + } + + if v != nil { + s.span.SetTag(string(attr.Key), v) + } + } +} + diff --git a/providers/opentracing/tracer.go b/providers/opentracing/tracer.go new file mode 100644 index 000000000..78fde2544 --- /dev/null +++ b/providers/opentracing/tracer.go @@ -0,0 +1,36 @@ +package opentracing + +import ( + "context" + + "github.com/opentracing/opentracing-go" + + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tracing" +) + +type tracer struct { + tracer opentracing.Tracer + // This is only used for server. + traceHeaderName string +} + +// Compatibility check. +var _ tracing.Tracer = &tracer{} + +// InterceptorTracer converts OpenTracing tracer to Tracer adapter. +func InterceptorTracer(opts ...Option) *tracer { + o := evaluateOptions(opts) + + return &tracer{tracer: o.tracer, traceHeaderName: o.traceHeaderName} +} + +func (t *tracer) Start(ctx context.Context, spanName string, kind tracing.SpanKind) (context.Context, tracing.Span) { + var span opentracing.Span + switch kind { + case tracing.SpanKindClient: + ctx, span = newClientSpanFromContext(ctx, t.tracer, spanName) + case tracing.SpanKindServer: + ctx, span = newServerSpanFromInbound(ctx, t.tracer, t.traceHeaderName, spanName) + } + return ctx, newSpan(span, ctx) +}