1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2024-12-12 10:04:29 +02:00
opentelemetry-go/sdk/trace/trace_test.go
Jack Wink 33699d242d
Adds semantic conventions for exceptions (#1492)
Adds support for the opentelemetry exceptions semantic conventions. In
short, this has RecordError produce an exception event with exception
attributes instead of using the error event and error attributes.

While golang does not have exceptions, the spec itself does not
differentiate between errors and exceptions for recording purposes.
RecordError was kept as the method name, both for backwards
compatibility and to reduce confusion (the method signature takes in a
golang error object). The spec appears to allow this, as it suggests the
method is optional and signature may reflect whatever is most appropriate
for the language implementing it.

It may seem non-intuitive to log an exception event from a method called
RecordError, but it's beneficial to have consistent behavior across all
opentelemetry SDKs. Downstream projects like the opentelemetry-collector
can build off of the published API and not special case behaviors from
individual languages.
2021-04-01 13:07:46 -07:00

1741 lines
51 KiB
Go

// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package trace
import (
"context"
"errors"
"fmt"
"math"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/oteltest"
"go.opentelemetry.io/otel/semconv"
"go.opentelemetry.io/otel/trace"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ottest "go.opentelemetry.io/otel/internal/internaltest"
export "go.opentelemetry.io/otel/sdk/export/trace"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/resource"
)
type storingHandler struct {
errs []error
}
func (s *storingHandler) Handle(err error) {
s.errs = append(s.errs, err)
}
func (s *storingHandler) Reset() {
s.errs = nil
}
var (
tid trace.TraceID
sid trace.SpanID
sc trace.SpanContext
handler *storingHandler = &storingHandler{}
k1, k2, k3 attribute.Key
kv1, kv2, kv3 attribute.KeyValue
)
func init() {
tid, _ = trace.TraceIDFromHex("01020304050607080102040810203040")
sid, _ = trace.SpanIDFromHex("0102040810203040")
sc = trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
SpanID: sid,
TraceFlags: 0x1,
})
k1 = attribute.Key("k1")
kv1 = k1.String("v1")
k2 = attribute.Key("k2")
kv2 = k2.String("v2")
k3 = attribute.Key("k3")
kv3 = k3.String("v3")
otel.SetErrorHandler(handler)
}
func TestTracerFollowsExpectedAPIBehaviour(t *testing.T) {
harness := oteltest.NewHarness(t)
harness.TestTracerProvider(func() trace.TracerProvider {
return NewTracerProvider(WithSampler(TraceIDRatioBased(0)))
})
tp := NewTracerProvider(WithSampler(TraceIDRatioBased(0)))
harness.TestTracer(func() trace.Tracer {
return tp.Tracer("")
})
}
type testExporter struct {
mu sync.RWMutex
idx map[string]int
spans []*export.SpanSnapshot
}
func NewTestExporter() *testExporter {
return &testExporter{idx: make(map[string]int)}
}
func (te *testExporter) ExportSpans(_ context.Context, ss []*export.SpanSnapshot) error {
te.mu.Lock()
defer te.mu.Unlock()
i := len(te.spans)
for _, s := range ss {
te.idx[s.Name] = i
te.spans = append(te.spans, s)
i++
}
return nil
}
func (te *testExporter) Spans() []*export.SpanSnapshot {
te.mu.RLock()
defer te.mu.RUnlock()
cp := make([]*export.SpanSnapshot, len(te.spans))
copy(cp, te.spans)
return cp
}
func (te *testExporter) GetSpan(name string) (*export.SpanSnapshot, bool) {
te.mu.RLock()
defer te.mu.RUnlock()
i, ok := te.idx[name]
if !ok {
return nil, false
}
return te.spans[i], true
}
func (te *testExporter) Len() int {
te.mu.RLock()
defer te.mu.RUnlock()
return len(te.spans)
}
func (te *testExporter) Shutdown(context.Context) error {
te.Reset()
return nil
}
func (te *testExporter) Reset() {
te.mu.Lock()
defer te.mu.Unlock()
te.idx = make(map[string]int)
te.spans = te.spans[:0]
}
type testSampler struct {
callCount int
prefix string
t *testing.T
}
func (ts *testSampler) ShouldSample(p SamplingParameters) SamplingResult {
ts.callCount++
ts.t.Logf("called sampler for name %q", p.Name)
decision := Drop
if strings.HasPrefix(p.Name, ts.prefix) {
decision = RecordAndSample
}
return SamplingResult{Decision: decision, Attributes: []attribute.KeyValue{attribute.Int("callCount", ts.callCount)}}
}
func (ts testSampler) Description() string {
return "testSampler"
}
func TestSetName(t *testing.T) {
tp := NewTracerProvider()
type testCase struct {
name string
newName string
}
for idx, tt := range []testCase{
{ // 0
name: "foobar",
newName: "foobaz",
},
{ // 1
name: "foobar",
newName: "barbaz",
},
{ // 2
name: "barbar",
newName: "barbaz",
},
{ // 3
name: "barbar",
newName: "foobar",
},
} {
sp := startNamedSpan(tp, "SetName", tt.name)
if sdkspan, ok := sp.(*span); ok {
if sdkspan.Name() != tt.name {
t.Errorf("%d: invalid name at span creation, expected %v, got %v", idx, tt.name, sdkspan.Name())
}
} else {
t.Errorf("%d: unable to coerce span to SDK span, is type %T", idx, sp)
}
sp.SetName(tt.newName)
if sdkspan, ok := sp.(*span); ok {
if sdkspan.Name() != tt.newName {
t.Errorf("%d: span name not changed, expected %v, got %v", idx, tt.newName, sdkspan.Name())
}
} else {
t.Errorf("%d: unable to coerce span to SDK span, is type %T", idx, sp)
}
sp.End()
}
}
func TestSpanIsRecording(t *testing.T) {
t.Run("while Span active", func(t *testing.T) {
for name, tc := range map[string]struct {
sampler Sampler
want bool
}{
"Always sample, recording on": {sampler: AlwaysSample(), want: true},
"Never sample recording off": {sampler: NeverSample(), want: false},
} {
tp := NewTracerProvider(WithSampler(tc.sampler))
_, span := tp.Tracer(name).Start(context.Background(), "StartSpan")
defer span.End()
got := span.IsRecording()
assert.Equal(t, got, tc.want, name)
}
})
t.Run("after Span end", func(t *testing.T) {
for name, tc := range map[string]Sampler{
"Always Sample": AlwaysSample(),
"Never Sample": NeverSample(),
} {
tp := NewTracerProvider(WithSampler(tc))
_, span := tp.Tracer(name).Start(context.Background(), "StartSpan")
span.End()
got := span.IsRecording()
assert.False(t, got, name)
}
})
}
func TestSampling(t *testing.T) {
idg := defaultIDGenerator()
const total = 10000
for name, tc := range map[string]struct {
sampler Sampler
expect float64
parent bool
sampledParent bool
}{
// Span w/o a parent
"NeverSample": {sampler: NeverSample(), expect: 0},
"AlwaysSample": {sampler: AlwaysSample(), expect: 1.0},
"TraceIdRatioBased_-1": {sampler: TraceIDRatioBased(-1.0), expect: 0},
"TraceIdRatioBased_.25": {sampler: TraceIDRatioBased(0.25), expect: .25},
"TraceIdRatioBased_.50": {sampler: TraceIDRatioBased(0.50), expect: .5},
"TraceIdRatioBased_.75": {sampler: TraceIDRatioBased(0.75), expect: .75},
"TraceIdRatioBased_2.0": {sampler: TraceIDRatioBased(2.0), expect: 1},
// Spans w/o a parent and using ParentBased(DelegateSampler()) Sampler, receive DelegateSampler's sampling decision
"ParentNeverSample": {sampler: ParentBased(NeverSample()), expect: 0},
"ParentAlwaysSample": {sampler: ParentBased(AlwaysSample()), expect: 1},
"ParentTraceIdRatioBased_.50": {sampler: ParentBased(TraceIDRatioBased(0.50)), expect: .5},
// An unadorned TraceIDRatioBased sampler ignores parent spans
"UnsampledParentSpanWithTraceIdRatioBased_.25": {sampler: TraceIDRatioBased(0.25), expect: .25, parent: true},
"SampledParentSpanWithTraceIdRatioBased_.25": {sampler: TraceIDRatioBased(0.25), expect: .25, parent: true, sampledParent: true},
"UnsampledParentSpanWithTraceIdRatioBased_.50": {sampler: TraceIDRatioBased(0.50), expect: .5, parent: true},
"SampledParentSpanWithTraceIdRatioBased_.50": {sampler: TraceIDRatioBased(0.50), expect: .5, parent: true, sampledParent: true},
"UnsampledParentSpanWithTraceIdRatioBased_.75": {sampler: TraceIDRatioBased(0.75), expect: .75, parent: true},
"SampledParentSpanWithTraceIdRatioBased_.75": {sampler: TraceIDRatioBased(0.75), expect: .75, parent: true, sampledParent: true},
// Spans with a sampled parent but using NeverSample Sampler, are not sampled
"SampledParentSpanWithNeverSample": {sampler: NeverSample(), expect: 0, parent: true, sampledParent: true},
// Spans with a sampled parent and using ParentBased(DelegateSampler()) Sampler, inherit the parent span's sampling status
"SampledParentSpanWithParentNeverSample": {sampler: ParentBased(NeverSample()), expect: 1, parent: true, sampledParent: true},
"UnsampledParentSpanWithParentNeverSampler": {sampler: ParentBased(NeverSample()), expect: 0, parent: true, sampledParent: false},
"SampledParentSpanWithParentAlwaysSampler": {sampler: ParentBased(AlwaysSample()), expect: 1, parent: true, sampledParent: true},
"UnsampledParentSpanWithParentAlwaysSampler": {sampler: ParentBased(AlwaysSample()), expect: 0, parent: true, sampledParent: false},
"SampledParentSpanWithParentTraceIdRatioBased_.50": {sampler: ParentBased(TraceIDRatioBased(0.50)), expect: 1, parent: true, sampledParent: true},
"UnsampledParentSpanWithParentTraceIdRatioBased_.50": {sampler: ParentBased(TraceIDRatioBased(0.50)), expect: 0, parent: true, sampledParent: false},
} {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
p := NewTracerProvider(WithSampler(tc.sampler))
tr := p.Tracer("test")
var sampled int
for i := 0; i < total; i++ {
ctx := context.Background()
if tc.parent {
tid, sid := idg.NewIDs(ctx)
psc := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
SpanID: sid,
})
if tc.sampledParent {
psc = psc.WithTraceFlags(trace.FlagsSampled)
}
ctx = trace.ContextWithRemoteSpanContext(ctx, psc)
}
_, span := tr.Start(ctx, "test")
if span.SpanContext().IsSampled() {
sampled++
}
}
tolerance := 0.0
got := float64(sampled) / float64(total)
if tc.expect > 0 && tc.expect < 1 {
// See https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval
const z = 4.75342 // This should succeed 99.9999% of the time
tolerance = z * math.Sqrt(got*(1-got)/total)
}
diff := math.Abs(got - tc.expect)
if diff > tolerance {
t.Errorf("got %f (diff: %f), expected %f (w/tolerance: %f)", got, diff, tc.expect, tolerance)
}
})
}
}
func TestStartSpanWithParent(t *testing.T) {
tp := NewTracerProvider()
tr := tp.Tracer("SpanWithParent")
ctx := context.Background()
_, s1 := tr.Start(trace.ContextWithRemoteSpanContext(ctx, sc), "span1-unsampled-parent1")
if err := checkChild(t, sc, s1); err != nil {
t.Error(err)
}
_, s2 := tr.Start(trace.ContextWithRemoteSpanContext(ctx, sc), "span2-unsampled-parent1")
if err := checkChild(t, sc, s2); err != nil {
t.Error(err)
}
ts, err := trace.TraceStateFromKeyValues(attribute.String("k", "v"))
if err != nil {
t.Error(err)
}
sc2 := sc.WithTraceState(ts)
_, s3 := tr.Start(trace.ContextWithRemoteSpanContext(ctx, sc2), "span3-sampled-parent2")
if err := checkChild(t, sc2, s3); err != nil {
t.Error(err)
}
ctx2, s4 := tr.Start(trace.ContextWithRemoteSpanContext(ctx, sc2), "span4-sampled-parent2")
if err := checkChild(t, sc2, s4); err != nil {
t.Error(err)
}
s4Sc := s4.SpanContext()
_, s5 := tr.Start(ctx2, "span5-implicit-childof-span4")
if err := checkChild(t, s4Sc, s5); err != nil {
t.Error(err)
}
}
func TestSetSpanAttributesOnStart(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
span := startSpan(tp,
"StartSpanAttribute",
trace.WithAttributes(attribute.String("key1", "value1")),
trace.WithAttributes(attribute.String("key2", "value2")),
)
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
Attributes: []attribute.KeyValue{
attribute.String("key1", "value1"),
attribute.String("key2", "value2"),
},
SpanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "StartSpanAttribute"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanAttributesOnStart: -got +want %s", diff)
}
}
func TestSetSpanAttributes(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
span := startSpan(tp, "SpanAttribute")
span.SetAttributes(attribute.String("key1", "value1"))
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
Attributes: []attribute.KeyValue{
attribute.String("key1", "value1"),
},
SpanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "SpanAttribute"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanAttributes: -got +want %s", diff)
}
}
func TestSamplerAttributesLocalChildSpan(t *testing.T) {
sampler := &testSampler{prefix: "span", t: t}
te := NewTestExporter()
tp := NewTracerProvider(WithSampler(sampler), WithSyncer(te), WithResource(resource.Empty()))
ctx := context.Background()
ctx, span := startLocalSpan(tp, ctx, "SpanOne", "span0")
_, spanTwo := startLocalSpan(tp, ctx, "SpanTwo", "span1")
spanTwo.End()
span.End()
got := te.Spans()
require.Len(t, got, 2)
// FILO order above means spanTwo <-> gotSpan0 and span <-> gotSpan1.
gotSpan0, gotSpan1 := got[0], got[1]
// Ensure sampler is called for local child spans by verifying the
// attributes set by the sampler are set on the child span.
assert.Equal(t, []attribute.KeyValue{attribute.Int("callCount", 2)}, gotSpan0.Attributes)
assert.Equal(t, []attribute.KeyValue{attribute.Int("callCount", 1)}, gotSpan1.Attributes)
}
func TestSetSpanAttributesOverLimit(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSpanLimits(SpanLimits{AttributeCountLimit: 2}), WithSyncer(te), WithResource(resource.Empty()))
span := startSpan(tp, "SpanAttributesOverLimit")
span.SetAttributes(
attribute.Bool("key1", true),
attribute.String("key2", "value2"),
attribute.Bool("key1", false), // Replace key1.
attribute.Int64("key4", 4), // Remove key2 and add key4
)
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
Attributes: []attribute.KeyValue{
attribute.Bool("key1", false),
attribute.Int64("key4", 4),
},
SpanKind: trace.SpanKindInternal,
DroppedAttributeCount: 1,
InstrumentationLibrary: instrumentation.Library{Name: "SpanAttributesOverLimit"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanAttributesOverLimit: -got +want %s", diff)
}
}
func TestSetSpanAttributesWithInvalidKey(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSpanLimits(SpanLimits{}), WithSyncer(te), WithResource(resource.Empty()))
span := startSpan(tp, "SpanToSetInvalidKeyOrValue")
span.SetAttributes(
attribute.Bool("", true),
attribute.Bool("key1", false),
)
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
Attributes: []attribute.KeyValue{
attribute.Bool("key1", false),
},
SpanKind: trace.SpanKindInternal,
DroppedAttributeCount: 0,
InstrumentationLibrary: instrumentation.Library{Name: "SpanToSetInvalidKeyOrValue"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanAttributesWithInvalidKey: -got +want %s", diff)
}
}
func TestEvents(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
span := startSpan(tp, "Events")
k1v1 := attribute.String("key1", "value1")
k2v2 := attribute.Bool("key2", true)
k3v3 := attribute.Int64("key3", 3)
span.AddEvent("foo", trace.WithAttributes(attribute.String("key1", "value1")))
span.AddEvent("bar", trace.WithAttributes(
attribute.Bool("key2", true),
attribute.Int64("key3", 3),
))
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
for i := range got.MessageEvents {
if !checkTime(&got.MessageEvents[i].Time) {
t.Error("exporting span: expected nonzero Event Time")
}
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
MessageEvents: []trace.Event{
{Name: "foo", Attributes: []attribute.KeyValue{k1v1}},
{Name: "bar", Attributes: []attribute.KeyValue{k2v2, k3v3}},
},
SpanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "Events"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("Message Events: -got +want %s", diff)
}
}
func TestEventsOverLimit(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSpanLimits(SpanLimits{EventCountLimit: 2}), WithSyncer(te), WithResource(resource.Empty()))
span := startSpan(tp, "EventsOverLimit")
k1v1 := attribute.String("key1", "value1")
k2v2 := attribute.Bool("key2", false)
k3v3 := attribute.String("key3", "value3")
span.AddEvent("fooDrop", trace.WithAttributes(attribute.String("key1", "value1")))
span.AddEvent("barDrop", trace.WithAttributes(
attribute.Bool("key2", true),
attribute.String("key3", "value3"),
))
span.AddEvent("foo", trace.WithAttributes(attribute.String("key1", "value1")))
span.AddEvent("bar", trace.WithAttributes(
attribute.Bool("key2", false),
attribute.String("key3", "value3"),
))
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
for i := range got.MessageEvents {
if !checkTime(&got.MessageEvents[i].Time) {
t.Error("exporting span: expected nonzero Event Time")
}
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
MessageEvents: []trace.Event{
{Name: "foo", Attributes: []attribute.KeyValue{k1v1}},
{Name: "bar", Attributes: []attribute.KeyValue{k2v2, k3v3}},
},
DroppedMessageEventCount: 2,
SpanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "EventsOverLimit"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("Message Event over limit: -got +want %s", diff)
}
}
func TestLinks(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
k1v1 := attribute.String("key1", "value1")
k2v2 := attribute.String("key2", "value2")
k3v3 := attribute.String("key3", "value3")
sc1 := trace.NewSpanContext(trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}})
sc2 := trace.NewSpanContext(trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}})
links := []trace.Link{
{SpanContext: sc1, Attributes: []attribute.KeyValue{k1v1}},
{SpanContext: sc2, Attributes: []attribute.KeyValue{k2v2, k3v3}},
}
span := startSpan(tp, "Links", trace.WithLinks(links...))
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
Links: links,
SpanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "Links"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("Link: -got +want %s", diff)
}
}
func TestLinksOverLimit(t *testing.T) {
te := NewTestExporter()
sc1 := trace.NewSpanContext(trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}})
sc2 := trace.NewSpanContext(trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}})
sc3 := trace.NewSpanContext(trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}})
tp := NewTracerProvider(WithSpanLimits(SpanLimits{LinkCountLimit: 2}), WithSyncer(te), WithResource(resource.Empty()))
span := startSpan(tp, "LinksOverLimit",
trace.WithLinks(
trace.Link{SpanContext: sc1, Attributes: []attribute.KeyValue{attribute.String("key1", "value1")}},
trace.Link{SpanContext: sc2, Attributes: []attribute.KeyValue{attribute.String("key2", "value2")}},
trace.Link{SpanContext: sc3, Attributes: []attribute.KeyValue{attribute.String("key3", "value3")}},
),
)
k2v2 := attribute.String("key2", "value2")
k3v3 := attribute.String("key3", "value3")
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
Links: []trace.Link{
{SpanContext: sc2, Attributes: []attribute.KeyValue{k2v2}},
{SpanContext: sc3, Attributes: []attribute.KeyValue{k3v3}},
},
DroppedLinkCount: 1,
SpanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "LinksOverLimit"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("Link over limit: -got +want %s", diff)
}
}
func TestSetSpanName(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
ctx := context.Background()
want := "SpanName-1"
ctx = trace.ContextWithRemoteSpanContext(ctx, sc)
_, span := tp.Tracer("SetSpanName").Start(ctx, "SpanName-1")
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
if got.Name != want {
t.Errorf("span.Name: got %q; want %q", got.Name, want)
}
}
func TestSetSpanStatus(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
span := startSpan(tp, "SpanStatus")
span.SetStatus(codes.Error, "Error")
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
SpanKind: trace.SpanKindInternal,
StatusCode: codes.Error,
StatusMessage: "Error",
InstrumentationLibrary: instrumentation.Library{Name: "SpanStatus"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanStatus: -got +want %s", diff)
}
}
func TestSetSpanStatusWithoutMessageWhenStatusIsNotError(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
span := startSpan(tp, "SpanStatus")
span.SetStatus(codes.Ok, "This message will be ignored")
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
SpanKind: trace.SpanKindInternal,
StatusCode: codes.Ok,
StatusMessage: "",
InstrumentationLibrary: instrumentation.Library{Name: "SpanStatus"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanStatus: -got +want %s", diff)
}
}
func cmpDiff(x, y interface{}) string {
return cmp.Diff(x, y,
cmp.AllowUnexported(attribute.Value{}),
cmp.AllowUnexported(trace.Event{}),
cmp.AllowUnexported(trace.TraceState{}))
}
// checkChild is test utility function that tests that c has fields set appropriately,
// given that it is a child span of p.
func checkChild(t *testing.T, p trace.SpanContext, apiSpan trace.Span) error {
s := apiSpan.(*span)
if s == nil {
return fmt.Errorf("got nil child span, want non-nil")
}
if got, want := s.spanContext.TraceID().String(), p.TraceID().String(); got != want {
return fmt.Errorf("got child trace ID %s, want %s", got, want)
}
if childID, parentID := s.spanContext.SpanID().String(), p.SpanID().String(); childID == parentID {
return fmt.Errorf("got child span ID %s, parent span ID %s; want unequal IDs", childID, parentID)
}
if got, want := s.spanContext.TraceFlags(), p.TraceFlags(); got != want {
return fmt.Errorf("got child trace options %d, want %d", got, want)
}
got, want := s.spanContext.TraceState(), p.TraceState()
assert.Equal(t, want, got)
return nil
}
// startSpan starts a span with a name "span0". See startNamedSpan for
// details.
func startSpan(tp *TracerProvider, trName string, args ...trace.SpanOption) trace.Span {
return startNamedSpan(tp, trName, "span0", args...)
}
// startNamed Span is a test utility func that starts a span with a
// passed name and with remote span context as parent. The remote span
// context contains TraceFlags with sampled bit set. This allows the
// span to be automatically sampled.
func startNamedSpan(tp *TracerProvider, trName, name string, args ...trace.SpanOption) trace.Span {
_, span := tp.Tracer(trName).Start(
trace.ContextWithRemoteSpanContext(context.Background(), sc),
name,
args...,
)
return span
}
// startLocalSpan is a test utility func that starts a span with a
// passed name and with the passed context. The context is returned
// along with the span so this parent can be used to create child
// spans.
func startLocalSpan(tp *TracerProvider, ctx context.Context, trName, name string, args ...trace.SpanOption) (context.Context, trace.Span) {
ctx, span := tp.Tracer(trName).Start(
ctx,
name,
args...,
)
return ctx, span
}
// endSpan is a test utility function that ends the span in the context and
// returns the exported export.SpanSnapshot.
// It requires that span be sampled using one of these methods
// 1. Passing parent span context in context
// 2. Use WithSampler(AlwaysSample())
// 3. Configuring AlwaysSample() as default sampler
//
// It also does some basic tests on the span.
// It also clears spanID in the export.SpanSnapshot to make the comparison
// easier.
func endSpan(te *testExporter, span trace.Span) (*export.SpanSnapshot, error) {
if !span.IsRecording() {
return nil, fmt.Errorf("IsRecording: got false, want true")
}
if !span.SpanContext().IsSampled() {
return nil, fmt.Errorf("IsSampled: got false, want true")
}
span.End()
if te.Len() != 1 {
return nil, fmt.Errorf("got %d exported spans, want one span", te.Len())
}
got := te.Spans()[0]
if !got.SpanContext.SpanID().IsValid() {
return nil, fmt.Errorf("exporting span: expected nonzero SpanID")
}
got.SpanContext = got.SpanContext.WithSpanID(trace.SpanID{})
if !checkTime(&got.StartTime) {
return nil, fmt.Errorf("exporting span: expected nonzero StartTime")
}
if !checkTime(&got.EndTime) {
return nil, fmt.Errorf("exporting span: expected nonzero EndTime")
}
return got, nil
}
// checkTime checks that a nonzero time was set in x, then clears it.
func checkTime(x *time.Time) bool {
if x.IsZero() {
return false
}
*x = time.Time{}
return true
}
func TestEndSpanTwice(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te))
st := time.Now()
et1 := st.Add(100 * time.Millisecond)
et2 := st.Add(200 * time.Millisecond)
span := startSpan(tp, "EndSpanTwice", trace.WithTimestamp(st))
span.End(trace.WithTimestamp(et1))
span.End(trace.WithTimestamp(et2))
if te.Len() != 1 {
t.Fatalf("expected only a single span, got %#v", te.Spans())
}
ro := span.(ReadOnlySpan)
if ro.EndTime() != et1 {
t.Fatalf("2nd call to End() should not modify end time")
}
}
func TestStartSpanAfterEnd(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSampler(AlwaysSample()), WithSyncer(te))
ctx := context.Background()
tr := tp.Tracer("SpanAfterEnd")
ctx, span0 := tr.Start(trace.ContextWithRemoteSpanContext(ctx, sc), "parent")
ctx1, span1 := tr.Start(ctx, "span-1")
span1.End()
// Start a new span with the context containing span-1
// even though span-1 is ended, we still add this as a new child of span-1
_, span2 := tr.Start(ctx1, "span-2")
span2.End()
span0.End()
if got, want := te.Len(), 3; got != want {
t.Fatalf("len(%#v) = %d; want %d", te.Spans(), got, want)
}
gotParent, ok := te.GetSpan("parent")
if !ok {
t.Fatal("parent not recorded")
}
gotSpan1, ok := te.GetSpan("span-1")
if !ok {
t.Fatal("span-1 not recorded")
}
gotSpan2, ok := te.GetSpan("span-2")
if !ok {
t.Fatal("span-2 not recorded")
}
if got, want := gotSpan1.SpanContext.TraceID(), gotParent.SpanContext.TraceID(); got != want {
t.Errorf("span-1.TraceID=%q; want %q", got, want)
}
if got, want := gotSpan2.SpanContext.TraceID(), gotParent.SpanContext.TraceID(); got != want {
t.Errorf("span-2.TraceID=%q; want %q", got, want)
}
if got, want := gotSpan1.Parent.SpanID(), gotParent.SpanContext.SpanID(); got != want {
t.Errorf("span-1.ParentSpanID=%q; want %q (parent.SpanID)", got, want)
}
if got, want := gotSpan2.Parent.SpanID(), gotSpan1.SpanContext.SpanID(); got != want {
t.Errorf("span-2.ParentSpanID=%q; want %q (span1.SpanID)", got, want)
}
}
func TestChildSpanCount(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSampler(AlwaysSample()), WithSyncer(te))
tr := tp.Tracer("ChidSpanCount")
ctx, span0 := tr.Start(context.Background(), "parent")
ctx1, span1 := tr.Start(ctx, "span-1")
_, span2 := tr.Start(ctx1, "span-2")
span2.End()
span1.End()
_, span3 := tr.Start(ctx, "span-3")
span3.End()
span0.End()
if got, want := te.Len(), 4; got != want {
t.Fatalf("len(%#v) = %d; want %d", te.Spans(), got, want)
}
gotParent, ok := te.GetSpan("parent")
if !ok {
t.Fatal("parent not recorded")
}
gotSpan1, ok := te.GetSpan("span-1")
if !ok {
t.Fatal("span-1 not recorded")
}
gotSpan2, ok := te.GetSpan("span-2")
if !ok {
t.Fatal("span-2 not recorded")
}
gotSpan3, ok := te.GetSpan("span-3")
if !ok {
t.Fatal("span-3 not recorded")
}
if got, want := gotSpan3.ChildSpanCount, 0; got != want {
t.Errorf("span-3.ChildSpanCount=%d; want %d", got, want)
}
if got, want := gotSpan2.ChildSpanCount, 0; got != want {
t.Errorf("span-2.ChildSpanCount=%d; want %d", got, want)
}
if got, want := gotSpan1.ChildSpanCount, 1; got != want {
t.Errorf("span-1.ChildSpanCount=%d; want %d", got, want)
}
if got, want := gotParent.ChildSpanCount, 2; got != want {
t.Errorf("parent.ChildSpanCount=%d; want %d", got, want)
}
}
func TestNilSpanEnd(t *testing.T) {
var span *span
span.End()
}
func TestExecutionTracerTaskEnd(t *testing.T) {
var n uint64
tp := NewTracerProvider(WithSampler(NeverSample()))
tr := tp.Tracer("Execution Tracer Task End")
executionTracerTaskEnd := func() {
atomic.AddUint64(&n, 1)
}
var spans []*span
_, apiSpan := tr.Start(context.Background(), "foo")
s := apiSpan.(*span)
s.executionTracerTaskEnd = executionTracerTaskEnd
spans = append(spans, s) // never sample
tID, _ := trace.TraceIDFromHex("0102030405060708090a0b0c0d0e0f")
sID, _ := trace.SpanIDFromHex("0001020304050607")
ctx := context.Background()
ctx = trace.ContextWithRemoteSpanContext(ctx,
trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tID,
SpanID: sID,
TraceFlags: 0,
}),
)
_, apiSpan = tr.Start(
ctx,
"foo",
)
s = apiSpan.(*span)
s.executionTracerTaskEnd = executionTracerTaskEnd
spans = append(spans, s) // parent not sampled
tp.sampler = AlwaysSample()
_, apiSpan = tr.Start(context.Background(), "foo")
s = apiSpan.(*span)
s.executionTracerTaskEnd = executionTracerTaskEnd
spans = append(spans, s) // always sample
for _, span := range spans {
span.End()
}
// Only one span should be sampled meaning only one execution of
// executionTracerTaskEnd.
if got, want := n, uint64(1); got != want {
t.Fatalf("Execution tracer task ended for %v spans; want %v", got, want)
}
}
func TestCustomStartEndTime(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithSampler(AlwaysSample()))
startTime := time.Date(2019, time.August, 27, 14, 42, 0, 0, time.UTC)
endTime := startTime.Add(time.Second * 20)
_, span := tp.Tracer("Custom Start and End time").Start(
context.Background(),
"testspan",
trace.WithTimestamp(startTime),
)
span.End(trace.WithTimestamp(endTime))
if te.Len() != 1 {
t.Fatalf("got %d exported spans, want one span", te.Len())
}
got := te.Spans()[0]
if got.StartTime != startTime {
t.Errorf("expected start time to be %s, got %s", startTime, got.StartTime)
}
if got.EndTime != endTime {
t.Errorf("expected end time to be %s, got %s", endTime, got.EndTime)
}
}
func TestRecordError(t *testing.T) {
scenarios := []struct {
err error
typ string
msg string
}{
{
err: ottest.NewTestError("test error"),
typ: "go.opentelemetry.io/otel/internal/internaltest.TestError",
msg: "test error",
},
{
err: errors.New("test error 2"),
typ: "*errors.errorString",
msg: "test error 2",
},
}
for _, s := range scenarios {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
span := startSpan(tp, "RecordError")
errTime := time.Now()
span.RecordError(s.err, trace.WithTimestamp(errTime))
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
StatusCode: codes.Unset,
SpanKind: trace.SpanKindInternal,
MessageEvents: []trace.Event{
{
Name: semconv.ExceptionEventName,
Time: errTime,
Attributes: []attribute.KeyValue{
semconv.ExceptionTypeKey.String(s.typ),
semconv.ExceptionMessageKey.String(s.msg),
},
},
},
InstrumentationLibrary: instrumentation.Library{Name: "RecordError"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SpanErrorOptions: -got +want %s", diff)
}
}
}
func TestRecordErrorNil(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
span := startSpan(tp, "RecordErrorNil")
span.RecordError(nil)
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
SpanKind: trace.SpanKindInternal,
StatusCode: codes.Unset,
StatusMessage: "",
InstrumentationLibrary: instrumentation.Library{Name: "RecordErrorNil"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SpanErrorOptions: -got +want %s", diff)
}
}
func TestWithSpanKind(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithSampler(AlwaysSample()), WithResource(resource.Empty()))
tr := tp.Tracer("withSpanKind")
_, span := tr.Start(context.Background(), "WithoutSpanKind")
spanData, err := endSpan(te, span)
if err != nil {
t.Error(err.Error())
}
if spanData.SpanKind != trace.SpanKindInternal {
t.Errorf("Default value of Spankind should be Internal: got %+v, want %+v\n", spanData.SpanKind, trace.SpanKindInternal)
}
sks := []trace.SpanKind{
trace.SpanKindInternal,
trace.SpanKindServer,
trace.SpanKindClient,
trace.SpanKindProducer,
trace.SpanKindConsumer,
}
for _, sk := range sks {
te.Reset()
_, span := tr.Start(context.Background(), fmt.Sprintf("SpanKind-%v", sk), trace.WithSpanKind(sk))
spanData, err := endSpan(te, span)
if err != nil {
t.Error(err.Error())
}
if spanData.SpanKind != sk {
t.Errorf("WithSpanKind check: got %+v, want %+v\n", spanData.SpanKind, sks)
}
}
}
func TestWithResource(t *testing.T) {
cases := []struct {
name string
options []TracerProviderOption
want *resource.Resource
msg string
}{
{
name: "explicitly empty resource",
options: []TracerProviderOption{WithResource(resource.Empty())},
want: resource.Empty(),
},
{
name: "uses default if no resource option",
options: []TracerProviderOption{},
want: resource.Default(),
},
{
name: "explicit resource",
options: []TracerProviderOption{WithResource(resource.NewWithAttributes(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)))},
want: resource.NewWithAttributes(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)),
},
{
name: "last resource wins",
options: []TracerProviderOption{
WithResource(resource.NewWithAttributes(attribute.String("rk1", "vk1"), attribute.Int64("rk2", 5))),
WithResource(resource.NewWithAttributes(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10)))},
want: resource.NewWithAttributes(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10)),
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
te := NewTestExporter()
defaultOptions := []TracerProviderOption{WithSyncer(te), WithSampler(AlwaysSample())}
tp := NewTracerProvider(append(defaultOptions, tc.options...)...)
span := startSpan(tp, "WithResource")
span.SetAttributes(attribute.String("key1", "value1"))
got, err := endSpan(te, span)
if err != nil {
t.Error(err.Error())
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
Attributes: []attribute.KeyValue{
attribute.String("key1", "value1"),
},
SpanKind: trace.SpanKindInternal,
Resource: tc.want,
InstrumentationLibrary: instrumentation.Library{Name: "WithResource"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("WithResource:\n -got +want %s", diff)
}
})
}
}
func TestWithInstrumentationVersion(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
ctx := context.Background()
ctx = trace.ContextWithRemoteSpanContext(ctx, sc)
_, span := tp.Tracer(
"WithInstrumentationVersion",
trace.WithInstrumentationVersion("v0.1.0"),
).Start(ctx, "span0")
got, err := endSpan(te, span)
if err != nil {
t.Error(err.Error())
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
SpanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{
Name: "WithInstrumentationVersion",
Version: "v0.1.0",
},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("WithResource:\n -got +want %s", diff)
}
}
func TestSpanCapturesPanic(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
_, span := tp.Tracer("CatchPanic").Start(
context.Background(),
"span",
)
f := func() {
defer span.End()
panic(errors.New("error message"))
}
require.PanicsWithError(t, "error message", f)
spans := te.Spans()
require.Len(t, spans, 1)
require.Len(t, spans[0].MessageEvents, 1)
assert.Equal(t, spans[0].MessageEvents[0].Name, semconv.ExceptionEventName)
assert.Equal(t, spans[0].MessageEvents[0].Attributes, []attribute.KeyValue{
semconv.ExceptionTypeKey.String("*errors.errorString"),
semconv.ExceptionMessageKey.String("error message"),
})
}
func TestReadOnlySpan(t *testing.T) {
kv := attribute.String("foo", "bar")
tp := NewTracerProvider(WithResource(resource.NewWithAttributes(kv)))
tr := tp.Tracer("ReadOnlySpan", trace.WithInstrumentationVersion("3"))
// Initialize parent context.
tID, sID := tp.idGenerator.NewIDs(context.Background())
parent := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tID,
SpanID: sID,
TraceFlags: 0x1,
Remote: true,
})
ctx := trace.ContextWithRemoteSpanContext(context.Background(), parent)
// Initialize linked context.
tID, sID = tp.idGenerator.NewIDs(context.Background())
linked := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tID,
SpanID: sID,
TraceFlags: 0x1,
})
st := time.Now()
ctx, span := tr.Start(ctx, "foo", trace.WithTimestamp(st),
trace.WithLinks(trace.Link{SpanContext: linked}))
span.SetAttributes(kv)
span.AddEvent("foo", trace.WithAttributes(kv))
span.SetStatus(codes.Ok, "foo")
// Verify span implements ReadOnlySpan.
ro, ok := span.(ReadOnlySpan)
require.True(t, ok)
assert.Equal(t, "foo", ro.Name())
assert.Equal(t, trace.SpanContextFromContext(ctx), ro.SpanContext())
assert.Equal(t, parent, ro.Parent())
assert.Equal(t, trace.SpanKindInternal, ro.SpanKind())
assert.Equal(t, st, ro.StartTime())
assert.True(t, ro.EndTime().IsZero())
assert.Equal(t, kv.Key, ro.Attributes()[0].Key)
assert.Equal(t, kv.Value, ro.Attributes()[0].Value)
assert.Equal(t, linked, ro.Links()[0].SpanContext)
assert.Equal(t, kv.Key, ro.Events()[0].Attributes[0].Key)
assert.Equal(t, kv.Value, ro.Events()[0].Attributes[0].Value)
assert.Equal(t, codes.Ok, ro.StatusCode())
assert.Equal(t, "", ro.StatusMessage())
assert.Equal(t, "ReadOnlySpan", ro.InstrumentationLibrary().Name)
assert.Equal(t, "3", ro.InstrumentationLibrary().Version)
assert.Equal(t, kv.Key, ro.Resource().Attributes()[0].Key)
assert.Equal(t, kv.Value, ro.Resource().Attributes()[0].Value)
// Verify changes to the original span are reflected in the ReadOnlySpan.
span.SetName("bar")
assert.Equal(t, "bar", ro.Name())
// Verify Snapshot() returns snapshots that are independent from the
// original span and from one another.
d1 := ro.Snapshot()
span.AddEvent("baz")
d2 := ro.Snapshot()
for _, e := range d1.MessageEvents {
if e.Name == "baz" {
t.Errorf("Didn't expect to find 'baz' event")
}
}
var exists bool
for _, e := range d2.MessageEvents {
if e.Name == "baz" {
exists = true
}
}
if !exists {
t.Errorf("Expected to find 'baz' event")
}
et := st.Add(time.Millisecond)
span.End(trace.WithTimestamp(et))
assert.Equal(t, et, ro.EndTime())
}
func TestReadWriteSpan(t *testing.T) {
tp := NewTracerProvider(WithResource(resource.Empty()))
tr := tp.Tracer("ReadWriteSpan")
// Initialize parent context.
tID, sID := tp.idGenerator.NewIDs(context.Background())
parent := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tID,
SpanID: sID,
TraceFlags: 0x1,
})
ctx := trace.ContextWithRemoteSpanContext(context.Background(), parent)
_, span := tr.Start(ctx, "foo")
defer span.End()
// Verify span implements ReadOnlySpan.
rw, ok := span.(ReadWriteSpan)
require.True(t, ok)
// Verify the span can be read from.
assert.False(t, rw.StartTime().IsZero())
// Verify the span can be written to.
rw.SetName("bar")
assert.Equal(t, "bar", rw.Name())
// NOTE: This function tests ReadWriteSpan which is an interface which
// embeds trace.Span and ReadOnlySpan. Since both of these interfaces have
// their own tests, there is no point in testing all the possible methods
// available via ReadWriteSpan as doing so would mean creating a lot of
// duplication.
}
func TestAddEventsWithMoreAttributesThanLimit(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(
WithSpanLimits(SpanLimits{AttributePerEventCountLimit: 2}),
WithSyncer(te),
WithResource(resource.Empty()),
)
span := startSpan(tp, "AddSpanEventWithOverLimitedAttributes")
span.AddEvent("test1", trace.WithAttributes(
attribute.Bool("key1", true),
attribute.String("key2", "value2"),
))
// Parts of the attribute should be discard
span.AddEvent("test2", trace.WithAttributes(
attribute.Bool("key1", true),
attribute.String("key2", "value2"),
attribute.String("key3", "value3"),
attribute.String("key4", "value4"),
))
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
for i := range got.MessageEvents {
if !checkTime(&got.MessageEvents[i].Time) {
t.Error("exporting span: expected nonzero Event Time")
}
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
Attributes: nil,
MessageEvents: []trace.Event{
{
Name: "test1",
Attributes: []attribute.KeyValue{
attribute.Bool("key1", true),
attribute.String("key2", "value2"),
},
},
{
Name: "test2",
Attributes: []attribute.KeyValue{
attribute.Bool("key1", true),
attribute.String("key2", "value2"),
},
},
},
SpanKind: trace.SpanKindInternal,
DroppedAttributeCount: 2,
InstrumentationLibrary: instrumentation.Library{Name: "AddSpanEventWithOverLimitedAttributes"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanAttributesOverLimit: -got +want %s", diff)
}
}
func TestAddLinksWithMoreAttributesThanLimit(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(
WithSpanLimits(SpanLimits{AttributePerLinkCountLimit: 1}),
WithSyncer(te),
WithResource(resource.Empty()),
)
k1v1 := attribute.String("key1", "value1")
k2v2 := attribute.String("key2", "value2")
k3v3 := attribute.String("key3", "value3")
k4v4 := attribute.String("key4", "value4")
sc1 := trace.NewSpanContext(trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}})
sc2 := trace.NewSpanContext(trace.SpanContextConfig{TraceID: trace.TraceID([16]byte{1, 1}), SpanID: trace.SpanID{3}})
span := startSpan(tp, "Links", trace.WithLinks([]trace.Link{
{SpanContext: sc1, Attributes: []attribute.KeyValue{k1v1, k2v2}},
{SpanContext: sc2, Attributes: []attribute.KeyValue{k2v2, k3v3, k4v4}},
}...))
got, err := endSpan(te, span)
if err != nil {
t.Fatal(err)
}
want := &export.SpanSnapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: 0x1,
}),
Parent: sc.WithRemote(true),
Name: "span0",
Links: []trace.Link{
{SpanContext: sc1, Attributes: []attribute.KeyValue{k1v1}},
{SpanContext: sc2, Attributes: []attribute.KeyValue{k2v2}},
},
DroppedAttributeCount: 3,
SpanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "Links"},
}
if diff := cmpDiff(got, want); diff != "" {
t.Errorf("Link: -got +want %s", diff)
}
}
type stateSampler struct {
prefix string
f func(trace.TraceState) trace.TraceState
}
func (s *stateSampler) ShouldSample(p SamplingParameters) SamplingResult {
decision := Drop
if strings.HasPrefix(p.Name, s.prefix) {
decision = RecordAndSample
}
ts := s.f(trace.SpanContextFromContext(p.ParentContext).TraceState())
return SamplingResult{Decision: decision, Tracestate: ts}
}
func (s stateSampler) Description() string {
return "stateSampler"
}
// Check that a new span propagates the SamplerResult.TraceState
func TestSamplerTraceState(t *testing.T) {
mustTS := func(t trace.TraceState, err error) trace.TraceState { return t }
makeInserter := func(k attribute.KeyValue, prefix string) Sampler {
return &stateSampler{
prefix: prefix,
f: func(t trace.TraceState) trace.TraceState { return mustTS(t.Insert(k)) },
}
}
makeDeleter := func(k attribute.Key, prefix string) Sampler {
return &stateSampler{
prefix: prefix,
f: func(t trace.TraceState) trace.TraceState { return mustTS(t.Delete(k)) },
}
}
clearer := func(prefix string) Sampler {
return &stateSampler{
prefix: prefix,
f: func(t trace.TraceState) trace.TraceState { return trace.TraceState{} },
}
}
tests := []struct {
name string
sampler Sampler
spanName string
input trace.TraceState
want trace.TraceState
exportSpan bool
}{
{
name: "alwaysOn",
sampler: AlwaysSample(),
input: mustTS(trace.TraceStateFromKeyValues(kv1)),
want: mustTS(trace.TraceStateFromKeyValues(kv1)),
exportSpan: true,
},
{
name: "alwaysOff",
sampler: NeverSample(),
input: mustTS(trace.TraceStateFromKeyValues(kv1)),
want: mustTS(trace.TraceStateFromKeyValues(kv1)),
exportSpan: false,
},
{
name: "insertKeySampled",
sampler: makeInserter(kv2, "span"),
spanName: "span0",
input: mustTS(trace.TraceStateFromKeyValues(kv1)),
want: mustTS(trace.TraceStateFromKeyValues(kv2, kv1)),
exportSpan: true,
},
{
name: "insertKeyDropped",
sampler: makeInserter(kv2, "span"),
spanName: "nospan0",
input: mustTS(trace.TraceStateFromKeyValues(kv1)),
want: mustTS(trace.TraceStateFromKeyValues(kv2, kv1)),
exportSpan: false,
},
{
name: "deleteKeySampled",
sampler: makeDeleter(k1, "span"),
spanName: "span0",
input: mustTS(trace.TraceStateFromKeyValues(kv1, kv2)),
want: mustTS(trace.TraceStateFromKeyValues(kv2)),
exportSpan: true,
},
{
name: "deleteKeyDropped",
sampler: makeDeleter(k1, "span"),
spanName: "nospan0",
input: mustTS(trace.TraceStateFromKeyValues(kv1, kv2, kv3)),
want: mustTS(trace.TraceStateFromKeyValues(kv2, kv3)),
exportSpan: false,
},
{
name: "clearer",
sampler: clearer("span"),
spanName: "span0",
input: mustTS(trace.TraceStateFromKeyValues(kv1, kv3)),
want: mustTS(trace.TraceStateFromKeyValues()),
exportSpan: true,
},
}
for _, ts := range tests {
ts := ts
t.Run(ts.name, func(t *testing.T) {
te := NewTestExporter()
tp := NewTracerProvider(WithSampler(ts.sampler), WithSyncer(te), WithResource(resource.Empty()))
tr := tp.Tracer("TraceState")
sc1 := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
SpanID: sid,
TraceFlags: trace.FlagsSampled,
TraceState: ts.input,
})
ctx := trace.ContextWithRemoteSpanContext(context.Background(), sc1)
_, span := tr.Start(ctx, ts.spanName)
// span's TraceState should be set regardless of Sampled/NonSampled state.
require.Equal(t, ts.want, span.SpanContext().TraceState())
span.End()
got := te.Spans()
if len(got) > 0 != ts.exportSpan {
t.Errorf("unexpected number of exported spans %d", len(got))
}
if len(got) == 0 {
return
}
receivedState := got[0].SpanContext.TraceState()
if diff := cmpDiff(receivedState, ts.want); diff != "" {
t.Errorf("TraceState not propagated: -got +want %s", diff)
}
})
}
}
type testIDGenerator struct {
traceID int
spanID int
}
func (gen *testIDGenerator) NewIDs(ctx context.Context) (trace.TraceID, trace.SpanID) {
traceIDHex := fmt.Sprintf("%032x", gen.traceID)
traceID, _ := trace.TraceIDFromHex(traceIDHex)
gen.traceID++
spanID := gen.NewSpanID(ctx, traceID)
return traceID, spanID
}
func (gen *testIDGenerator) NewSpanID(ctx context.Context, traceID trace.TraceID) trace.SpanID {
spanIDHex := fmt.Sprintf("%016x", gen.spanID)
spanID, _ := trace.SpanIDFromHex(spanIDHex)
gen.spanID++
return spanID
}
var _ IDGenerator = (*testIDGenerator)(nil)
func TestWithIDGenerator(t *testing.T) {
const (
startTraceID = 1
startSpanID = 1
numSpan = 10
)
gen := &testIDGenerator{traceID: startSpanID, spanID: startSpanID}
for i := 0; i < numSpan; i++ {
te := NewTestExporter()
tp := NewTracerProvider(
WithSyncer(te),
WithIDGenerator(gen),
)
span := startSpan(tp, "TestWithIDGenerator")
got, err := strconv.ParseUint(span.SpanContext().SpanID().String(), 16, 64)
require.NoError(t, err)
want := uint64(startSpanID + i)
assert.Equal(t, got, want)
_, err = endSpan(te, span)
require.NoError(t, err)
}
}