testutils/testtracing/propagation.go (229 lines of code) (raw):

// Copyright (c) 2015 Uber Technologies, Inc. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package testtracing import ( "fmt" "testing" "time" "github.com/uber/tchannel-go" "github.com/uber/tchannel-go/testutils" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/mocktracer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/uber/jaeger-client-go" "golang.org/x/net/context" ) const ( // BaggageKey is used for testing baggage propagation BaggageKey = "luggage" // BaggageValue is used for testing baggage propagation BaggageValue = "suitcase" ) // TracingRequest tests tracing capabilities in a given server. type TracingRequest struct { // ForwardCount tells the server how many times to forward this request to itself recursively ForwardCount int } // TracingResponse captures the trace info observed in the server and its downstream calls type TracingResponse struct { TraceID uint64 SpanID uint64 ParentID uint64 TracingEnabled bool Child *TracingResponse Luggage string } // ObserveSpan extracts an OpenTracing span from the context and populates the response. func (r *TracingResponse) ObserveSpan(ctx context.Context) *TracingResponse { if span := opentracing.SpanFromContext(ctx); span != nil { if mockSpan, ok := span.(*mocktracer.MockSpan); ok { sc := mockSpan.Context().(mocktracer.MockSpanContext) r.TraceID = uint64(sc.TraceID) r.SpanID = uint64(sc.SpanID) r.ParentID = uint64(mockSpan.ParentID) r.TracingEnabled = sc.Sampled } else if span := tchannel.CurrentSpan(ctx); span != nil { r.TraceID = span.TraceID() r.SpanID = span.SpanID() r.ParentID = span.ParentID() r.TracingEnabled = span.Flags()&1 == 1 } r.Luggage = span.BaggageItem(BaggageKey) } return r } // TraceHandler is a base class for testing tracing propagation type TraceHandler struct { Ch *tchannel.Channel } // HandleCall is used by handlers from different encodings as the main business logic. // It respects the ForwardCount input parameter to make downstream calls, and returns // a result containing the observed tracing span and the downstream results. func (h *TraceHandler) HandleCall( ctx context.Context, req *TracingRequest, downstream TracingCall, ) (*TracingResponse, error) { var childResp *TracingResponse if req.ForwardCount > 0 { downstreamReq := &TracingRequest{ForwardCount: req.ForwardCount - 1} if resp, err := downstream(ctx, downstreamReq); err == nil { childResp = resp } else { return nil, err } } resp := &TracingResponse{Child: childResp} resp.ObserveSpan(ctx) return resp, nil } // TracerType is a convenient enum to indicate which type of tracer is being used in the test. // It is a string because it's printed as part of the test description in the logs. type TracerType string const ( // Noop is for the default no-op tracer from OpenTracing Noop TracerType = "NOOP" // Mock tracer, baggage-capable, non-Zipkin trace IDs Mock TracerType = "MOCK" // Jaeger is Uber's tracer, baggage-capable, Zipkin-style trace IDs Jaeger TracerType = "JAEGER" ) // TracingCall is used in a few other structs here type TracingCall func(ctx context.Context, req *TracingRequest) (*TracingResponse, error) // EncodingInfo describes the encoding used with tracing propagation test type EncodingInfo struct { Format tchannel.Format HeadersSupported bool } // PropagationTestSuite is a collection of test cases for a certain encoding type PropagationTestSuite struct { Encoding EncodingInfo Register func(t *testing.T, ch *tchannel.Channel) TracingCall TestCases map[TracerType][]PropagationTestCase } // PropagationTestCase describes a single propagation test case and expected results type PropagationTestCase struct { ForwardCount int TracingDisabled bool ExpectedBaggage string ExpectedSpanCount int } type tracerChoice struct { tracerType TracerType tracer opentracing.Tracer spansRecorded func() int resetSpans func() isFake bool zipkinCompatible bool } // Run executes the test cases in the test suite against 3 different tracer implementations func (s *PropagationTestSuite) Run(t *testing.T) { tests := []struct { name string run func(t *testing.T) }{ {"Noop_Tracer", s.runWithNoopTracer}, {"Mock_Tracer", s.runWithMockTracer}, {"Jaeger_Tracer", s.runWithJaegerTracer}, } for _, test := range tests { t.Logf("Running with %s", test.name) test.run(t) } } func (s *PropagationTestSuite) runWithNoopTracer(t *testing.T) { s.runWithTracer(t, tracerChoice{ tracer: nil, // will cause opentracing.GlobalTracer() to be used tracerType: Noop, spansRecorded: func() int { return 0 }, resetSpans: func() {}, isFake: true, }) } func (s *PropagationTestSuite) runWithMockTracer(t *testing.T) { mockTracer := mocktracer.New() s.runWithTracer(t, tracerChoice{ tracerType: Mock, tracer: mockTracer, spansRecorded: func() int { return len(MockTracerSampledSpans(mockTracer)) }, resetSpans: func() { mockTracer.Reset() }, }) } func (s *PropagationTestSuite) runWithJaegerTracer(t *testing.T) { jaegerReporter := jaeger.NewInMemoryReporter() jaegerTracer, jaegerCloser := jaeger.NewTracer(testutils.DefaultServerName, jaeger.NewConstSampler(true), jaegerReporter) // To enable logging, use composite reporter: // jaeger.NewCompositeReporter(jaegerReporter, jaeger.NewLoggingReporter(jaeger.StdLogger))) defer jaegerCloser.Close() s.runWithTracer(t, tracerChoice{ tracerType: Jaeger, tracer: jaegerTracer, spansRecorded: func() int { return len(jaegerReporter.GetSpans()) }, resetSpans: func() { jaegerReporter.Reset() }, zipkinCompatible: true, }) } func (s *PropagationTestSuite) runWithTracer(t *testing.T, tracer tracerChoice) { testCases, ok := s.TestCases[tracer.tracerType] if !ok { t.Logf("No test cases for encoding=%s and tracer=%s", s.Encoding.Format, tracer.tracerType) return } opts := &testutils.ChannelOpts{ ChannelOptions: tchannel.ChannelOptions{Tracer: tracer.tracer}, DisableRelay: true, } ch := testutils.NewServer(t, opts) defer ch.Close() ch.Peers().Add(ch.PeerInfo().HostPort) call := s.Register(t, ch) for _, tt := range testCases { s.runTestCase(t, tracer, ch, tt, call) } } func (s *PropagationTestSuite) runTestCase( t *testing.T, tracer tracerChoice, ch *tchannel.Channel, test PropagationTestCase, call TracingCall, ) { descr := fmt.Sprintf("test %+v with tracer %+v", test, tracer) ch.Logger().Debugf("Starting tracing test %s", descr) tracer.resetSpans() span := ch.Tracer().StartSpan("client") span.SetBaggageItem(BaggageKey, BaggageValue) ctx := opentracing.ContextWithSpan(context.Background(), span) ctxBuilder := tchannel.NewContextBuilder(5 * time.Second).SetParentContext(ctx) if test.TracingDisabled { ctxBuilder.DisableTracing() } ctx, cancel := ctxBuilder.Build() defer cancel() req := &TracingRequest{ForwardCount: test.ForwardCount} ch.Logger().Infof("Sending tracing request %+v", req) response, err := call(ctx, req) require.NoError(t, err) ch.Logger().Infof("Received tracing response %+v", response) // Spans are finished in inbound.doneSending() or outbound.doneReading(), // which are called on different go-routines and may execute *after* the // response has been received by the client. Give them a chance to run. for i := 0; i < 1000; i++ { if spanCount := tracer.spansRecorded(); spanCount == test.ExpectedSpanCount { break } time.Sleep(testutils.Timeout(time.Millisecond)) } spanCount := tracer.spansRecorded() ch.Logger().Debugf("end span count: %d", spanCount) // finish span after taking count of recorded spans, as we're only interested // in the count of spans created by RPC calls. span.Finish() root := new(TracingResponse).ObserveSpan(ctx) if !tracer.isFake { assert.Equal(t, uint64(0), root.ParentID) assert.NotEqual(t, uint64(0), root.TraceID) } assert.Equal(t, test.ExpectedSpanCount, spanCount, "Wrong span count; %s", descr) for r, cnt := response, 0; r != nil || cnt <= test.ForwardCount; r, cnt = r.Child, cnt+1 { require.NotNil(t, r, "Expecting response for forward=%d; %s", cnt, descr) if !tracer.isFake { if tracer.zipkinCompatible || s.Encoding.HeadersSupported { assert.Equal(t, root.TraceID, r.TraceID, "traceID should be the same; %s", descr) } assert.Equal(t, test.ExpectedBaggage, r.Luggage, "baggage should propagate; %s", descr) } } ch.Logger().Debugf("Finished tracing test %s", descr) } // MockTracerSampledSpans is a helper function that returns only sampled spans from MockTracer func MockTracerSampledSpans(tracer *mocktracer.MockTracer) []*mocktracer.MockSpan { var spans []*mocktracer.MockSpan for _, span := range tracer.FinishedSpans() { if span.Context().(mocktracer.MockSpanContext).Sampled { spans = append(spans, span) } } return spans }