Adding a way to inject a request ID (#1046)

* Adding a way to inject a request ID

It is very useful to associate a request ID to each incoming request,
this change allows to provide a function to do that via Server Option.
The change comes with a default function which will generate a new
request ID. The request ID is put in the request context along with a
common logger which always logs the request-id

We add gRPC interceptors to the server so it can get the request ID out
of the gRPC metadata and put it in the common logger stored in the
context so as all the log lines using the common logger from the context
will have the request ID logged
This commit is contained in:
Andrea Rosa
2018-06-14 10:40:55 +01:00
committed by GitHub
parent 3790d34eee
commit e637661ea2
133 changed files with 10665 additions and 14 deletions

View File

@@ -0,0 +1,118 @@
# grpc_opentracing
`import "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"`
* [Overview](#pkg-overview)
* [Imported Packages](#pkg-imports)
* [Index](#pkg-index)
## <a name="pkg-overview">Overview</a>
`grpc_opentracing` adds OpenTracing
### OpenTracing Interceptors
These are both client-side and server-side interceptors for OpenTracing. They are a provider-agnostic, with backends
such as Zipkin, or Google Stackdriver Trace.
For a service that sends out requests and receives requests, you *need* to use both, otherwise downstream requests will
not have the appropriate requests propagated.
All server-side spans are tagged with grpc_ctxtags information.
For more information see:
<a href="http://opentracing.io/documentation/">http://opentracing.io/documentation/</a>
<a href="https://github.com/opentracing/specification/blob/master/semantic_conventions.md">https://github.com/opentracing/specification/blob/master/semantic_conventions.md</a>
## <a name="pkg-imports">Imported Packages</a>
- [github.com/grpc-ecosystem/go-grpc-middleware](./../..)
- [github.com/grpc-ecosystem/go-grpc-middleware/tags](./../../tags)
- [github.com/grpc-ecosystem/go-grpc-middleware/util/metautils](./../../util/metautils)
- [github.com/opentracing/opentracing-go](https://godoc.org/github.com/opentracing/opentracing-go)
- [github.com/opentracing/opentracing-go/ext](https://godoc.org/github.com/opentracing/opentracing-go/ext)
- [github.com/opentracing/opentracing-go/log](https://godoc.org/github.com/opentracing/opentracing-go/log)
- [golang.org/x/net/context](https://godoc.org/golang.org/x/net/context)
- [google.golang.org/grpc](https://godoc.org/google.golang.org/grpc)
- [google.golang.org/grpc/grpclog](https://godoc.org/google.golang.org/grpc/grpclog)
- [google.golang.org/grpc/metadata](https://godoc.org/google.golang.org/grpc/metadata)
## <a name="pkg-index">Index</a>
* [Constants](#pkg-constants)
* [func ClientAddContextTags(ctx context.Context, tags opentracing.Tags) context.Context](#ClientAddContextTags)
* [func StreamClientInterceptor(opts ...Option) grpc.StreamClientInterceptor](#StreamClientInterceptor)
* [func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor](#StreamServerInterceptor)
* [func UnaryClientInterceptor(opts ...Option) grpc.UnaryClientInterceptor](#UnaryClientInterceptor)
* [func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor](#UnaryServerInterceptor)
* [type FilterFunc](#FilterFunc)
* [type Option](#Option)
* [func WithFilterFunc(f FilterFunc) Option](#WithFilterFunc)
* [func WithTracer(tracer opentracing.Tracer) Option](#WithTracer)
#### <a name="pkg-files">Package files</a>
[client_interceptors.go](./client_interceptors.go) [doc.go](./doc.go) [id_extract.go](./id_extract.go) [metadata.go](./metadata.go) [options.go](./options.go) [server_interceptors.go](./server_interceptors.go)
## <a name="pkg-constants">Constants</a>
``` go
const (
TagTraceId = "trace.traceid"
TagSpanId = "trace.spanid"
)
```
## <a name="ClientAddContextTags">func</a> [ClientAddContextTags](./client_interceptors.go#L105)
``` go
func ClientAddContextTags(ctx context.Context, tags opentracing.Tags) context.Context
```
ClientAddContextTags returns a context with specified opentracing tags, which
are used by UnaryClientInterceptor/StreamClientInterceptor when creating a
new span.
## <a name="StreamClientInterceptor">func</a> [StreamClientInterceptor](./client_interceptors.go#L35)
``` go
func StreamClientInterceptor(opts ...Option) grpc.StreamClientInterceptor
```
StreamClientInterceptor returns a new streaming client interceptor for OpenTracing.
## <a name="StreamServerInterceptor">func</a> [StreamServerInterceptor](./server_interceptors.go#L37)
``` go
func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor
```
StreamServerInterceptor returns a new streaming server interceptor for OpenTracing.
## <a name="UnaryClientInterceptor">func</a> [UnaryClientInterceptor](./client_interceptors.go#L21)
``` go
func UnaryClientInterceptor(opts ...Option) grpc.UnaryClientInterceptor
```
UnaryClientInterceptor returns a new unary client interceptor for OpenTracing.
## <a name="UnaryServerInterceptor">func</a> [UnaryServerInterceptor](./server_interceptors.go#L23)
``` go
func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor
```
UnaryServerInterceptor returns a new unary server interceptor for OpenTracing.
## <a name="FilterFunc">type</a> [FilterFunc](./options.go#L22)
``` go
type FilterFunc func(ctx context.Context, fullMethodName string) bool
```
FilterFunc allows users to provide a function that filters out certain methods from being traced.
If it returns false, the given request will not be traced.
## <a name="Option">type</a> [Option](./options.go#L41)
``` go
type Option func(*options)
```
### <a name="WithFilterFunc">func</a> [WithFilterFunc](./options.go#L44)
``` go
func WithFilterFunc(f FilterFunc) Option
```
WithFilterFunc customizes the function used for deciding whether a given call is traced or not.
### <a name="WithTracer">func</a> [WithTracer](./options.go#L51)
``` go
func WithTracer(tracer opentracing.Tracer) Option
```
WithTracer sets a custom tracer to be used for this middleware, otherwise the opentracing.GlobalTracer is used.
- - -
Generated by [godoc2ghmd](https://github.com/GandalfUK/godoc2ghmd)

View File

@@ -0,0 +1 @@
DOC.md

View File

@@ -0,0 +1,142 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_opentracing
import (
"io"
"sync"
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/log"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
)
// UnaryClientInterceptor returns a new unary client interceptor for OpenTracing.
func UnaryClientInterceptor(opts ...Option) grpc.UnaryClientInterceptor {
o := evaluateOptions(opts)
return func(parentCtx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
if o.filterOutFunc != nil && !o.filterOutFunc(parentCtx, method) {
return invoker(parentCtx, method, req, reply, cc, opts...)
}
newCtx, clientSpan := newClientSpanFromContext(parentCtx, o.tracer, method)
err := invoker(newCtx, method, req, reply, cc, opts...)
finishClientSpan(clientSpan, err)
return err
}
}
// StreamClientInterceptor returns a new streaming client interceptor for OpenTracing.
func StreamClientInterceptor(opts ...Option) grpc.StreamClientInterceptor {
o := evaluateOptions(opts)
return func(parentCtx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
if o.filterOutFunc != nil && !o.filterOutFunc(parentCtx, method) {
return streamer(parentCtx, desc, cc, method, opts...)
}
newCtx, clientSpan := newClientSpanFromContext(parentCtx, o.tracer, method)
clientStream, err := streamer(newCtx, desc, cc, method, opts...)
if err != nil {
finishClientSpan(clientSpan, err)
return nil, err
}
return &tracedClientStream{ClientStream: clientStream, clientSpan: clientSpan}, nil
}
}
// type serverStreamingRetryingStream is the implementation of grpc.ClientStream that acts as a
// proxy to the underlying call. If any of the RecvMsg() calls fail, it will try to reestablish
// a new ClientStream according to the retry policy.
type tracedClientStream struct {
grpc.ClientStream
mu sync.Mutex
alreadyFinished bool
clientSpan opentracing.Span
}
func (s *tracedClientStream) Header() (metadata.MD, error) {
h, err := s.ClientStream.Header()
if err != nil {
s.finishClientSpan(err)
}
return h, err
}
func (s *tracedClientStream) SendMsg(m interface{}) error {
err := s.ClientStream.SendMsg(m)
if err != nil {
s.finishClientSpan(err)
}
return err
}
func (s *tracedClientStream) CloseSend() error {
err := s.ClientStream.CloseSend()
if err != nil {
s.finishClientSpan(err)
}
return err
}
func (s *tracedClientStream) RecvMsg(m interface{}) error {
err := s.ClientStream.RecvMsg(m)
if err != nil {
s.finishClientSpan(err)
}
return err
}
func (s *tracedClientStream) finishClientSpan(err error) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.alreadyFinished {
finishClientSpan(s.clientSpan, err)
s.alreadyFinished = true
}
}
// 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.Printf("grpc_opentracing: failed serializing trace information: %v", err)
}
ctxWithMetadata := md.ToOutgoing(ctx)
return opentracing.ContextWithSpan(ctxWithMetadata, clientSpan), clientSpan
}
func finishClientSpan(clientSpan opentracing.Span, err error) {
if err != nil && err != io.EOF {
ext.Error.Set(clientSpan, true)
clientSpan.LogFields(log.String("event", "error"), log.String("message", err.Error()))
}
clientSpan.Finish()
}

View File

@@ -0,0 +1,22 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
/*
`grpc_opentracing` adds OpenTracing
OpenTracing Interceptors
These are both client-side and server-side interceptors for OpenTracing. They are a provider-agnostic, with backends
such as Zipkin, or Google Stackdriver Trace.
For a service that sends out requests and receives requests, you *need* to use both, otherwise downstream requests will
not have the appropriate requests propagated.
All server-side spans are tagged with grpc_ctxtags information.
For more information see:
http://opentracing.io/documentation/
https://github.com/opentracing/specification/blob/master/semantic_conventions.md
*/
package grpc_opentracing

View File

@@ -0,0 +1,42 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_opentracing
import (
"strings"
"github.com/grpc-ecosystem/go-grpc-middleware/tags"
"github.com/opentracing/opentracing-go"
"google.golang.org/grpc/grpclog"
)
const (
TagTraceId = "trace.traceid"
TagSpanId = "trace.spanid"
)
// hackyInjectOpentracingIdsToTags writes the given context to the ctxtags.
// 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
func hackyInjectOpentracingIdsToTags(span opentracing.Span, tags grpc_ctxtags.Tags) {
if err := span.Tracer().Inject(span.Context(), opentracing.HTTPHeaders, &hackyTagsCarrier{tags}); err != nil {
grpclog.Printf("grpc_opentracing: failed extracting trace info into ctx %v", err)
}
}
// hackyTagsCarrier is a really hacky way of
type hackyTagsCarrier struct {
grpc_ctxtags.Tags
}
func (t *hackyTagsCarrier) Set(key, val string) {
if strings.Contains(key, "traceid") || strings.Contains(strings.ToLower(key), "traceid") {
t.Tags.Set(TagTraceId, val) // this will most likely be base-16 (hex) encoded
} else if (strings.Contains(key, "spanid") && !strings.Contains(key, "parent")) || (strings.Contains(strings.ToLower(key), "spanid") && !strings.Contains(strings.ToLower(key), "parent")) {
t.Tags.Set(TagSpanId, val) // this will most likely be base-16 (hex) encoded
}
}

View File

@@ -0,0 +1,206 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_opentracing_test
import (
"encoding/json"
"testing"
"fmt"
"net/http"
"io"
"github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/go-grpc-middleware/tags"
grpc_testing "github.com/grpc-ecosystem/go-grpc-middleware/testing"
pb_testproto "github.com/grpc-ecosystem/go-grpc-middleware/testing/testproto"
grpc_opentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
var (
goodPing = &pb_testproto.PingRequest{Value: "something", SleepTimeMs: 9999}
fakeInboundTraceId = 1337
fakeInboundSpanId = 999
)
func tagsToJson(value map[string]interface{}) string {
str, _ := json.Marshal(value)
return string(str)
}
func tagsFromJson(t *testing.T, jstring string) map[string]interface{} {
var msgMapTemplate interface{}
err := json.Unmarshal([]byte(jstring), &msgMapTemplate)
if err != nil {
t.Fatalf("failed unmarshaling tags from response %v", err)
}
return msgMapTemplate.(map[string]interface{})
}
type tracingAssertService struct {
pb_testproto.TestServiceServer
T *testing.T
}
func (s *tracingAssertService) Ping(ctx context.Context, ping *pb_testproto.PingRequest) (*pb_testproto.PingResponse, error) {
assert.NotNil(s.T, opentracing.SpanFromContext(ctx), "handlers must have the spancontext in their context, otherwise propagation will fail")
tags := grpc_ctxtags.Extract(ctx)
assert.True(s.T, tags.Has(grpc_opentracing.TagTraceId), "tags must contain traceid")
assert.True(s.T, tags.Has(grpc_opentracing.TagSpanId), "tags must contain traceid")
return s.TestServiceServer.Ping(ctx, ping)
}
func (s *tracingAssertService) PingError(ctx context.Context, ping *pb_testproto.PingRequest) (*pb_testproto.Empty, 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 *pb_testproto.PingRequest, stream pb_testproto.TestService_PingListServer) error {
assert.NotNil(s.T, opentracing.SpanFromContext(stream.Context()), "handlers must have the spancontext in their context, otherwise propagation will fail")
tags := grpc_ctxtags.Extract(stream.Context())
assert.True(s.T, tags.Has(grpc_opentracing.TagTraceId), "tags must contain traceid")
assert.True(s.T, tags.Has(grpc_opentracing.TagSpanId), "tags must contain traceid")
return s.TestServiceServer.PingList(ping, stream)
}
func (s *tracingAssertService) PingEmpty(ctx context.Context, empty *pb_testproto.Empty) (*pb_testproto.PingResponse, error) {
assert.NotNil(s.T, opentracing.SpanFromContext(ctx), "handlers must have the spancontext in their context, otherwise propagation will fail")
return s.TestServiceServer.PingEmpty(ctx, empty)
}
func TestTaggingSuite(t *testing.T) {
mockTracer := mocktracer.New()
opts := []grpc_opentracing.Option{
grpc_opentracing.WithTracer(mockTracer),
}
s := &OpentracingSuite{
mockTracer: mockTracer,
InterceptorTestSuite: &grpc_testing.InterceptorTestSuite{
TestService: &tracingAssertService{TestServiceServer: &grpc_testing.TestPingService{T: t}, T: t},
ClientOpts: []grpc.DialOption{
grpc.WithUnaryInterceptor(grpc_opentracing.UnaryClientInterceptor(opts...)),
grpc.WithStreamInterceptor(grpc_opentracing.StreamClientInterceptor(opts...)),
},
ServerOpts: []grpc.ServerOption{
grpc_middleware.WithStreamServerChain(
grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
grpc_opentracing.StreamServerInterceptor(opts...)),
grpc_middleware.WithUnaryServerChain(
grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
grpc_opentracing.UnaryServerInterceptor(opts...)),
},
},
}
suite.Run(t, s)
}
type OpentracingSuite struct {
*grpc_testing.InterceptorTestSuite
mockTracer *mocktracer.MockTracer
}
func (s *OpentracingSuite) SetupTest() {
s.mockTracer.Reset()
}
func (s *OpentracingSuite) createContextFromFakeHttpRequestParent(ctx context.Context) context.Context {
hdr := http.Header{}
hdr.Set("mockpfx-ids-traceid", fmt.Sprint(fakeInboundTraceId))
hdr.Set("mockpfx-ids-spanid", fmt.Sprint(fakeInboundSpanId))
hdr.Set("mockpfx-ids-sampled", fmt.Sprint(true))
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(), serverSpan.Tag("grpc.request.value"), "something", "grpc_ctxtags must be propagated, in this case ones from request fields")
return clientSpan, serverSpan
}
func (s *OpentracingSuite) TestPing_PropagatesTraces() {
ctx := s.createContextFromFakeHttpRequestParent(s.SimpleCtx())
_, err := s.Client.Ping(ctx, goodPing)
require.NoError(s.T(), err, "there must be not be an on a successful call")
s.assertTracesCreated("/mwitkow.testproto.TestService/Ping")
}
func (s *OpentracingSuite) TestPing_ClientContextTags() {
const name = "opentracing.custom"
ctx := grpc_opentracing.ClientAddContextTags(
s.createContextFromFakeHttpRequestParent(s.SimpleCtx()),
opentracing.Tags{name: ""},
)
_, 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 == "/mwitkow.testproto.TestService/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())
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("/mwitkow.testproto.TestService/PingList")
}
func (s *OpentracingSuite) TestPingError_PropagatesTraces() {
ctx := s.createContextFromFakeHttpRequestParent(s.SimpleCtx())
erroringPing := &pb_testproto.PingRequest{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("/mwitkow.testproto.TestService/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")
}

View File

@@ -0,0 +1,56 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_opentracing
import (
"encoding/base64"
"strings"
"fmt"
"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 decodedKey, decodedVal, err := metadata.DecodeKeyValue(k, v); err == nil {
if err = callback(decodedKey, decodedVal); err != nil {
return err
}
} else {
return fmt.Errorf("failed decoding opentracing from gRPC metadata: %v", 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) {
val := base64.StdEncoding.EncodeToString([]byte(v))
v = string(val)
}
return k, v
}

View File

@@ -0,0 +1,55 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_opentracing
import (
"context"
"github.com/opentracing/opentracing-go"
)
var (
defaultOptions = &options{
filterOutFunc: nil,
tracer: nil,
}
)
// FilterFunc allows users to provide a function that filters out certain methods from being traced.
//
// If it returns false, the given request will not be traced.
type FilterFunc func(ctx context.Context, fullMethodName string) bool
type options struct {
filterOutFunc FilterFunc
tracer opentracing.Tracer
}
func evaluateOptions(opts []Option) *options {
optCopy := &options{}
*optCopy = *defaultOptions
for _, o := range opts {
o(optCopy)
}
if optCopy.tracer == nil {
optCopy.tracer = opentracing.GlobalTracer()
}
return optCopy
}
type Option func(*options)
// WithFilterFunc customizes the function used for deciding whether a given call is traced or not.
func WithFilterFunc(f FilterFunc) Option {
return func(o *options) {
o.filterOutFunc = f
}
}
// 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
}
}

View File

@@ -0,0 +1,86 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_opentracing
import (
"github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/go-grpc-middleware/tags"
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/log"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
)
var (
grpcTag = opentracing.Tag{Key: string(ext.Component), Value: "gRPC"}
)
// UnaryServerInterceptor returns a new unary server interceptor for OpenTracing.
func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor {
o := evaluateOptions(opts)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if o.filterOutFunc != nil && !o.filterOutFunc(ctx, info.FullMethod) {
return handler(ctx, req)
}
newCtx, serverSpan := newServerSpanFromInbound(ctx, o.tracer, info.FullMethod)
resp, err := handler(newCtx, req)
finishServerSpan(ctx, serverSpan, err)
return resp, err
}
}
// StreamServerInterceptor returns a new streaming server interceptor for OpenTracing.
func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor {
o := evaluateOptions(opts)
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if o.filterOutFunc != nil && !o.filterOutFunc(stream.Context(), info.FullMethod) {
return handler(srv, stream)
}
newCtx, serverSpan := newServerSpanFromInbound(stream.Context(), o.tracer, info.FullMethod)
wrappedStream := grpc_middleware.WrapServerStream(stream)
wrappedStream.WrappedContext = newCtx
err := handler(srv, wrappedStream)
finishServerSpan(newCtx, serverSpan, err)
return err
}
}
func newServerSpanFromInbound(ctx context.Context, tracer opentracing.Tracer, 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.Printf("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,
)
hackyInjectOpentracingIdsToTags(serverSpan, grpc_ctxtags.Extract(ctx))
return opentracing.ContextWithSpan(ctx, serverSpan), serverSpan
}
func finishServerSpan(ctx context.Context, serverSpan opentracing.Span, err error) {
// Log context information
tags := grpc_ctxtags.Extract(ctx)
for k, v := range tags.Values() {
// Don't tag errors, log them instead.
if vErr, ok := v.(error); ok {
serverSpan.LogKV(k, vErr.Error())
} else {
serverSpan.SetTag(k, v)
}
}
if err != nil {
ext.Error.Set(serverSpan, true)
serverSpan.LogFields(log.String("event", "error"), log.String("message", err.Error()))
}
serverSpan.Finish()
}