diff --git a/core/trace/opentelemetry/agent.go b/core/trace/opentelemetry/agent.go index 265f0608..2e379f49 100644 --- a/core/trace/opentelemetry/agent.go +++ b/core/trace/opentelemetry/agent.go @@ -18,12 +18,12 @@ var ( enabled syncx.AtomicBool ) -// Enabled returns if prometheus is enabled. +// Enabled returns if opentelemetry is enabled. func Enabled() bool { return enabled.True() } -// StartAgent starts a prometheus agent. +// StartAgent starts a opentelemetry agent. func StartAgent(c Config) { once.Do(func() { if len(c.Endpoint) == 0 { diff --git a/core/trace/opentelemetry/tracer_test.go b/core/trace/opentelemetry/tracer_test.go new file mode 100644 index 00000000..cdfa8add --- /dev/null +++ b/core/trace/opentelemetry/tracer_test.go @@ -0,0 +1,347 @@ +package opentelemetry + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/metadata" +) + +const ( + traceIDStr = "4bf92f3577b34da6a3ce929d0e0e4736" + spanIDStr = "00f067aa0ba902b7" +) + +var ( + traceID = mustTraceIDFromHex(traceIDStr) + spanID = mustSpanIDFromHex(spanIDStr) +) + +func mustTraceIDFromHex(s string) (t trace.TraceID) { + var err error + t, err = trace.TraceIDFromHex(s) + if err != nil { + panic(err) + } + return +} + +func mustSpanIDFromHex(s string) (t trace.SpanID) { + var err error + t, err = trace.SpanIDFromHex(s) + if err != nil { + panic(err) + } + return +} + +func TestExtractValidTraceContext(t *testing.T) { + stateStr := "key1=value1,key2=value2" + state, err := trace.ParseTraceState(stateStr) + require.NoError(t, err) + + tests := []struct { + name string + traceparent string + tracestate string + sc trace.SpanContext + }{ + { + name: "not sampled", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "sampled", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "valid tracestate", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + tracestate: stateStr, + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceState: state, + Remote: true, + }), + }, + { + name: "invalid tracestate perserves traceparent", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + tracestate: "invalid$@#=invalid", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "future version not sampled", + traceparent: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "future version sampled", + traceparent: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "future version sample bit set", + traceparent: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "future version sample bit not set", + traceparent: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-08", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "future version additional data", + traceparent: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00-XYZxsf09", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "B3 format ending in dash", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00-", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "future version B3 format ending in dash", + traceparent: "03-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00-", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + } + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + md := metadata.MD{} + md.Set("traceparent", tt.traceparent) + md.Set("tracestate", tt.tracestate) + _, spanCtx := Extract(ctx, propagator, &md) + assert.Equal(t, tt.sc, spanCtx) + }) + } +} + +func TestExtractInvalidTraceContext(t *testing.T) { + + tests := []struct { + name string + header string + }{ + { + name: "wrong version length", + header: "0000-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "wrong trace ID length", + header: "00-ab00000000000000000000000000000000-cd00000000000000-01", + }, + { + name: "wrong span ID length", + header: "00-ab000000000000000000000000000000-cd0000000000000000-01", + }, + { + name: "wrong trace flag length", + header: "00-ab000000000000000000000000000000-cd00000000000000-0100", + }, + { + name: "bogus version", + header: "qw-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "bogus trace ID", + header: "00-qw000000000000000000000000000000-cd00000000000000-01", + }, + { + name: "bogus span ID", + header: "00-ab000000000000000000000000000000-qw00000000000000-01", + }, + { + name: "bogus trace flag", + header: "00-ab000000000000000000000000000000-cd00000000000000-qw", + }, + { + name: "upper case version", + header: "A0-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "upper case trace ID", + header: "00-AB000000000000000000000000000000-cd00000000000000-01", + }, + { + name: "upper case span ID", + header: "00-ab000000000000000000000000000000-CD00000000000000-01", + }, + { + name: "upper case trace flag", + header: "00-ab000000000000000000000000000000-cd00000000000000-A1", + }, + { + name: "zero trace ID and span ID", + header: "00-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "trace-flag unused bits set", + header: "00-ab000000000000000000000000000000-cd00000000000000-09", + }, + { + name: "missing options", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7", + }, + { + name: "empty options", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-", + }, + } + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + md := metadata.MD{} + md.Set("traceparent", tt.header) + _, spanCtx := Extract(ctx, propagator, &md) + assert.Equal(t, trace.SpanContext{}, spanCtx) + }) + } +} + +func TestInjectValidTraceContext(t *testing.T) { + stateStr := "key1=value1,key2=value2" + state, err := trace.ParseTraceState(stateStr) + require.NoError(t, err) + + tests := []struct { + name string + traceparent string + tracestate string + sc trace.SpanContext + }{ + { + name: "not sampled", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + Remote: true, + }), + }, + { + name: "sampled", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "unsupported trace flag bits dropped", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: 0xff, + Remote: true, + }), + }, + { + name: "with tracestate", + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + tracestate: stateStr, + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceState: state, + Remote: true, + }), + }, + } + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + ctx = trace.ContextWithRemoteSpanContext(ctx, tt.sc) + + want := metadata.MD{} + want.Set("traceparent", tt.traceparent) + if len(tt.tracestate) > 0 { + want.Set("tracestate", tt.tracestate) + } + + md := metadata.MD{} + Inject(ctx, propagator, &md) + assert.Equal(t, want, md) + }) + } +} + +func TestInvalidSpanContextDropped(t *testing.T) { + invalidSC := trace.SpanContext{} + require.False(t, invalidSC.IsValid()) + ctx := trace.ContextWithRemoteSpanContext(context.Background(), invalidSC) + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + + md := metadata.MD{} + Inject(ctx, propagator, &md) + mm := &metadataSupplier{ + metadata: &md, + } + assert.Equal(t, "", mm.Get("traceparent"), "injected invalid SpanContext") +} diff --git a/core/trace/opentelemetry/utils_test.go b/core/trace/opentelemetry/utils_test.go new file mode 100644 index 00000000..a064a329 --- /dev/null +++ b/core/trace/opentelemetry/utils_test.go @@ -0,0 +1,70 @@ +package opentelemetry + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +func TestParseFullMethod(t *testing.T) { + tests := []struct { + fullMethod string + name string + attr []attribute.KeyValue + }{ + { + fullMethod: "/grpc.test.EchoService/Echo", + name: "grpc.test.EchoService/Echo", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("grpc.test.EchoService"), + semconv.RPCMethodKey.String("Echo"), + }, + }, { + fullMethod: "/com.example.ExampleRmiService/exampleMethod", + name: "com.example.ExampleRmiService/exampleMethod", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("com.example.ExampleRmiService"), + semconv.RPCMethodKey.String("exampleMethod"), + }, + }, { + fullMethod: "/MyCalcService.Calculator/Add", + name: "MyCalcService.Calculator/Add", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("MyCalcService.Calculator"), + semconv.RPCMethodKey.String("Add"), + }, + }, { + fullMethod: "/MyServiceReference.ICalculator/Add", + name: "MyServiceReference.ICalculator/Add", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("MyServiceReference.ICalculator"), + semconv.RPCMethodKey.String("Add"), + }, + }, { + fullMethod: "/MyServiceWithNoPackage/theMethod", + name: "MyServiceWithNoPackage/theMethod", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("MyServiceWithNoPackage"), + semconv.RPCMethodKey.String("theMethod"), + }, + }, { + fullMethod: "/pkg.srv", + name: "pkg.srv", + attr: []attribute.KeyValue(nil), + }, { + fullMethod: "/pkg.srv/", + name: "pkg.srv/", + attr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("pkg.srv"), + }, + }, + } + + for _, test := range tests { + n, a := ParseFullMethod(test.fullMethod) + assert.Equal(t, test.name, n) + assert.Equal(t, test.attr, a) + } +}