// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 package opentracing import ( "context" "fmt" "net/http" "reflect" "strconv" "strings" "testing" ot "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" otlog "github.com/opentracing/opentracing-go/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" ) type testOnlyTextMapReader struct{} func newTestOnlyTextMapReader() *testOnlyTextMapReader { return &testOnlyTextMapReader{} } func (*testOnlyTextMapReader) ForeachKey(handler func(key, val string) error) error { _ = handler("key1", "val1") _ = handler("key2", "val2") return nil } type testOnlyTextMapWriter struct { m map[string]string } func newTestOnlyTextMapWriter() *testOnlyTextMapWriter { return &testOnlyTextMapWriter{m: map[string]string{}} } func (t *testOnlyTextMapWriter) Set(key, val string) { t.m[key] = val } type testTextMapReaderAndWriter struct { *testOnlyTextMapReader *testOnlyTextMapWriter } func newTestTextMapReaderAndWriter() *testTextMapReaderAndWriter { return &testTextMapReaderAndWriter{ testOnlyTextMapReader: newTestOnlyTextMapReader(), testOnlyTextMapWriter: newTestOnlyTextMapWriter(), } } func TestTextMapWrapper_New(t *testing.T) { _, err := newTextMapWrapperForExtract(newTestOnlyTextMapReader()) assert.NoError(t, err) _, err = newTextMapWrapperForExtract(newTestOnlyTextMapWriter()) assert.ErrorIs(t, err, ot.ErrInvalidCarrier) _, err = newTextMapWrapperForExtract(newTestTextMapReaderAndWriter()) assert.NoError(t, err) _, err = newTextMapWrapperForInject(newTestOnlyTextMapWriter()) assert.NoError(t, err) _, err = newTextMapWrapperForInject(newTestOnlyTextMapReader()) assert.ErrorIs(t, err, ot.ErrInvalidCarrier) _, err = newTextMapWrapperForInject(newTestTextMapReaderAndWriter()) assert.NoError(t, err) } func TestTextMapWrapper_action(t *testing.T) { testExtractFunc := func(carrier propagation.TextMapCarrier) { str := carrier.Keys() assert.Len(t, str, 2) assert.Contains(t, str, "key1", "key2") assert.Equal(t, "val1", carrier.Get("key1")) assert.Equal(t, "val2", carrier.Get("key2")) } testInjectFunc := func(carrier propagation.TextMapCarrier) { carrier.Set("key1", "val1") carrier.Set("key2", "val2") wrap, ok := carrier.(*textMapWrapper) assert.True(t, ok) writer, ok := wrap.TextMapWriter.(*testOnlyTextMapWriter) if ok { assert.Contains(t, writer.m, "key1", "key2", "val1", "val2") return } writer2, ok := wrap.TextMapWriter.(*testTextMapReaderAndWriter) assert.True(t, ok) assert.Contains(t, writer2.m, "key1", "key2", "val1", "val2") } onlyWriter, err := newTextMapWrapperForExtract(newTestOnlyTextMapReader()) assert.NoError(t, err) testExtractFunc(onlyWriter) onlyReader, err := newTextMapWrapperForInject(&testOnlyTextMapWriter{m: map[string]string{}}) assert.NoError(t, err) testInjectFunc(onlyReader) both, err := newTextMapWrapperForExtract(newTestTextMapReaderAndWriter()) assert.NoError(t, err) testExtractFunc(both) both, err = newTextMapWrapperForInject(newTestTextMapReaderAndWriter()) assert.NoError(t, err) testInjectFunc(both) } var ( testHeader = "test-trace-id" traceID trace.TraceID = [16]byte{byte(10)} spanID trace.SpanID = [8]byte{byte(11)} ) type testTextMapPropagator struct{} func (testTextMapPropagator) Inject(_ context.Context, carrier propagation.TextMapCarrier) { carrier.Set(testHeader, traceID.String()+":"+spanID.String()) // Test for panic _ = carrier.Get("test") _ = carrier.Keys() } func (testTextMapPropagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context { traces := carrier.Get(testHeader) str := strings.Split(traces, ":") if len(str) != 2 { return ctx } exist := false for _, key := range carrier.Keys() { if strings.EqualFold(testHeader, key) { exist = true break } } if !exist { return ctx } var ( traceID, _ = trace.TraceIDFromHex(str[0]) spanID, _ = trace.SpanIDFromHex(str[1]) sc = trace.NewSpanContext(trace.SpanContextConfig{ TraceID: traceID, SpanID: spanID, }) ) // Test for panic carrier.Set("key", "val") return trace.ContextWithRemoteSpanContext(ctx, sc) } func (testTextMapPropagator) Fields() []string { return []string{"test"} } // textMapCarrier Implemented propagation.TextMapCarrier interface. type textMapCarrier struct { m map[string]string } var _ propagation.TextMapCarrier = (*textMapCarrier)(nil) func newTextCarrier() *textMapCarrier { return &textMapCarrier{m: map[string]string{}} } func (t *textMapCarrier) Get(key string) string { return t.m[key] } func (t *textMapCarrier) Set(key, value string) { t.m[key] = value } func (t *textMapCarrier) Keys() []string { str := make([]string, 0, len(t.m)) for key := range t.m { str = append(str, key) } return str } // testTextMapReader only implemented opentracing.TextMapReader interface. type testTextMapReader struct { m map[string]string } func newTestTextMapReader(m map[string]string) *testTextMapReader { return &testTextMapReader{m: m} } func (t *testTextMapReader) ForeachKey(handler func(key, val string) error) error { for key, val := range t.m { if err := handler(key, val); err != nil { return err } } return nil } // testTextMapWriter only implemented opentracing.TextMapWriter interface. type testTextMapWriter struct { m map[string]string } func newTestTextMapWriter(m map[string]string) *testTextMapWriter { return &testTextMapWriter{m: m} } func (t *testTextMapWriter) Set(key, val string) { t.m[key] = val } type samplable interface { IsSampled() bool } func TestBridgeTracer_ExtractAndInject(t *testing.T) { bridge := NewBridgeTracer() bridge.SetTextMapPropagator(new(testTextMapPropagator)) tmc := newTextCarrier() shareMap := map[string]string{} otTextMap := ot.TextMapCarrier{} httpHeader := ot.HTTPHeadersCarrier(http.Header{}) testCases := []struct { name string injectCarrierType ot.BuiltinFormat extractCarrierType ot.BuiltinFormat extractCarrier any injectCarrier any extractErr error injectErr error }{ { name: "support for propagation.TextMapCarrier", injectCarrierType: ot.TextMap, injectCarrier: tmc, extractCarrierType: ot.TextMap, extractCarrier: tmc, }, { name: "support for opentracing.TextMapReader and opentracing.TextMapWriter", injectCarrierType: ot.TextMap, injectCarrier: otTextMap, extractCarrierType: ot.TextMap, extractCarrier: otTextMap, }, { name: "support for HTTPHeaders", injectCarrierType: ot.HTTPHeaders, injectCarrier: httpHeader, extractCarrierType: ot.HTTPHeaders, extractCarrier: httpHeader, }, { name: "support for opentracing.TextMapReader and opentracing.TextMapWriter,non-same instance", injectCarrierType: ot.TextMap, injectCarrier: newTestTextMapWriter(shareMap), extractCarrierType: ot.TextMap, extractCarrier: newTestTextMapReader(shareMap), }, { name: "inject: format type is HTTPHeaders, but carrier is not HTTPHeadersCarrier", injectCarrierType: ot.HTTPHeaders, injectCarrier: struct{}{}, injectErr: ot.ErrInvalidCarrier, }, { name: "extract: format type is HTTPHeaders, but carrier is not HTTPHeadersCarrier", injectCarrierType: ot.HTTPHeaders, injectCarrier: httpHeader, extractCarrierType: ot.HTTPHeaders, extractCarrier: struct{}{}, extractErr: ot.ErrInvalidCarrier, }, { name: "inject: format type is TextMap, but carrier is cannot be wrapped into propagation.TextMapCarrier", injectCarrierType: ot.TextMap, injectCarrier: struct{}{}, injectErr: ot.ErrInvalidCarrier, }, { name: "extract: format type is TextMap, but carrier is cannot be wrapped into propagation.TextMapCarrier", injectCarrierType: ot.TextMap, injectCarrier: otTextMap, extractCarrierType: ot.TextMap, extractCarrier: struct{}{}, extractErr: ot.ErrInvalidCarrier, }, { name: "inject: unsupported format type", injectCarrierType: ot.Binary, injectErr: ot.ErrUnsupportedFormat, }, { name: "extract: unsupported format type", injectCarrierType: ot.TextMap, injectCarrier: otTextMap, extractCarrierType: ot.Binary, extractCarrier: struct{}{}, extractErr: ot.ErrUnsupportedFormat, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := bridge.Inject(newBridgeSpanContext(trace.NewSpanContext(trace.SpanContextConfig{ TraceID: [16]byte{byte(1)}, SpanID: [8]byte{byte(2)}, }), nil), tc.injectCarrierType, tc.injectCarrier) assert.Equal(t, tc.injectErr, err) if tc.injectErr == nil { spanContext, err := bridge.Extract(tc.extractCarrierType, tc.extractCarrier) assert.Equal(t, tc.extractErr, err) if tc.extractErr == nil { bsc, ok := spanContext.(*bridgeSpanContext) assert.True(t, ok) require.NotNil(t, bsc) require.NotNil(t, bsc.SpanContext) require.NotNil(t, bsc.SpanID()) require.NotNil(t, bsc.TraceID()) assert.Equal(t, spanID.String(), bsc.SpanID().String()) assert.Equal(t, traceID.String(), bsc.TraceID().String()) } } }) } } type nonDeferWrapperTracer struct { *WrapperTracer } func (t *nonDeferWrapperTracer) Start( _ context.Context, name string, opts ...trace.SpanStartOption, ) (context.Context, trace.Span) { // Run start on the parent wrapper with a brand new context // so `WithDeferredSetup` hasn't been called, and the OpenTracing context is injected. return t.WrapperTracer.Start(context.Background(), name, opts...) } func TestBridgeTracer_StartSpan(t *testing.T) { testCases := []struct { name string before func(*testing.T, *BridgeTracer) expectWarnings []string }{ { name: "with no option set", expectWarnings: []string{ "The OpenTelemetry tracer is not set, default no-op tracer is used! Call SetOpenTelemetryTracer to set it up.\n", }, }, { name: "with wrapper tracer set", before: func(_ *testing.T, bridge *BridgeTracer) { wTracer := NewWrapperTracer(bridge, otel.Tracer("test")) bridge.SetOpenTelemetryTracer(wTracer) }, expectWarnings: []string(nil), }, { name: "with a non-deferred wrapper tracer", before: func(_ *testing.T, bridge *BridgeTracer) { wTracer := &nonDeferWrapperTracer{ NewWrapperTracer(bridge, otel.Tracer("test")), } bridge.SetOpenTelemetryTracer(wTracer) }, expectWarnings: []string{ "SDK should have deferred the context setup, see the documentation of go.opentelemetry.io/otel/bridge/opentracing/migration\n", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var warningMessages []string bridge := NewBridgeTracer() bridge.SetWarningHandler(func(msg string) { warningMessages = append(warningMessages, msg) }) if tc.before != nil { tc.before(t, bridge) } span := bridge.StartSpan("test") assert.NotNil(t, span) assert.Equal(t, tc.expectWarnings, warningMessages) }) } } func Test_otTagToOTelAttr(t *testing.T) { key := attribute.Key("test") testCases := []struct { value any expected attribute.KeyValue }{ { value: int8(12), expected: key.Int64(int64(12)), }, { value: uint8(12), expected: key.Int64(int64(12)), }, { value: int16(12), expected: key.Int64(int64(12)), }, { value: uint16(12), expected: key.Int64(int64(12)), }, } for _, tc := range testCases { t.Run(fmt.Sprintf("%s %v", reflect.TypeOf(tc.value), tc.value), func(t *testing.T) { att := otTagToOTelAttr(string(key), tc.value) assert.Equal(t, tc.expected, att) }) } } func TestBridgeSpan_SetTag(t *testing.T) { tracer := newMockTracer() b, _ := NewTracerPair(tracer) testCases := []struct { name string tagKey string tagValue any expected any }{ { name: "basic string key / value", tagKey: "key", tagValue: "value", expected: attribute.String("key", "value"), }, { name: "tag SpanKind no attribute", tagKey: "span.kind", tagValue: "value", expected: nil, }, { name: "Error with bool value and set status code 1", tagKey: "error", tagValue: true, expected: attribute.Int64("status.code", 1), }, { name: "Error with bool but we don't set status code", tagKey: "error", tagValue: false, expected: nil, }, { name: "Error with non-bool type but we don't set status code", tagKey: "error", tagValue: "false", expected: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { span := b.StartSpan("test") span.SetTag(tc.tagKey, tc.tagValue) mockSpan := span.(*bridgeSpan).otelSpan.(*mockSpan) if tc.expected != nil { assert.Contains(t, mockSpan.Attributes, tc.expected) } else { assert.Nil(t, mockSpan.Attributes) } }) } } func Test_otTagsToOTelAttributesKindAndError(t *testing.T) { tracer := newMockTracer() sc := &bridgeSpanContext{} testCases := []struct { name string opt []ot.StartSpanOption expected trace.SpanKind }{ { name: "client", opt: []ot.StartSpanOption{ext.SpanKindRPCClient}, expected: trace.SpanKindClient, }, { name: "server", opt: []ot.StartSpanOption{ext.RPCServerOption(sc)}, expected: trace.SpanKindServer, }, { name: "client string", opt: []ot.StartSpanOption{ot.Tag{Key: "span.kind", Value: "client"}}, expected: trace.SpanKindClient, }, { name: "server string", opt: []ot.StartSpanOption{ot.Tag{Key: "span.kind", Value: "server"}}, expected: trace.SpanKindServer, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { b, _ := NewTracerPair(tracer) s := b.StartSpan(tc.name, tc.opt...) assert.Equal(t, tc.expected, s.(*bridgeSpan).otelSpan.(*mockSpan).SpanKind) }) } } func TestBridge_SpanContext_IsSampled(t *testing.T) { testCases := []struct { name string flags trace.TraceFlags expected bool }{ { name: "not sampled", flags: 0, expected: false, }, { name: "sampled", flags: trace.FlagsSampled, expected: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tracer := newMockTracer() tracer.TraceFlags = tc.flags b, _ := NewTracerPair(tracer) s := b.StartSpan("abc") sc := s.Context() assert.Equal(t, tc.expected, sc.(samplable).IsSampled()) }) } } func TestBridgeSpanContextPromotedMethods(t *testing.T) { bridge := NewBridgeTracer() bridge.SetTextMapPropagator(new(testTextMapPropagator)) tmc := newTextCarrier() type spanContextProvider interface { HasTraceID() bool TraceID() trace.TraceID HasSpanID() bool SpanID() trace.SpanID } err := bridge.Inject(newBridgeSpanContext(trace.NewSpanContext(trace.SpanContextConfig{ TraceID: [16]byte{byte(1)}, SpanID: [8]byte{byte(2)}, }), nil), ot.TextMap, tmc) assert.NoError(t, err) spanContext, err := bridge.Extract(ot.TextMap, tmc) assert.NoError(t, err) assert.NotPanics(t, func() { assert.Equal(t, spanID.String(), spanContext.(spanContextProvider).SpanID().String()) assert.Equal(t, traceID.String(), spanContext.(spanContextProvider).TraceID().String()) assert.True(t, spanContext.(spanContextProvider).HasSpanID()) assert.True(t, spanContext.(spanContextProvider).HasTraceID()) }) } func TestBridgeCarrierBaggagePropagation(t *testing.T) { carriers := []struct { name string factory func() any format ot.BuiltinFormat }{ { name: "TextMapCarrier", factory: func() any { return ot.TextMapCarrier{} }, format: ot.TextMap, }, { name: "HTTPHeadersCarrier", factory: func() any { return ot.HTTPHeadersCarrier{} }, format: ot.HTTPHeaders, }, } testCases := []struct { name string baggageItems []bipBaggage }{ { name: "single baggage item", baggageItems: []bipBaggage{ { key: "foo", value: "bar", }, }, }, { name: "multiple baggage items", baggageItems: []bipBaggage{ { key: "foo", value: "bar", }, { key: "foo2", value: "bar2", }, }, }, { name: "with characters escaped by baggage propagator", baggageItems: []bipBaggage{ { key: "space", value: "Hello world!", }, { key: "utf8", value: "Świat", }, }, }, } for _, c := range carriers { for _, tc := range testCases { t.Run(fmt.Sprintf("%s %s", c.name, tc.name), func(t *testing.T) { mockOtelTracer := newMockTracer() b, _ := NewTracerPair(mockOtelTracer) b.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}), // Required for baggage propagation. ) // Set baggage items. span := b.StartSpan("test") for _, bi := range tc.baggageItems { span.SetBaggageItem(bi.key, bi.value) } defer span.Finish() carrier := c.factory() err := b.Inject(span.Context(), c.format, carrier) assert.NoError(t, err) spanContext, err := b.Extract(c.format, carrier) assert.NoError(t, err) // Check baggage items. bsc, ok := spanContext.(*bridgeSpanContext) assert.True(t, ok) var got []bipBaggage for _, m := range bsc.bag.Members() { got = append(got, bipBaggage{m.Key(), m.Value()}) } assert.ElementsMatch(t, tc.baggageItems, got) }) } } } func TestBridgeFiledEncoder(t *testing.T) { t.Run("emit string", func(t *testing.T) { encoder := &bridgeFieldEncoder{} encoder.EmitString("stringKey", "bar") assert.Equal(t, attribute.String("stringKey", "bar"), encoder.pairs[0]) }) t.Run("emit bool", func(t *testing.T) { encoder := &bridgeFieldEncoder{} encoder.EmitBool("boolKey", true) assert.Equal(t, attribute.Bool("boolKey", true), encoder.pairs[0]) }) t.Run("emit int", func(t *testing.T) { encoder := &bridgeFieldEncoder{} encoder.EmitInt("intKey", 123) assert.Equal(t, attribute.Int("intKey", 123), encoder.pairs[0]) }) t.Run("emit int32", func(t *testing.T) { encoder := &bridgeFieldEncoder{} encoder.EmitInt32("int32Key", int32(123)) assert.Equal(t, attribute.Int("int32Key", 123), encoder.pairs[0]) }) t.Run("emit int64", func(t *testing.T) { encoder := &bridgeFieldEncoder{} encoder.EmitInt64("int64Key", int64(123)) assert.Equal(t, attribute.Int("int64Key", 123), encoder.pairs[0]) }) t.Run("emit uint32", func(t *testing.T) { encoder := &bridgeFieldEncoder{} encoder.EmitUint32("uint32Key", uint32(123)) assert.Equal(t, attribute.Int64("uint32Key", 123), encoder.pairs[0]) }) t.Run("emit uint64", func(t *testing.T) { encoder := &bridgeFieldEncoder{} encoder.EmitUint64("uint64Key", uint64(123)) assert.Equal(t, attribute.String("uint64Key", strconv.FormatUint(123, 10)), encoder.pairs[0]) }) t.Run("emit float32", func(t *testing.T) { encoder := &bridgeFieldEncoder{} encoder.EmitFloat32("float32Key", float32(1.1)) attr := encoder.pairs[0] assert.InDelta(t, float32(1.1), attr.Value.AsFloat64(), 0.0001) }) t.Run("emit float64", func(t *testing.T) { encoder := &bridgeFieldEncoder{} encoder.EmitFloat64("float64Key", 1.1) assert.Equal(t, attribute.Float64("float64Key", 1.1), encoder.pairs[0]) }) t.Run("emit object", func(t *testing.T) { encoder := &bridgeFieldEncoder{} encoder.EmitObject("objectKey", struct{}{}) assert.Equal(t, attribute.String("objectKey", "{}"), encoder.pairs[0]) }) t.Run("emit logger", func(t *testing.T) { encoder := &bridgeFieldEncoder{} called := false encoder.EmitLazyLogger(func(oe otlog.Encoder) { called = true oe.EmitString("lazy", "value") }) assert.True(t, called) assert.Equal(t, attribute.String("lazy", "value"), encoder.pairs[0]) }) } func TestBridgeSpan_LogFields(t *testing.T) { testCases := []struct { name string field otlog.Field expected attribute.KeyValue }{ { name: "string", field: otlog.String("stringKey", "bar"), expected: attribute.String("stringKey", "bar"), }, { name: "bool", field: otlog.Bool("boolKey", true), expected: attribute.Bool("boolKey", true), }, { name: "int", field: otlog.Int("intKey", 12), expected: attribute.Int("intKey", 12), }, { name: "int32", field: otlog.Int32("int32Key", int32(12)), expected: attribute.Int64("int32Key", 12), }, { name: "int64", field: otlog.Int64("int64Key", int64(12)), expected: attribute.Int64("int64Key", 12), }, { name: "uint32", field: otlog.Uint32("uint32Key", uint32(12)), expected: attribute.Int64("uint32Key", 12), }, { name: "uint64", field: otlog.Uint64("uint64Key", uint64(12)), expected: attribute.String("uint64Key", strconv.FormatUint(12, 10)), }, { name: "float32", field: otlog.Float32("float32", float32(1)), expected: attribute.Float64("float32", float64(1)), }, { name: "float64", field: otlog.Float64("float64", 1.1), expected: attribute.Float64("float64", 1.1), }, { name: "error", field: otlog.Error(fmt.Errorf("error")), expected: attribute.String("error.object", "error"), }, { name: "object", field: otlog.Object("object", struct{}{}), expected: attribute.String("object", "{}"), }, { name: "event", field: otlog.Event("eventValue"), expected: attribute.String("event", "eventValue"), }, { name: "message", field: otlog.Message("messageValue"), expected: attribute.String("message", "messageValue"), }, { name: "lazyLog", field: otlog.Lazy(func(fv otlog.Encoder) { fv.EmitBool("bool", true) }), expected: attribute.Bool("bool", true), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tracer := newMockTracer() b, _ := NewTracerPair(tracer) span := b.StartSpan("test") span.LogFields(tc.field) mockSpan := span.(*bridgeSpan).otelSpan.(*mockSpan) event := mockSpan.Events[0] assert.Contains(t, event.Attributes, tc.expected) }) } } func TestBridgeSpan_LogKV(t *testing.T) { testCases := []struct { name string kv [2]any expected attribute.KeyValue }{ { name: "string", kv: [2]any{"string", "value"}, expected: attribute.String("string", "value"), }, { name: "bool", kv: [2]any{"boolKey", true}, expected: attribute.Bool("boolKey", true), }, { name: "int", kv: [2]any{"intKey", int(12)}, expected: attribute.Int("intKey", 12), }, { name: "int8", kv: [2]any{"int8Key", int8(12)}, expected: attribute.Int64("int8Key", 12), }, { name: "int16", kv: [2]any{"int16Key", int16(12)}, expected: attribute.Int64("int16Key", 12), }, { name: "int32", kv: [2]any{"int32", int32(12)}, expected: attribute.Int64("int32", 12), }, { name: "int64", kv: [2]any{"int64Key", int64(12)}, expected: attribute.Int64("int64Key", 12), }, { name: "uint", kv: [2]any{"uintKey", uint(12)}, expected: attribute.String("uintKey", strconv.FormatUint(12, 10)), }, { name: "uint8", kv: [2]any{"uint8Key", uint8(12)}, expected: attribute.Int64("uint8Key", 12), }, { name: "uint16", kv: [2]any{"uint16Key", uint16(12)}, expected: attribute.Int64("uint16Key", 12), }, { name: "uint32", kv: [2]any{"uint32Key", uint32(12)}, expected: attribute.Int64("uint32Key", 12), }, { name: "uint64", kv: [2]any{"uint64Key", uint64(12)}, expected: attribute.String("uint64Key", strconv.FormatUint(12, 10)), }, { name: "float32", kv: [2]any{"float32Key", float32(12)}, expected: attribute.Float64("float32Key", float64(12)), }, { name: "float64", kv: [2]any{"float64Key", 1.1}, expected: attribute.Float64("float64Key", 1.1), }, { name: "error", kv: [2]any{"errorKey", fmt.Errorf("error")}, expected: attribute.String("errorKey", "error"), }, { name: "objectKey", kv: [2]any{"objectKey", struct{}{}}, expected: attribute.String("objectKey", "{}"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tracer := newMockTracer() b, _ := NewTracerPair(tracer) span := b.StartSpan("test") span.LogKV(tc.kv[0], tc.kv[1]) mockSpan := span.(*bridgeSpan).otelSpan.(*mockSpan) event := mockSpan.Events[0] assert.Contains(t, event.Attributes, tc.expected) }) } } func TestBridgeSpan_BaggageItem(t *testing.T) { tracer := NewBridgeTracer() span := tracer.StartSpan("span") assert.Empty(t, span.BaggageItem("invalid-key")) span.SetBaggageItem("key", "val") assert.Equal(t, "val", span.BaggageItem("key")) assert.Equal(t, 1, span.Context().(*bridgeSpanContext).bag.Len()) assert.Equal(t, "key=val", span.Context().(*bridgeSpanContext).bag.String()) span.Context().ForeachBaggageItem(func(k, v string) bool { assert.Equal(t, "key", k) assert.Equal(t, "val", v) return true }) } func TestBridgeSpan_LogEventMethods(t *testing.T) { tracer := newMockTracer() b, _ := NewTracerPair(tracer) span := b.StartSpan("test").(*bridgeSpan) t.Run("LogEvent", func(t *testing.T) { span.LogEvent("event1") mockSpan := span.otelSpan.(*mockSpan) if len(mockSpan.Events) == 0 { t.Fatalf("expected at least one event, got none") } found := false for _, e := range mockSpan.Events { for _, attr := range e.Attributes { if attr.Key == "event" && attr.Value.AsString() == "event1" { found = true } } } if !found { t.Errorf("LogEvent did not log expected event attribute") } }) t.Run("LogEventWithPayload", func(t *testing.T) { span2 := b.StartSpan("test2").(*bridgeSpan) span2.LogEventWithPayload("event2", "payload2") mockSpan := span2.otelSpan.(*mockSpan) foundEvent, foundPayload := false, false for _, e := range mockSpan.Events { for _, attr := range e.Attributes { if attr.Key == "event" && attr.Value.AsString() == "event2" { foundEvent = true } if attr.Key == "payload" && attr.Value.AsString() == "payload2" { foundPayload = true } } } if !foundEvent { t.Errorf("LogEventWithPayload did not log expected event attribute") } if !foundPayload { t.Errorf("LogEventWithPayload did not log expected payload attribute") } }) t.Run("Log", func(t *testing.T) { span3 := b.StartSpan("test3").(*bridgeSpan) logData := ot.LogData{Event: "event3", Payload: "payload3"} span3.Log(logData) mockSpan := span3.otelSpan.(*mockSpan) foundEvent, foundPayload := false, false for _, e := range mockSpan.Events { for _, attr := range e.Attributes { if attr.Key == "event" && attr.Value.AsString() == "event3" { foundEvent = true } if attr.Key == "payload" && attr.Value.AsString() == "payload3" { foundPayload = true } } } if !foundEvent { t.Errorf("Log did not log expected event attribute") } if !foundPayload { t.Errorf("Log did not log expected payload attribute") } }) }