diff --git a/trace/auto_test.go b/trace/auto_test.go new file mode 100644 index 00000000000..6db55916461 --- /dev/null +++ b/trace/auto_test.go @@ -0,0 +1,1167 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package trace + +import ( + "context" + "encoding/json" + "errors" + "math" + "strconv" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace/internal/telemetry" +) + +const tName = "tracer.name" + +var ( + attrs = []attribute.KeyValue{ + attribute.Bool("bool", true), + attribute.Int("int", -1), + attribute.Int64("int64", 43), + attribute.Float64("float64", 0.3), + attribute.String("string", "value"), + attribute.BoolSlice("bool slice", []bool{true, false, true}), + attribute.IntSlice("int slice", []int{-1, -30, 328}), + attribute.Int64Slice("int64 slice", []int64{1030, 0, 0}), + attribute.Float64Slice("float64 slice", []float64{1e9}), + attribute.StringSlice("string slice", []string{"one", "two"}), + } + + tAttrs = []telemetry.Attr{ + telemetry.Bool("bool", true), + telemetry.Int("int", -1), + telemetry.Int64("int64", 43), + telemetry.Float64("float64", 0.3), + telemetry.String("string", "value"), + telemetry.Slice( + "bool slice", + telemetry.BoolValue(true), + telemetry.BoolValue(false), + telemetry.BoolValue(true), + ), + telemetry.Slice("int slice", + telemetry.IntValue(-1), + telemetry.IntValue(-30), + telemetry.IntValue(328), + ), + telemetry.Slice("int64 slice", + telemetry.Int64Value(1030), + telemetry.Int64Value(0), + telemetry.Int64Value(0), + ), + telemetry.Slice("float64 slice", telemetry.Float64Value(1e9)), + telemetry.Slice("string slice", + telemetry.StringValue("one"), + telemetry.StringValue("two"), + ), + } + + spanContext0 = NewSpanContext(SpanContextConfig{ + TraceID: TraceID{0x1}, + SpanID: SpanID{0x1}, + TraceFlags: FlagsSampled, + }) + spanContext1 = NewSpanContext(SpanContextConfig{ + TraceID: TraceID{0x2}, + SpanID: SpanID{0x2}, + TraceFlags: FlagsSampled, + }) + + link0 = Link{ + SpanContext: spanContext0, + Attributes: []attribute.KeyValue{ + attribute.Int("n", 0), + }, + } + link1 = Link{ + SpanContext: spanContext1, + Attributes: []attribute.KeyValue{ + attribute.Int("n", 1), + }, + } + + tLink0 = &telemetry.SpanLink{ + TraceID: telemetry.TraceID(spanContext0.TraceID()), + SpanID: telemetry.SpanID(spanContext0.SpanID()), + Flags: uint32(spanContext0.TraceFlags()), + Attrs: []telemetry.Attr{telemetry.Int("n", 0)}, + } + tLink1 = &telemetry.SpanLink{ + TraceID: telemetry.TraceID(spanContext1.TraceID()), + SpanID: telemetry.SpanID(spanContext1.SpanID()), + Flags: uint32(spanContext1.TraceFlags()), + Attrs: []telemetry.Attr{telemetry.Int("n", 1)}, + } +) + +func TestTracerProviderInstance(t *testing.T) { + t.Parallel() + + tp0, tp1 := newAutoTracerProvider(), newAutoTracerProvider() + + assert.Same(t, tracerProviderInstance, tp0) + assert.Same(t, tracerProviderInstance, tp1) +} + +func TestTracerProviderConcurrentSafe(t *testing.T) { + t.Parallel() + + const goroutines = 10 + + run := func(tp TracerProvider) <-chan struct{} { + done := make(chan struct{}) + go func(tp TracerProvider) { + defer close(done) + + var wg sync.WaitGroup + for i := 0; i < goroutines; i++ { + wg.Add(1) + go func(name, version string) { + defer wg.Done() + _ = tp.Tracer(name, WithInstrumentationVersion(version)) + }("tracer"+strconv.Itoa(i%4), strconv.Itoa(i%2)) + } + + wg.Wait() + }(tp) + return done + } + + assert.NotPanics(t, func() { + done0, done1 := run(newAutoTracerProvider()), run(newAutoTracerProvider()) + + <-done0 + <-done1 + }) +} + +func TestSpanKindTransform(t *testing.T) { + tests := map[SpanKind]telemetry.SpanKind{ + SpanKind(-1): telemetry.SpanKind(0), + SpanKindUnspecified: telemetry.SpanKind(0), + SpanKind(math.MaxInt): telemetry.SpanKind(0), + + SpanKindInternal: telemetry.SpanKindInternal, + SpanKindServer: telemetry.SpanKindServer, + SpanKindClient: telemetry.SpanKindClient, + SpanKindProducer: telemetry.SpanKindProducer, + SpanKindConsumer: telemetry.SpanKindConsumer, + } + + for in, want := range tests { + assert.Equal(t, want, spanKind(in), in.String()) + } +} + +func TestTracerStartPropagatesOrigCtx(t *testing.T) { + t.Parallel() + + type ctxKey struct{} + var key ctxKey + val := "value" + + ctx := context.WithValue(context.Background(), key, val) + ctx, _ = newAutoTracerProvider().Tracer(tName).Start(ctx, "span.name") + + assert.Equal(t, val, ctx.Value(key)) +} + +func TestTracerStartReturnsNonNilSpan(t *testing.T) { + t.Parallel() + + tr := newAutoTracerProvider().Tracer(tName) + _, s := tr.Start(context.Background(), "span.name") + assert.NotNil(t, s) +} + +func TestTracerStartAddsSpanToCtx(t *testing.T) { + t.Parallel() + + tr := newAutoTracerProvider().Tracer(tName) + ctx, s := tr.Start(context.Background(), "span.name") + + assert.Same(t, s, SpanFromContext(ctx)) +} + +func TestTracerConcurrentSafe(t *testing.T) { + t.Parallel() + + const goroutines = 10 + + ctx := context.Background() + run := func(tracer Tracer) <-chan struct{} { + done := make(chan struct{}) + + go func(tr Tracer) { + defer close(done) + + var wg sync.WaitGroup + for i := 0; i < goroutines; i++ { + wg.Add(1) + go func(name string) { + defer wg.Done() + _, _ = tr.Start(ctx, name) + }("span" + strconv.Itoa(i)) + } + + wg.Wait() + }(tracer) + + return done + } + + assert.NotPanics(t, func() { + tp := newAutoTracerProvider() + done0, done1 := run(tp.Tracer("t0")), run(tp.Tracer("t1")) + + <-done0 + <-done1 + }) +} + +func TestSpanCreation(t *testing.T) { + const ( + spanName = "span name" + tracerName = "go.opentelemetry.io/otel/sdk/test" + tracerVer = "v0.1.0" + ) + + ts := time.Now() + + tracer := newAutoTracerProvider().Tracer( + tracerName, + WithInstrumentationVersion(tracerVer), + WithSchemaURL(semconv.SchemaURL), + ) + + assertTracer := func(traces *telemetry.Traces) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + + rs := traces.ResourceSpans + require.Len(t, rs, 1) + sss := rs[0].ScopeSpans + require.Len(t, sss, 1) + ss := sss[0] + assert.Equal(t, tracerName, ss.Scope.Name, "tracer name") + assert.Equal(t, tracerVer, ss.Scope.Version, "tracer version") + assert.Equal(t, semconv.SchemaURL, ss.SchemaURL, "tracer schema URL") + } + } + + testcases := []struct { + TestName string + SpanName string + Options []SpanStartOption + Setup func(*testing.T) + Eval func(*testing.T, context.Context, *autoSpan) + }{ + { + TestName: "SampledByDefault", + Eval: func(t *testing.T, _ context.Context, s *autoSpan) { + t.Run("Tracer", assertTracer(s.traces)) + + assert.True(t, s.sampled.Load(), "not sampled by default.") + }, + }, + { + TestName: "ParentSpanContext", + Setup: func(t *testing.T) { + orig := start + t.Cleanup(func() { start = orig }) + start = func(_ context.Context, _ *autoSpan, psc *SpanContext, _ *bool, _ *SpanContext) { + *psc = spanContext0 + } + }, + Eval: func(t *testing.T, _ context.Context, s *autoSpan) { + t.Run("Tracer", assertTracer(s.traces)) + + want := spanContext0.SpanID().String() + got := s.span.ParentSpanID.String() + assert.Equal(t, want, got) + }, + }, + { + TestName: "SpanContext", + Setup: func(t *testing.T) { + orig := start + t.Cleanup(func() { start = orig }) + start = func(_ context.Context, _ *autoSpan, _ *SpanContext, _ *bool, sc *SpanContext) { + *sc = spanContext0 + } + }, + Eval: func(t *testing.T, _ context.Context, s *autoSpan) { + t.Run("Tracer", assertTracer(s.traces)) + + str := func(i interface{ String() string }) string { + return i.String() + } + assert.Equal(t, str(spanContext0.TraceID()), s.span.TraceID.String(), "trace ID") + assert.Equal(t, str(spanContext0.SpanID()), s.span.SpanID.String(), "autoSpan ID") + assert.Equal(t, uint32(spanContext0.TraceFlags()), s.span.Flags, "flags") + assert.Equal(t, str(spanContext0.TraceState()), s.span.TraceState, "tracestate") + }, + }, + { + TestName: "NotSampled", + Setup: func(t *testing.T) { + orig := start + t.Cleanup(func() { start = orig }) + start = func(_ context.Context, _ *autoSpan, _ *SpanContext, s *bool, _ *SpanContext) { + *s = false + } + }, + Eval: func(t *testing.T, _ context.Context, s *autoSpan) { + assert.False(t, s.sampled.Load(), "sampled") + }, + }, + { + TestName: "WithName", + SpanName: spanName, + Eval: func(t *testing.T, _ context.Context, s *autoSpan) { + t.Run("Tracer", assertTracer(s.traces)) + assert.Equal(t, spanName, s.span.Name) + }, + }, + { + TestName: "WithSpanKind", + Options: []SpanStartOption{ + WithSpanKind(SpanKindClient), + }, + Eval: func(t *testing.T, _ context.Context, s *autoSpan) { + t.Run("Tracer", assertTracer(s.traces)) + assert.Equal(t, telemetry.SpanKindClient, s.span.Kind) + }, + }, + { + TestName: "WithTimestamp", + Options: []SpanStartOption{ + WithTimestamp(ts), + }, + Eval: func(t *testing.T, _ context.Context, s *autoSpan) { + t.Run("Tracer", assertTracer(s.traces)) + assert.Equal(t, ts, s.span.StartTime) + }, + }, + { + TestName: "WithAttributes", + Options: []SpanStartOption{ + WithAttributes(attrs...), + }, + Eval: func(t *testing.T, _ context.Context, s *autoSpan) { + t.Run("Tracer", assertTracer(s.traces)) + assert.Equal(t, tAttrs, s.span.Attrs) + }, + }, + { + TestName: "WithLinks", + Options: []SpanStartOption{ + WithLinks(link0, link1), + }, + Eval: func(t *testing.T, _ context.Context, s *autoSpan) { + t.Run("Tracer", assertTracer(s.traces)) + want := []*telemetry.SpanLink{tLink0, tLink1} + assert.Equal(t, want, s.span.Links) + }, + }, + } + + ctx := context.Background() + for _, tc := range testcases { + t.Run(tc.TestName, func(t *testing.T) { + if tc.Setup != nil { + tc.Setup(t) + } + + c, sIface := tracer.Start(ctx, tc.SpanName, tc.Options...) + require.IsType(t, &autoSpan{}, sIface) + s := sIface.(*autoSpan) + + tc.Eval(t, c, s) + }) + } +} + +func TestSpanEnd(t *testing.T) { + orig := ended + t.Cleanup(func() { ended = orig }) + + var buf []byte + ended = func(b []byte) { buf = b } + + timeNow := time.Unix(0, time.Now().UnixNano()) // No location. + + tests := []struct { + Name string + Options []SpanEndOption + Eval func(*testing.T, time.Time) + }{ + { + Name: "Now", + Eval: func(t *testing.T, ts time.Time) { + assert.False(t, ts.IsZero(), "zero end time") + }, + }, + { + Name: "WithTimestamp", + Options: []SpanEndOption{ + WithTimestamp(timeNow), + }, + Eval: func(t *testing.T, ts time.Time) { + assert.Equal(t, timeNow, ts, "end time not set") + }, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + s := spanBuilder{}.Build() + s.End(test.Options...) + + assert.False(t, s.sampled.Load(), "ended autoSpan should not be sampled") + require.NotNil(t, buf, "no span data emitted") + + var traces telemetry.Traces + err := json.Unmarshal(buf, &traces) + require.NoError(t, err) + + rs := traces.ResourceSpans + require.Len(t, rs, 1) + ss := rs[0].ScopeSpans + require.Len(t, ss, 1) + spans := ss[0].Spans + require.Len(t, spans, 1) + + test.Eval(t, spans[0].EndTime) + }) + } +} + +func TestSpanNilUnsampledGuards(t *testing.T) { + run := func(fn func(s *autoSpan)) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + + f := func(s *autoSpan) func() { return func() { fn(s) } } + assert.NotPanics(t, f(nil), "nil span") + assert.NotPanics(t, f(new(autoSpan)), "unsampled span") + } + } + + t.Run("End", run(func(s *autoSpan) { s.End() })) + t.Run("AddEvent", run(func(s *autoSpan) { s.AddEvent("event name") })) + t.Run("AddLink", run(func(s *autoSpan) { s.AddLink(Link{}) })) + t.Run("IsRecording", run(func(s *autoSpan) { _ = s.IsRecording() })) + t.Run("RecordError", run(func(s *autoSpan) { s.RecordError(nil) })) + t.Run("SpanContext", run(func(s *autoSpan) { _ = s.SpanContext() })) + t.Run("SetStatus", run(func(s *autoSpan) { s.SetStatus(codes.Error, "test") })) + t.Run("SetName", run(func(s *autoSpan) { s.SetName("span name") })) + t.Run("SetAttributes", run(func(s *autoSpan) { s.SetAttributes(attrs...) })) + t.Run("TracerProvider", run(func(s *autoSpan) { _ = s.TracerProvider() })) +} + +func TestSpanAddLink(t *testing.T) { + s := spanBuilder{ + Options: []SpanStartOption{WithLinks(link0)}, + }.Build() + s.AddLink(link1) + + want := []*telemetry.SpanLink{tLink0, tLink1} + assert.Equal(t, want, s.span.Links) +} + +func TestSpanAddLinkLimit(t *testing.T) { + tests := []struct { + limit int + want []*telemetry.SpanLink + dropped uint32 + }{ + {0, nil, 2}, + {1, []*telemetry.SpanLink{tLink1}, 1}, + {2, []*telemetry.SpanLink{tLink0, tLink1}, 0}, + {-1, []*telemetry.SpanLink{tLink0, tLink1}, 0}, + } + + for _, test := range tests { + t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) { + orig := maxSpan.Links + maxSpan.Links = test.limit + t.Cleanup(func() { maxSpan.Links = orig }) + + builder := spanBuilder{} + s := builder.Build() + s.AddLink(link0) + s.AddLink(link1) + assert.Equal(t, test.want, s.span.Links, "AddLink") + assert.Equal(t, test.dropped, s.span.DroppedLinks, "AddLink DroppedLinks") + + builder.Options = []SpanStartOption{ + WithLinks(link0, link1), + } + s = builder.Build() + assert.Equal(t, test.want, s.span.Links, "NewSpan") + assert.Equal(t, test.dropped, s.span.DroppedLinks, "NewSpan DroppedLinks") + }) + } +} + +func TestSpanLinkAttrLimit(t *testing.T) { + tests := []struct { + limit int + want []telemetry.Attr + dropped uint32 + }{ + {0, nil, uint32(len(tAttrs))}, + {2, tAttrs[:2], uint32(len(tAttrs) - 2)}, + {len(tAttrs), tAttrs, 0}, + {-1, tAttrs, 0}, + } + + link := Link{Attributes: attrs} + for _, test := range tests { + t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) { + orig := maxSpan.LinkAttrs + maxSpan.LinkAttrs = test.limit + t.Cleanup(func() { maxSpan.LinkAttrs = orig }) + + builder := spanBuilder{} + + s := builder.Build() + s.AddLink(link) + + require.Len(t, s.span.Links, 1) + got := s.span.Links[0] + assert.Equal(t, test.want, got.Attrs, "AddLink attrs") + assert.Equal(t, test.dropped, got.DroppedAttrs, "dropped AddLink attrs") + + builder.Options = []SpanStartOption{WithLinks(link)} + s = builder.Build() + + require.Len(t, s.span.Links, 1) + got = s.span.Links[0] + assert.Equal(t, test.want, got.Attrs, "NewSpan link attrs") + assert.Equal(t, test.dropped, got.DroppedAttrs, "dropped NewSpan link attrs") + }) + } +} + +func TestSpanIsRecording(t *testing.T) { + builder := spanBuilder{} + s := builder.Build() + assert.True(t, s.IsRecording(), "sampled span should be recorded") + + builder.NotSampled = true + s = builder.Build() + assert.False(t, s.IsRecording(), "unsampled span should not be recorded") +} + +func TestSpanRecordError(t *testing.T) { + s := spanBuilder{}.Build() + + var want []*telemetry.SpanEvent + s.RecordError(nil) + require.Equal(t, want, s.span.Events, "nil error recorded") + + ts := time.Now() + err := errors.New("test") + s.RecordError( + err, + WithTimestamp(ts), + WithAttributes(attribute.Bool("testing", true)), + ) + want = append(want, &telemetry.SpanEvent{ + Name: semconv.ExceptionEventName, + Time: ts, + Attrs: []telemetry.Attr{ + telemetry.Bool("testing", true), + telemetry.String(string(semconv.ExceptionTypeKey), "*errors.errorString"), + telemetry.String(string(semconv.ExceptionMessageKey), err.Error()), + }, + }) + assert.Equal(t, want, s.span.Events, "nil error recorded") + + s.RecordError(err, WithStackTrace(true)) + require.Len(t, s.span.Events, 2, "missing event") + + var hasST bool + for _, attr := range s.span.Events[1].Attrs { + if attr.Key == string(semconv.ExceptionStacktraceKey) { + hasST = true + break + } + } + assert.True(t, hasST, "missing stacktrace attribute") +} + +func TestAddEventLimit(t *testing.T) { + const a, b, c = "a", "b", "c" + + ts := time.Now() + + evtA := &telemetry.SpanEvent{Name: "a", Time: ts} + evtB := &telemetry.SpanEvent{Name: "b", Time: ts} + evtC := &telemetry.SpanEvent{Name: "c", Time: ts} + + tests := []struct { + limit int + want []*telemetry.SpanEvent + dropped uint32 + }{ + {0, nil, 3}, + {1, []*telemetry.SpanEvent{evtC}, 2}, + {2, []*telemetry.SpanEvent{evtB, evtC}, 1}, + {3, []*telemetry.SpanEvent{evtA, evtB, evtC}, 0}, + {-1, []*telemetry.SpanEvent{evtA, evtB, evtC}, 0}, + } + + for _, test := range tests { + t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) { + orig := maxSpan.Events + maxSpan.Events = test.limit + t.Cleanup(func() { maxSpan.Events = orig }) + + builder := spanBuilder{} + + s := builder.Build() + s.addEvent(a, ts, nil) + s.addEvent(b, ts, nil) + s.addEvent(c, ts, nil) + + assert.Equal(t, test.want, s.span.Events, "add event") + assert.Equal(t, test.dropped, s.span.DroppedEvents, "dropped events") + }) + } +} + +func TestAddEventAttrLimit(t *testing.T) { + tests := []struct { + limit int + want []telemetry.Attr + dropped uint32 + }{ + {0, nil, uint32(len(tAttrs))}, + {2, tAttrs[:2], uint32(len(tAttrs) - 2)}, + {len(tAttrs), tAttrs, 0}, + {-1, tAttrs, 0}, + } + + for _, test := range tests { + t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) { + orig := maxSpan.EventAttrs + maxSpan.EventAttrs = test.limit + t.Cleanup(func() { maxSpan.EventAttrs = orig }) + + builder := spanBuilder{} + + s := builder.Build() + s.addEvent("name", time.Now(), attrs) + + require.Len(t, s.span.Events, 1) + got := s.span.Events[0] + assert.Equal(t, test.want, got.Attrs, "event attrs") + assert.Equal(t, test.dropped, got.DroppedAttrs, "dropped event attrs") + }) + } +} + +func TestSpanSpanContext(t *testing.T) { + s := spanBuilder{SpanContext: spanContext0}.Build() + assert.Equal(t, spanContext0, s.SpanContext()) +} + +func TestSpanSetStatus(t *testing.T) { + s := spanBuilder{}.Build() + + assert.Nil(t, s.span.Status, "empty status should not be set") + + const msg = "test" + want := &telemetry.Status{Message: msg} + + for c, tCode := range map[codes.Code]telemetry.StatusCode{ + codes.Error: telemetry.StatusCodeError, + codes.Ok: telemetry.StatusCodeOK, + codes.Unset: telemetry.StatusCodeUnset, + } { + want.Code = tCode + s.SetStatus(c, msg) + assert.Equalf(t, want, s.span.Status, "code: %s, msg: %s", c, msg) + } +} + +func TestSpanSetName(t *testing.T) { + const name = "span name" + builder := spanBuilder{} + + s := builder.Build() + s.SetName(name) + assert.Equal(t, name, s.span.Name, "span name not set") + + builder.Name = "alt" + s = builder.Build() + s.SetName(name) + assert.Equal(t, name, s.span.Name, "SetName did not overwrite") +} + +func TestSpanSetAttributes(t *testing.T) { + builder := spanBuilder{} + + s := builder.Build() + s.SetAttributes(attrs...) + assert.Equal(t, tAttrs, s.span.Attrs, "span attributes not set") + + builder.Options = []SpanStartOption{ + WithAttributes(attrs[0].Key.Bool(!attrs[0].Value.AsBool())), + } + + s = builder.Build() + s.SetAttributes(attrs...) + assert.Equal(t, tAttrs, s.span.Attrs, "SpanAttributes did not override") +} + +func TestSpanAttributeLimits(t *testing.T) { + tests := []struct { + limit int + want []telemetry.Attr + dropped uint32 + }{ + {0, nil, uint32(len(tAttrs))}, + {2, tAttrs[:2], uint32(len(tAttrs) - 2)}, + {len(tAttrs), tAttrs, 0}, + {-1, tAttrs, 0}, + } + + for _, test := range tests { + t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) { + orig := maxSpan.Attrs + maxSpan.Attrs = test.limit + t.Cleanup(func() { maxSpan.Attrs = orig }) + + builder := spanBuilder{} + + s := builder.Build() + s.SetAttributes(attrs...) + assert.Equal(t, test.want, s.span.Attrs, "set span attributes") + assert.Equal(t, test.dropped, s.span.DroppedAttrs, "dropped attrs") + + s.SetAttributes(attrs...) + assert.Equal(t, test.want, s.span.Attrs, "set span attributes twice") + assert.Equal(t, 2*test.dropped, s.span.DroppedAttrs, "2x dropped attrs") + + builder.Options = []SpanStartOption{WithAttributes(attrs...)} + + s = builder.Build() + assert.Equal(t, test.want, s.span.Attrs, "new span attributes") + assert.Equal(t, test.dropped, s.span.DroppedAttrs, "dropped attrs") + }) + } +} + +func TestSpanAttributeValueLimits(t *testing.T) { + value := "hello world" + + aStr := attribute.String("string", value) + aStrSlice := attribute.StringSlice("slice", []string{value, value}) + + eq := func(a, b []telemetry.Attr) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !a[i].Equal(b[i]) { + return false + } + } + return true + } + + tests := []struct { + limit int + want string + }{ + {0, ""}, + {2, value[:2]}, + {11, value}, + {-1, value}, + } + for _, test := range tests { + t.Run("Limit/"+strconv.Itoa(test.limit), func(t *testing.T) { + orig := maxSpan.AttrValueLen + maxSpan.AttrValueLen = test.limit + t.Cleanup(func() { maxSpan.AttrValueLen = orig }) + + builder := spanBuilder{} + + want := []telemetry.Attr{ + telemetry.String("string", test.want), + telemetry.Slice( + "slice", + telemetry.StringValue(test.want), + telemetry.StringValue(test.want), + ), + } + + s := builder.Build() + s.SetAttributes(aStr, aStrSlice) + assert.Truef(t, eq(want, s.span.Attrs), "set span attributes: got %#v, want %#v", s.span.Attrs, want) + + s.AddEvent("test", WithAttributes(aStr, aStrSlice)) + assert.Truef(t, eq(want, s.span.Events[0].Attrs), "span event attributes: got %#v, want %#v", s.span.Events[0].Attrs, want) + + s.AddLink(Link{ + Attributes: []attribute.KeyValue{aStr, aStrSlice}, + }) + assert.Truef(t, eq(want, s.span.Links[0].Attrs), "span link attributes: got %#v, want %#v", s.span.Links[0].Attrs, want) + + builder.Options = []SpanStartOption{ + WithAttributes(aStr, aStrSlice), + WithLinks(Link{ + Attributes: []attribute.KeyValue{aStr, aStrSlice}, + }), + } + s = builder.Build() + assert.Truef(t, eq(want, s.span.Attrs), "new span attributes: got %#v, want %#v", s.span.Attrs, want) + assert.Truef(t, eq(want, s.span.Links[0].Attrs), "new span link attributes: got %#v, want %#v", s.span.Attrs, want) + }) + } +} + +func TestSpanTracerProvider(t *testing.T) { + var s autoSpan + + got := s.TracerProvider() + assert.IsType(t, &autoTracerProvider{}, got) +} + +type spanBuilder struct { + Name string + NotSampled bool + SpanContext SpanContext + Options []SpanStartOption +} + +func (b spanBuilder) Build() *autoSpan { + tracer := new(autoTracer) + s := &autoSpan{spanContext: b.SpanContext} + s.sampled.Store(!b.NotSampled) + s.traces, s.span = tracer.traces( + b.Name, + NewSpanStartConfig(b.Options...), + s.spanContext, + SpanContext{}, + ) + + return s +} + +func TestTruncate(t *testing.T) { + type group struct { + limit int + input string + expected string + } + + tests := []struct { + name string + groups []group + }{ + // Edge case: limit is negative, no truncation should occur + { + name: "NoTruncation", + groups: []group{ + {-1, "No truncation!", "No truncation!"}, + }, + }, + + // Edge case: string is already shorter than the limit, no truncation + // should occur + { + name: "ShortText", + groups: []group{ + {10, "Short text", "Short text"}, + {15, "Short text", "Short text"}, + {100, "Short text", "Short text"}, + }, + }, + + // Edge case: truncation happens with ASCII characters only + { + name: "ASCIIOnly", + groups: []group{ + {1, "Hello World!", "H"}, + {5, "Hello World!", "Hello"}, + {12, "Hello World!", "Hello World!"}, + }, + }, + + // Truncation including multi-byte characters (UTF-8) + { + name: "ValidUTF-8", + groups: []group{ + {7, "Hello, 世界", "Hello, "}, + {8, "Hello, 世界", "Hello, 世"}, + {2, "こんにちは", "こん"}, + {3, "こんにちは", "こんに"}, + {5, "こんにちは", "こんにちは"}, + {12, "こんにちは", "こんにちは"}, + }, + }, + + // Truncation with invalid UTF-8 characters + { + name: "InvalidUTF-8", + groups: []group{ + {11, "Invalid\x80text", "Invalidtext"}, + // Do not modify invalid text if equal to limit. + {11, "Valid text\x80", "Valid text\x80"}, + // Do not modify invalid text if under limit. + {15, "Valid text\x80", "Valid text\x80"}, + {5, "Hello\x80World", "Hello"}, + {11, "Hello\x80World\x80!", "HelloWorld!"}, + {15, "Hello\x80World\x80Test", "HelloWorldTest"}, + {15, "Hello\x80\x80\x80World\x80Test", "HelloWorldTest"}, + {15, "\x80\x80\x80Hello\x80\x80\x80World\x80Test\x80\x80", "HelloWorldTest"}, + }, + }, + + // Truncation with mixed validn and invalid UTF-8 characters + { + name: "MixedUTF-8", + groups: []group{ + {6, "€"[0:2] + "hello€€", "hello€"}, + {6, "€" + "€"[0:2] + "hello", "€hello"}, + {11, "Valid text\x80📜", "Valid text📜"}, + {11, "Valid text📜\x80", "Valid text📜"}, + {14, "😊 Hello\x80World🌍🚀", "😊 HelloWorld🌍🚀"}, + {14, "😊\x80 Hello\x80World🌍🚀", "😊 HelloWorld🌍🚀"}, + {14, "😊\x80 Hello\x80World🌍\x80🚀", "😊 HelloWorld🌍🚀"}, + {14, "😊\x80 Hello\x80World🌍\x80🚀\x80", "😊 HelloWorld🌍🚀"}, + {14, "\x80😊\x80 Hello\x80World🌍\x80🚀\x80", "😊 HelloWorld🌍🚀"}, + }, + }, + + // Edge case: empty string, should return empty string + { + name: "Empty", + groups: []group{ + {5, "", ""}, + }, + }, + + // Edge case: limit is 0, should return an empty string + { + name: "Zero", + groups: []group{ + {0, "Some text", ""}, + {0, "", ""}, + }, + }, + } + + for _, tt := range tests { + for _, g := range tt.groups { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := truncate(g.limit, g.input) + assert.Equalf( + t, g.expected, got, + "input: %q([]rune%v))\ngot: %q([]rune%v)\nwant %q([]rune%v)", + g.input, []rune(g.input), + got, []rune(got), + g.expected, []rune(g.expected), + ) + }) + } + } +} + +func BenchmarkTruncate(b *testing.B) { + run := func(limit int, input string) func(b *testing.B) { + return func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + var out string + for pb.Next() { + out = truncate(limit, input) + } + _ = out + }) + } + } + b.Run("Unlimited", run(-1, "hello 😊 world 🌍🚀")) + b.Run("Zero", run(0, "Some text")) + b.Run("Short", run(10, "Short Text")) + b.Run("ASCII", run(5, "Hello, World!")) + b.Run("ValidUTF-8", run(10, "hello 😊 world 🌍🚀")) + b.Run("InvalidUTF-8", run(6, "€"[0:2]+"hello€€")) + b.Run("MixedUTF-8", run(14, "\x80😊\x80 Hello\x80World🌍\x80🚀\x80")) +} + +func TestSpanConcurrentSafe(t *testing.T) { + t.Parallel() + + const ( + nTracers = 2 + nSpans = 2 + nGoroutine = 10 + ) + + runSpan := func(s Span) <-chan struct{} { + done := make(chan struct{}) + go func(span Span) { + defer close(done) + + var wg sync.WaitGroup + for i := 0; i < nGoroutine; i++ { + wg.Add(1) + go func(n int) { + defer wg.Done() + + _ = span.IsRecording() + _ = span.SpanContext() + _ = span.TracerProvider() + + span.AddEvent("event") + span.AddLink(Link{}) + span.RecordError(errors.New("err")) + span.SetStatus(codes.Error, "error") + span.SetName("span" + strconv.Itoa(n)) + span.SetAttributes(attribute.Bool("key", true)) + + span.End() + }(i) + } + + wg.Wait() + }(s) + return done + } + + runTracer := func(tr Tracer) <-chan struct{} { + done := make(chan struct{}) + go func(tracer Tracer) { + defer close(done) + + ctx := context.Background() + + var wg sync.WaitGroup + for i := 0; i < nSpans; i++ { + wg.Add(1) + go func(n int) { + defer wg.Done() + _, s := tracer.Start(ctx, "span"+strconv.Itoa(n)) + <-runSpan(s) + }(i) + } + + wg.Wait() + }(tr) + return done + } + + run := func(tp TracerProvider) <-chan struct{} { + done := make(chan struct{}) + go func(provider TracerProvider) { + defer close(done) + + var wg sync.WaitGroup + for i := 0; i < nTracers; i++ { + wg.Add(1) + go func(n int) { + defer wg.Done() + <-runTracer(provider.Tracer("tracer" + strconv.Itoa(n))) + }(i) + } + + wg.Wait() + }(tp) + return done + } + + assert.NotPanics(t, func() { + done0, done1 := run(newAutoTracerProvider()), run(newAutoTracerProvider()) + + <-done0 + <-done1 + }) +} + +func TestSpanLimit(t *testing.T) { + tests := []struct { + name string + get func(spanLimits) int + zero int + keys []string + }{ + { + name: "AttributeValueLengthLimit", + get: func(sl spanLimits) int { return sl.AttrValueLen }, + zero: -1, + keys: []string{ + "OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", + "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", + }, + }, + { + name: "AttributeCountLimit", + get: func(sl spanLimits) int { return sl.Attrs }, + zero: 128, + keys: []string{ + "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", + "OTEL_ATTRIBUTE_COUNT_LIMIT", + }, + }, + { + name: "EventCountLimit", + get: func(sl spanLimits) int { return sl.Events }, + zero: 128, + keys: []string{"OTEL_SPAN_EVENT_COUNT_LIMIT"}, + }, + { + name: "EventAttributeCountLimit", + get: func(sl spanLimits) int { return sl.EventAttrs }, + zero: 128, + keys: []string{"OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT"}, + }, + { + name: "LinkCountLimit", + get: func(sl spanLimits) int { return sl.Links }, + zero: 128, + keys: []string{"OTEL_SPAN_LINK_COUNT_LIMIT"}, + }, + { + name: "LinkAttributeCountLimit", + get: func(sl spanLimits) int { return sl.LinkAttrs }, + zero: 128, + keys: []string{"OTEL_LINK_ATTRIBUTE_COUNT_LIMIT"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Run("Default", func(t *testing.T) { + assert.Equal(t, test.zero, test.get(newSpanLimits())) + }) + + t.Run("ValidValue", func(t *testing.T) { + for _, key := range test.keys { + t.Run(key, func(t *testing.T) { + t.Setenv(key, "43") + assert.Equal(t, 43, test.get(newSpanLimits())) + }) + } + }) + }) + } +}