1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-06-23 00:07:52 +02:00
Files
opentelemetry-go/trace/auto_test.go
Tyler Yahn 7512a2be2e Add the golines golangci-lint formatter (#6513)
Ensure consistent line wrapping (<= 120 characters) within the project.
2025-03-30 03:46:44 -07:00

1186 lines
29 KiB
Go

// 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()))
})
}
})
})
}
}