You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-06-23 00:07:52 +02:00
1186 lines
29 KiB
Go
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()))
|
|
})
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|