package tracing import ( "context" "fmt" "io" "net" "testing" "github.com/opentracing/opentracing-go" "github.com/stretchr/testify/require" "github.com/uber/jaeger-client-go" "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper" grpctracing "gitlab.com/gitlab-org/labkit/tracing/grpc" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/interop/grpc_testing" ) func TestExtractSpanContextFromEnv(t *testing.T) { _, cleanup := testhelper.StubTracingReporter(t) defer cleanup() injectedSpan := opentracing.StartSpan("test", opentracing.Tag{Key: "do-not-carry", Value: "value"}) injectedSpan.SetBaggageItem("hi", "hello") jaegerInjectedSpan := injectedSpan.(*jaeger.Span) jaegerInjectedSpanContext := jaegerInjectedSpan.SpanContext() createSpanContext := func() []string { env := envMap{} err := opentracing.GlobalTracer().Inject(injectedSpan.Context(), opentracing.TextMap, env) require.NoError(t, err) return env.toSlice() } tests := []struct { desc string envs []string expectedContext opentracing.SpanContext expectedError string }{ { desc: "empty environment map", envs: []string{}, expectedError: "opentracing: SpanContext not found in Extract carrier", }, { desc: "irrelevant environment map", envs: []string{"SOME_THING=A", "SOMETHING_ELSE=B"}, expectedError: "opentracing: SpanContext not found in Extract carrier", }, { desc: "environment variable includes span context", envs: createSpanContext(), }, } for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { spanContext, err := ExtractSpanContextFromEnv(tc.envs) if tc.expectedError != "" { require.Equal(t, tc.expectedError, err.Error()) } else { require.NoError(t, err) require.NotNil(t, spanContext) span := opentracing.StartSpan("test", opentracing.ChildOf(spanContext)) jaegerSpan := span.(*jaeger.Span) jaegerSpanContext := jaegerSpan.SpanContext() require.Equal(t, jaegerInjectedSpanContext.TraceID(), jaegerSpanContext.TraceID()) require.Equal(t, jaegerInjectedSpanContext.SpanID(), jaegerSpanContext.ParentID()) require.Equal(t, opentracing.Tags{}, jaegerSpan.Tags()) require.Equal(t, "hello", jaegerSpan.BaggageItem("hi")) } }) } } func TestUnaryPassthroughInterceptor(t *testing.T) { reporter, cleanup := testhelper.StubTracingReporter(t) defer cleanup() tests := []struct { desc string setup func(*testing.T) (jaeger.SpanID, opentracing.SpanContext, func()) expectedSpans []string }{ { desc: "empty span context", setup: func(t *testing.T) (jaeger.SpanID, opentracing.SpanContext, func()) { return 0, nil, func() {} }, expectedSpans: []string{ "/grpc.testing.TestService/UnaryCall", }, }, { desc: "span context with a simple span", setup: func(t *testing.T) (jaeger.SpanID, opentracing.SpanContext, func()) { span := opentracing.GlobalTracer().StartSpan("root") return span.(*jaeger.Span).SpanContext().SpanID(), span.Context(), span.Finish }, expectedSpans: []string{ "/grpc.testing.TestService/UnaryCall", "root", }, }, { desc: "span context with a trace chain", setup: func(t *testing.T) (jaeger.SpanID, opentracing.SpanContext, func()) { root := opentracing.GlobalTracer().StartSpan("root") child := opentracing.GlobalTracer().StartSpan("child", opentracing.ChildOf(root.Context())) grandChild := opentracing.GlobalTracer().StartSpan("grandChild", opentracing.ChildOf(child.Context())) return grandChild.(*jaeger.Span).SpanContext().SpanID(), grandChild.Context(), func() { grandChild.Finish() child.Finish() root.Finish() } }, expectedSpans: []string{ "/grpc.testing.TestService/UnaryCall", "grandChild", "child", "root", }, }, } for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { reporter.Reset() var parentID jaeger.SpanID service := &testSvc{ unaryCall: func(ctx context.Context, request *grpc_testing.SimpleRequest) (*grpc_testing.SimpleResponse, error) { if span := opentracing.SpanFromContext(ctx); span != nil { parentID = span.(*jaeger.Span).SpanContext().ParentID() } return &grpc_testing.SimpleResponse{}, nil }, } expectedParentID, spanContext, finishFunc := tc.setup(t) client := startFakeGitalyServer(t, service, spanContext) _, err := client.UnaryCall(testhelper.Context(t), &grpc_testing.SimpleRequest{}) require.NoError(t, err) finishFunc() require.Equal(t, expectedParentID, parentID) require.Equal(t, tc.expectedSpans, reportedSpans(t, reporter)) }) } } func TestStreamPassthroughInterceptor(t *testing.T) { reporter, cleanup := testhelper.StubTracingReporter(t) defer cleanup() tests := []struct { desc string setup func(*testing.T) (jaeger.SpanID, opentracing.SpanContext, func()) expectedSpans []string }{ { desc: "empty span context", setup: func(t *testing.T) (jaeger.SpanID, opentracing.SpanContext, func()) { return 0, nil, func() {} }, expectedSpans: []string{ "/grpc.testing.TestService/FullDuplexCall", }, }, { desc: "span context with a simple span", setup: func(t *testing.T) (jaeger.SpanID, opentracing.SpanContext, func()) { span := opentracing.GlobalTracer().StartSpan("root") return span.(*jaeger.Span).SpanContext().SpanID(), span.Context(), span.Finish }, expectedSpans: []string{ "/grpc.testing.TestService/FullDuplexCall", "root", }, }, { desc: "span context with a trace chain", setup: func(t *testing.T) (jaeger.SpanID, opentracing.SpanContext, func()) { root := opentracing.GlobalTracer().StartSpan("root") child := opentracing.GlobalTracer().StartSpan("child", opentracing.ChildOf(root.Context())) grandChild := opentracing.GlobalTracer().StartSpan("grandChild", opentracing.ChildOf(child.Context())) return grandChild.(*jaeger.Span).SpanContext().SpanID(), grandChild.Context(), func() { grandChild.Finish() child.Finish() root.Finish() } }, expectedSpans: []string{ "/grpc.testing.TestService/FullDuplexCall", "grandChild", "child", "root", }, }, } for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { reporter.Reset() var parentID jaeger.SpanID service := &testSvc{ fullDuplexCall: func(stream grpc_testing.TestService_FullDuplexCallServer) error { _, err := stream.Recv() require.NoError(t, err) if span := opentracing.SpanFromContext(stream.Context()); span != nil { parentID = span.(*jaeger.Span).SpanContext().ParentID() } require.NoError(t, stream.Send(&grpc_testing.StreamingOutputCallResponse{})) return nil }, } expectedParentID, spanContext, finishFunc := tc.setup(t) client := startFakeGitalyServer(t, service, spanContext) stream, err := client.FullDuplexCall(testhelper.Context(t)) require.NoError(t, err) require.NoError(t, stream.Send(&grpc_testing.StreamingOutputCallRequest{})) resp, err := stream.Recv() require.NoError(t, err) testhelper.ProtoEqual(t, &grpc_testing.StreamingOutputCallResponse{}, resp) resp, err = stream.Recv() require.Equal(t, io.EOF, err) require.Nil(t, resp) finishFunc() require.Equal(t, expectedParentID, parentID) require.Equal(t, tc.expectedSpans, reportedSpans(t, reporter)) }) } } type testSvc struct { grpc_testing.UnimplementedTestServiceServer unaryCall func(context.Context, *grpc_testing.SimpleRequest) (*grpc_testing.SimpleResponse, error) fullDuplexCall func(stream grpc_testing.TestService_FullDuplexCallServer) error } func (ts *testSvc) UnaryCall(ctx context.Context, r *grpc_testing.SimpleRequest) (*grpc_testing.SimpleResponse, error) { return ts.unaryCall(ctx, r) } func (ts *testSvc) FullDuplexCall(stream grpc_testing.TestService_FullDuplexCallServer) error { return ts.fullDuplexCall(stream) } func startFakeGitalyServer(t *testing.T, svc *testSvc, spanContext opentracing.SpanContext) grpc_testing.TestServiceClient { t.Helper() listener, err := net.Listen("tcp", "localhost:0") require.NoError(t, err) srv := grpc.NewServer( grpc.StreamInterceptor(grpctracing.StreamServerTracingInterceptor()), grpc.UnaryInterceptor(grpctracing.UnaryServerTracingInterceptor()), ) grpc_testing.RegisterTestServiceServer(srv, svc) go testhelper.MustServe(t, srv, listener) t.Cleanup(srv.Stop) conn, err := grpc.Dial( listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(UnaryPassthroughInterceptor(spanContext)), grpc.WithStreamInterceptor(StreamPassthroughInterceptor(spanContext)), ) require.NoError(t, err) t.Cleanup(func() { testhelper.MustClose(t, conn) }) return grpc_testing.NewTestServiceClient(conn) } // envMap implements opentracing.TextMapReader and opentracing.TextMapWriter. It is used to create // testing environment maps used in below tests type envMap map[string]string func (e envMap) Set(key, val string) { e[key] = val } func (e envMap) ForeachKey(handler func(key string, val string) error) error { for key, val := range e { if err := handler(key, val); err != nil { return err } } return nil } func (e envMap) toSlice() []string { var envSlice []string for key, value := range e { envSlice = append(envSlice, fmt.Sprintf("%s=%s", key, value)) } return envSlice }