Redefine ExportSpans of SpanExporter with ReadOnlySpan (#1873)

* Remove TODO from ReadOnlySpan interface

* Remove the Tracer method from the ReadOnlySpan

This is not required by the specification nor the use of this interface.

* Remove IsRecording from the ReadOnlySpan interface

A read-only span value does not need to know if updates to it will be
recorded. It by definition cannot be updated so no point in
communicating if an update would be recorded.

* Document the ReadOnlySpan interface

* Rename messageEvent* to just event*

* Move the SpanSnapshot into its own file

* Update ReadOnlySpan interface with meta info methods

Add the DroppedAttributes, DroppedLinks, DroppedEvents, and
ChildSpanCount methods to the interface to return additional information
about the span not specified by the specification, but that we are
already providing.

* Add SpanStub to the sdk/trace/tracetest pkg

* Redefine ExportSpans of SpanExporter with ReadOnlySpan

* Rename SpanSnapshot to snapshot and purge docs

* Remove Snapshot method from snapshot type

This method is a hold-over from previous version of the ReadOnlySpan
interface is not needed.

* Update CHANGELOG with changes
This commit is contained in:
Tyler Yahn 2021-05-04 23:45:13 +00:00 committed by GitHub
parent c99d5e999c
commit cbcd4b1a3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1046 additions and 675 deletions

View File

@ -22,6 +22,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
| 14 | Unavailable | | 14 | Unavailable |
| 15 | Data Loss | | 15 | Data Loss |
- The `Status` type was added to the `go.opentelemetry.io/otel/sdk/trace` package to represent the status of a span. (#1874) - The `Status` type was added to the `go.opentelemetry.io/otel/sdk/trace` package to represent the status of a span. (#1874)
- The `SpanStub` type and its associated functions were added to the `go.opentelemetry.io/otel/sdk/trace/tracetest` package.
This type can be used as a testing replacement for the `SpanSnapshot` that was removed from the `go.opentelemetry.io/otel/sdk/trace` package. (#1873)
### Changed ### Changed
@ -34,6 +36,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Renamed `CloudZoneKey` to `CloudAvailabilityZoneKey` in Resource semantic conventions according to spec. (#1871) - Renamed `CloudZoneKey` to `CloudAvailabilityZoneKey` in Resource semantic conventions according to spec. (#1871)
- The `StatusCode` and `StatusMessage` methods of the `ReadOnlySpan` interface and the `Span` produced by the `go.opentelemetry.io/otel/sdk/trace` package have been replaced with a single `Status` method. - The `StatusCode` and `StatusMessage` methods of the `ReadOnlySpan` interface and the `Span` produced by the `go.opentelemetry.io/otel/sdk/trace` package have been replaced with a single `Status` method.
This method returns the status of a span using the new `Status` type. (#1874) This method returns the status of a span using the new `Status` type. (#1874)
- The `ExportSpans` method of the`SpanExporter` interface type was updated to accept `ReadOnlySpan`s instead of the removed `SpanSnapshot`.
This brings the export interface into compliance with the specification in that it now accepts an explicitly immutable type instead of just an implied one. (#1873)
- Unembed `SpanContext` in `Link`. (#1877) - Unembed `SpanContext` in `Link`. (#1877)
### Deprecated ### Deprecated
@ -42,6 +46,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Remove `resource.WithoutBuiltin()`. Use `resource.New()`. (#1810) - Remove `resource.WithoutBuiltin()`. Use `resource.New()`. (#1810)
- Unexported types `resource.FromEnv`, `resource.Host`, and `resource.TelemetrySDK`, Use the corresponding `With*()` to use individually. (#1810) - Unexported types `resource.FromEnv`, `resource.Host`, and `resource.TelemetrySDK`, Use the corresponding `With*()` to use individually. (#1810)
- Removed the `Tracer` and `IsRecording` method from the `ReadOnlySpan` in the `go.opentelemetry.io/otel/sdk/trace`.
The `Tracer` method is not a required to be included in this interface and given the mutable nature of the tracer that is associated with a span, this method is not appropriate.
The `IsRecording` method returns if the span is recording or not.
A read-only span value does not need to know if updates to it will be recorded or not.
By definition, it cannot be updated so there is no point in communicating if an update is recorded. (#1873)
- Removed the `SpanSnapshot` type from the `go.opentelemetry.io/otel/sdk/trace` package.
The use of this type has been replaced with the use of the explicitly immutable `ReadOnlySpan` type.
When a concrete representation of a read-only span is needed for testing, the newly added `SpanStub` in the `go.opentelemetry.io/otel/sdk/trace/tracetest` package should be used. (#1873)
### Fixed ### Fixed

View File

@ -28,6 +28,7 @@ import (
"go.opentelemetry.io/otel/sdk/metric/aggregator/sum" "go.opentelemetry.io/otel/sdk/metric/aggregator/sum"
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace" tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
@ -78,39 +79,40 @@ func (OneRecordCheckpointSet) ForEach(kindSelector exportmetric.ExportKindSelect
return recordFunc(rec) return recordFunc(rec)
} }
// SingleSpanSnapshot returns a one-element slice with a snapshot. It // SingleReadOnlySpan returns a one-element slice with a read-only span. It
// may be useful for testing driver's trace export. // may be useful for testing driver's trace export.
func SingleSpanSnapshot() []*tracesdk.SpanSnapshot { func SingleReadOnlySpan() []tracesdk.ReadOnlySpan {
sd := &tracesdk.SpanSnapshot{ return tracetest.SpanStubs{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ {
TraceID: trace.TraceID{2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9}, SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
SpanID: trace.SpanID{3, 4, 5, 6, 7, 8, 9, 0}, TraceID: trace.TraceID{2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9},
TraceFlags: trace.FlagsSampled, SpanID: trace.SpanID{3, 4, 5, 6, 7, 8, 9, 0},
}), TraceFlags: trace.FlagsSampled,
Parent: trace.NewSpanContext(trace.SpanContextConfig{ }),
TraceID: trace.TraceID{2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9}, Parent: trace.NewSpanContext(trace.SpanContextConfig{
SpanID: trace.SpanID{1, 2, 3, 4, 5, 6, 7, 8}, TraceID: trace.TraceID{2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9},
TraceFlags: trace.FlagsSampled, SpanID: trace.SpanID{1, 2, 3, 4, 5, 6, 7, 8},
}), TraceFlags: trace.FlagsSampled,
SpanKind: trace.SpanKindInternal, }),
Name: "foo", SpanKind: trace.SpanKindInternal,
StartTime: time.Date(2020, time.December, 8, 20, 23, 0, 0, time.UTC), Name: "foo",
EndTime: time.Date(2020, time.December, 0, 20, 24, 0, 0, time.UTC), StartTime: time.Date(2020, time.December, 8, 20, 23, 0, 0, time.UTC),
Attributes: []attribute.KeyValue{}, EndTime: time.Date(2020, time.December, 0, 20, 24, 0, 0, time.UTC),
MessageEvents: []tracesdk.Event{}, Attributes: []attribute.KeyValue{},
Links: []trace.Link{}, Events: []tracesdk.Event{},
Status: tracesdk.Status{Code: codes.Ok}, Links: []trace.Link{},
DroppedAttributeCount: 0, Status: tracesdk.Status{Code: codes.Ok},
DroppedMessageEventCount: 0, DroppedAttributes: 0,
DroppedLinkCount: 0, DroppedEvents: 0,
ChildSpanCount: 0, DroppedLinks: 0,
Resource: resource.NewWithAttributes(attribute.String("a", "b")), ChildSpanCount: 0,
InstrumentationLibrary: instrumentation.Library{ Resource: resource.NewWithAttributes(attribute.String("a", "b")),
Name: "bar", InstrumentationLibrary: instrumentation.Library{
Version: "0.0.0", Name: "bar",
Version: "0.0.0",
},
}, },
} }.Snapshots()
return []*tracesdk.SpanSnapshot{sd}
} }
// EmptyCheckpointSet is a checkpointer that has no records at all. // EmptyCheckpointSet is a checkpointer that has no records at all.

View File

@ -25,12 +25,12 @@ import (
) )
const ( const (
maxMessageEventsPerSpan = 128 maxEventsPerSpan = 128
) )
// SpanData transforms a slice of SpanSnapshot into a slice of OTLP // Spans transforms a slice of OpenTelemetry spans into a slice of OTLP
// ResourceSpans. // ResourceSpans.
func SpanData(sdl []*tracesdk.SpanSnapshot) []*tracepb.ResourceSpans { func Spans(sdl []tracesdk.ReadOnlySpan) []*tracepb.ResourceSpans {
if len(sdl) == 0 { if len(sdl) == 0 {
return nil return nil
} }
@ -49,16 +49,16 @@ func SpanData(sdl []*tracesdk.SpanSnapshot) []*tracepb.ResourceSpans {
continue continue
} }
rKey := sd.Resource.Equivalent() rKey := sd.Resource().Equivalent()
iKey := ilsKey{ iKey := ilsKey{
r: rKey, r: rKey,
il: sd.InstrumentationLibrary, il: sd.InstrumentationLibrary(),
} }
ils, iOk := ilsm[iKey] ils, iOk := ilsm[iKey]
if !iOk { if !iOk {
// Either the resource or instrumentation library were unknown. // Either the resource or instrumentation library were unknown.
ils = &tracepb.InstrumentationLibrarySpans{ ils = &tracepb.InstrumentationLibrarySpans{
InstrumentationLibrary: instrumentationLibrary(sd.InstrumentationLibrary), InstrumentationLibrary: instrumentationLibrary(sd.InstrumentationLibrary()),
Spans: []*tracepb.Span{}, Spans: []*tracepb.Span{},
} }
} }
@ -70,7 +70,7 @@ func SpanData(sdl []*tracesdk.SpanSnapshot) []*tracepb.ResourceSpans {
resources++ resources++
// The resource was unknown. // The resource was unknown.
rs = &tracepb.ResourceSpans{ rs = &tracepb.ResourceSpans{
Resource: Resource(sd.Resource), Resource: Resource(sd.Resource()),
InstrumentationLibrarySpans: []*tracepb.InstrumentationLibrarySpans{ils}, InstrumentationLibrarySpans: []*tracepb.InstrumentationLibrarySpans{ils},
} }
rsm[rKey] = rs rsm[rKey] = rs
@ -96,32 +96,32 @@ func SpanData(sdl []*tracesdk.SpanSnapshot) []*tracepb.ResourceSpans {
} }
// span transforms a Span into an OTLP span. // span transforms a Span into an OTLP span.
func span(sd *tracesdk.SpanSnapshot) *tracepb.Span { func span(sd tracesdk.ReadOnlySpan) *tracepb.Span {
if sd == nil { if sd == nil {
return nil return nil
} }
tid := sd.SpanContext.TraceID() tid := sd.SpanContext().TraceID()
sid := sd.SpanContext.SpanID() sid := sd.SpanContext().SpanID()
s := &tracepb.Span{ s := &tracepb.Span{
TraceId: tid[:], TraceId: tid[:],
SpanId: sid[:], SpanId: sid[:],
TraceState: sd.SpanContext.TraceState().String(), TraceState: sd.SpanContext().TraceState().String(),
Status: status(sd.Status.Code, sd.Status.Description), Status: status(sd.Status().Code, sd.Status().Description),
StartTimeUnixNano: uint64(sd.StartTime.UnixNano()), StartTimeUnixNano: uint64(sd.StartTime().UnixNano()),
EndTimeUnixNano: uint64(sd.EndTime.UnixNano()), EndTimeUnixNano: uint64(sd.EndTime().UnixNano()),
Links: links(sd.Links), Links: links(sd.Links()),
Kind: spanKind(sd.SpanKind), Kind: spanKind(sd.SpanKind()),
Name: sd.Name, Name: sd.Name(),
Attributes: Attributes(sd.Attributes), Attributes: Attributes(sd.Attributes()),
Events: spanEvents(sd.MessageEvents), Events: spanEvents(sd.Events()),
DroppedAttributesCount: uint32(sd.DroppedAttributeCount), DroppedAttributesCount: uint32(sd.DroppedAttributes()),
DroppedEventsCount: uint32(sd.DroppedMessageEventCount), DroppedEventsCount: uint32(sd.DroppedEvents()),
DroppedLinksCount: uint32(sd.DroppedLinkCount), DroppedLinksCount: uint32(sd.DroppedLinks()),
} }
if psid := sd.Parent.SpanID(); psid.IsValid() { if psid := sd.Parent().SpanID(); psid.IsValid() {
s.ParentSpanId = psid[:] s.ParentSpanId = psid[:]
} }
@ -174,18 +174,18 @@ func spanEvents(es []tracesdk.Event) []*tracepb.Span_Event {
} }
evCount := len(es) evCount := len(es)
if evCount > maxMessageEventsPerSpan { if evCount > maxEventsPerSpan {
evCount = maxMessageEventsPerSpan evCount = maxEventsPerSpan
} }
events := make([]*tracepb.Span_Event, 0, evCount) events := make([]*tracepb.Span_Event, 0, evCount)
messageEvents := 0 nEvents := 0
// Transform message events // Transform message events
for _, e := range es { for _, e := range es {
if messageEvents >= maxMessageEventsPerSpan { if nEvents >= maxEventsPerSpan {
break break
} }
messageEvents++ nEvents++
events = append(events, events = append(events,
&tracepb.Span_Event{ &tracepb.Span_Event{
Name: e.Name, Name: e.Name,

View File

@ -32,6 +32,7 @@ import (
"go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace" tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
) )
func TestSpanKind(t *testing.T) { func TestSpanKind(t *testing.T) {
@ -101,15 +102,15 @@ func TestSpanEvent(t *testing.T) {
} }
func TestExcessiveSpanEvents(t *testing.T) { func TestExcessiveSpanEvents(t *testing.T) {
e := make([]tracesdk.Event, maxMessageEventsPerSpan+1) e := make([]tracesdk.Event, maxEventsPerSpan+1)
for i := 0; i < maxMessageEventsPerSpan+1; i++ { for i := 0; i < maxEventsPerSpan+1; i++ {
e[i] = tracesdk.Event{Name: strconv.Itoa(i)} e[i] = tracesdk.Event{Name: strconv.Itoa(i)}
} }
assert.Len(t, e, maxMessageEventsPerSpan+1) assert.Len(t, e, maxEventsPerSpan+1)
got := spanEvents(e) got := spanEvents(e)
assert.Len(t, got, maxMessageEventsPerSpan) assert.Len(t, got, maxEventsPerSpan)
// Ensure the drop order. // Ensure the drop order.
assert.Equal(t, strconv.Itoa(maxMessageEventsPerSpan-1), got[len(got)-1].Name) assert.Equal(t, strconv.Itoa(maxEventsPerSpan-1), got[len(got)-1].Name)
} }
func TestNilLinks(t *testing.T) { func TestNilLinks(t *testing.T) {
@ -185,11 +186,11 @@ func TestNilSpan(t *testing.T) {
} }
func TestNilSpanData(t *testing.T) { func TestNilSpanData(t *testing.T) {
assert.Nil(t, SpanData(nil)) assert.Nil(t, Spans(nil))
} }
func TestEmptySpanData(t *testing.T) { func TestEmptySpanData(t *testing.T) {
assert.Nil(t, SpanData(nil)) assert.Nil(t, Spans(nil))
} }
func TestSpanData(t *testing.T) { func TestSpanData(t *testing.T) {
@ -199,7 +200,7 @@ func TestSpanData(t *testing.T) {
startTime := time.Unix(1585674086, 1234) startTime := time.Unix(1585674086, 1234)
endTime := startTime.Add(10 * time.Second) endTime := startTime.Add(10 * time.Second)
traceState, _ := trace.TraceStateFromKeyValues(attribute.String("key1", "val1"), attribute.String("key2", "val2")) traceState, _ := trace.TraceStateFromKeyValues(attribute.String("key1", "val1"), attribute.String("key2", "val2"))
spanData := &tracesdk.SpanSnapshot{ spanData := tracetest.SpanStub{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: trace.TraceID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}, TraceID: trace.TraceID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F},
SpanID: trace.SpanID{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8}, SpanID: trace.SpanID{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8},
@ -215,7 +216,7 @@ func TestSpanData(t *testing.T) {
Name: "span data to span data", Name: "span data to span data",
StartTime: startTime, StartTime: startTime,
EndTime: endTime, EndTime: endTime,
MessageEvents: []tracesdk.Event{ Events: []tracesdk.Event{
{Time: startTime, {Time: startTime,
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
attribute.Int64("CompressedByteSize", 512), attribute.Int64("CompressedByteSize", 512),
@ -223,7 +224,7 @@ func TestSpanData(t *testing.T) {
}, },
{Time: endTime, {Time: endTime,
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
attribute.String("MessageEventType", "Recv"), attribute.String("EventType", "Recv"),
}, },
}, },
}, },
@ -256,10 +257,10 @@ func TestSpanData(t *testing.T) {
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
attribute.Int64("timeout_ns", 12e9), attribute.Int64("timeout_ns", 12e9),
}, },
DroppedAttributeCount: 1, DroppedAttributes: 1,
DroppedMessageEventCount: 2, DroppedEvents: 2,
DroppedLinkCount: 3, DroppedLinks: 3,
Resource: resource.NewWithAttributes(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)), Resource: resource.NewWithAttributes(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)),
InstrumentationLibrary: instrumentation.Library{ InstrumentationLibrary: instrumentation.Library{
Name: "go.opentelemetry.io/test/otel", Name: "go.opentelemetry.io/test/otel",
Version: "v0.0.1", Version: "v0.0.1",
@ -279,7 +280,7 @@ func TestSpanData(t *testing.T) {
StartTimeUnixNano: uint64(startTime.UnixNano()), StartTimeUnixNano: uint64(startTime.UnixNano()),
EndTimeUnixNano: uint64(endTime.UnixNano()), EndTimeUnixNano: uint64(endTime.UnixNano()),
Status: status(spanData.Status.Code, spanData.Status.Description), Status: status(spanData.Status.Code, spanData.Status.Description),
Events: spanEvents(spanData.MessageEvents), Events: spanEvents(spanData.Events),
Links: links(spanData.Links), Links: links(spanData.Links),
Attributes: Attributes(spanData.Attributes), Attributes: Attributes(spanData.Attributes),
DroppedAttributesCount: 1, DroppedAttributesCount: 1,
@ -287,7 +288,7 @@ func TestSpanData(t *testing.T) {
DroppedLinksCount: 3, DroppedLinksCount: 3,
} }
got := SpanData([]*tracesdk.SpanSnapshot{spanData}) got := Spans(tracetest.SpanStubs{spanData}.Snapshots())
require.Len(t, got, 1) require.Len(t, got, 1)
assert.Equal(t, got[0].GetResource(), Resource(spanData.Resource)) assert.Equal(t, got[0].GetResource(), Resource(spanData.Resource))
@ -304,7 +305,7 @@ func TestSpanData(t *testing.T) {
// Empty parent span ID should be treated as root span. // Empty parent span ID should be treated as root span.
func TestRootSpanData(t *testing.T) { func TestRootSpanData(t *testing.T) {
sd := SpanData([]*tracesdk.SpanSnapshot{{}}) sd := Spans(tracetest.SpanStubs{{}}.Snapshots())
require.Len(t, sd, 1) require.Len(t, sd, 1)
rs := sd[0] rs := sd[0]
got := rs.GetInstrumentationLibrarySpans()[0].GetSpans()[0].GetParentSpanId() got := rs.GetInstrumentationLibrarySpans()[0].GetSpans()[0].GetParentSpanId()
@ -314,5 +315,5 @@ func TestRootSpanData(t *testing.T) {
} }
func TestSpanDataNilResource(t *testing.T) { func TestSpanDataNilResource(t *testing.T) {
assert.NotPanics(t, func() { SpanData([]*tracesdk.SpanSnapshot{{}}) }) assert.NotPanics(t, func() { Spans(tracetest.SpanStubs{{}}.Snapshots()) })
} }

View File

@ -129,10 +129,10 @@ func (e *Exporter) ExportKindFor(desc *metric.Descriptor, kind aggregation.Kind)
return e.cfg.exportKindSelector.ExportKindFor(desc, kind) return e.cfg.exportKindSelector.ExportKindFor(desc, kind)
} }
// ExportSpans transforms and batches trace SpanSnapshots into OTLP Trace and // ExportSpans transforms and batches OpenTelemetry spans into OTLP Trace and
// transmits them to the configured collector. // transmits them to the configured collector.
func (e *Exporter) ExportSpans(ctx context.Context, ss []*tracesdk.SpanSnapshot) error { func (e *Exporter) ExportSpans(ctx context.Context, spans []tracesdk.ReadOnlySpan) error {
return e.driver.ExportTraces(ctx, ss) return e.driver.ExportTraces(ctx, spans)
} }
// NewExportPipeline sets up a complete export pipeline // NewExportPipeline sets up a complete export pipeline

View File

@ -31,6 +31,7 @@ import (
"go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace" tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
) )
func TestExportSpans(t *testing.T) { func TestExportSpans(t *testing.T) {
@ -41,19 +42,19 @@ func TestExportSpans(t *testing.T) {
endTime := startTime.Add(10 * time.Second) endTime := startTime.Add(10 * time.Second)
for _, test := range []struct { for _, test := range []struct {
sd []*tracesdk.SpanSnapshot sd tracetest.SpanStubs
want []*tracepb.ResourceSpans want []*tracepb.ResourceSpans
}{ }{
{ {
[]*tracesdk.SpanSnapshot(nil), tracetest.SpanStubsFromReadOnlySpans(nil),
[]*tracepb.ResourceSpans(nil), []*tracepb.ResourceSpans(nil),
}, },
{ {
[]*tracesdk.SpanSnapshot{}, tracetest.SpanStubs{},
[]*tracepb.ResourceSpans(nil), []*tracepb.ResourceSpans(nil),
}, },
{ {
[]*tracesdk.SpanSnapshot{ tracetest.SpanStubs{
{ {
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: trace.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), TraceID: trace.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}),
@ -338,7 +339,7 @@ func TestExportSpans(t *testing.T) {
}, },
} { } {
driver.Reset() driver.Reset()
assert.NoError(t, exp.ExportSpans(context.Background(), test.sd)) assert.NoError(t, exp.ExportSpans(context.Background(), test.sd.Snapshots()))
assert.ElementsMatch(t, test.want, driver.rs) assert.ElementsMatch(t, test.want, driver.rs)
} }
} }

View File

@ -29,16 +29,17 @@ import (
"go.opentelemetry.io/otel/exporters/otlp/internal/transform" "go.opentelemetry.io/otel/exporters/otlp/internal/transform"
metricsdk "go.opentelemetry.io/otel/sdk/export/metric" metricsdk "go.opentelemetry.io/otel/sdk/export/metric"
tracesdk "go.opentelemetry.io/otel/sdk/trace" tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
metricpb "go.opentelemetry.io/proto/otlp/metrics/v1" metricpb "go.opentelemetry.io/proto/otlp/metrics/v1"
tracepb "go.opentelemetry.io/proto/otlp/trace/v1" tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
) )
func stubSpanSnapshot(count int) []*tracesdk.SpanSnapshot { func readonlyspans(count int) []tracesdk.ReadOnlySpan {
spans := make([]*tracesdk.SpanSnapshot, 0, count) spans := make(tracetest.SpanStubs, 0, count)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
spans = append(spans, new(tracesdk.SpanSnapshot)) spans = append(spans, tracetest.SpanStub{})
} }
return spans return spans.Snapshots()
} }
type stubCheckpointSet struct { type stubCheckpointSet struct {
@ -71,7 +72,7 @@ type stubProtocolDriver struct {
injectedStopError error injectedStopError error
rm []metricsdk.Record rm []metricsdk.Record
rs []tracesdk.SpanSnapshot rs tracetest.SpanStubs
} }
var _ otlp.ProtocolDriver = (*stubProtocolDriver)(nil) var _ otlp.ProtocolDriver = (*stubProtocolDriver)(nil)
@ -104,13 +105,13 @@ func (m *stubProtocolDriver) ExportMetrics(parent context.Context, cps metricsdk
}) })
} }
func (m *stubProtocolDriver) ExportTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot) error { func (m *stubProtocolDriver) ExportTraces(ctx context.Context, ss []tracesdk.ReadOnlySpan) error {
m.tracesExported++ m.tracesExported++
for _, rs := range ss { for _, rs := range ss {
if rs == nil { if rs == nil {
continue continue
} }
m.rs = append(m.rs, *rs) m.rs = append(m.rs, tracetest.SpanStubFromReadOnlySpan(rs))
} }
return nil return nil
} }
@ -144,8 +145,8 @@ func (m *stubTransformingProtocolDriver) ExportMetrics(parent context.Context, c
return nil return nil
} }
func (m *stubTransformingProtocolDriver) ExportTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot) error { func (m *stubTransformingProtocolDriver) ExportTraces(ctx context.Context, ss []tracesdk.ReadOnlySpan) error {
for _, rs := range transform.SpanData(ss) { for _, rs := range transform.Spans(ss) {
if rs == nil { if rs == nil {
continue continue
} }
@ -289,7 +290,7 @@ func TestSplitDriver(t *testing.T) {
assertExport := func(t testing.TB, ctx context.Context, driver otlp.ProtocolDriver) { assertExport := func(t testing.TB, ctx context.Context, driver otlp.ProtocolDriver) {
t.Helper() t.Helper()
assert.NoError(t, driver.ExportMetrics(ctx, stubCheckpointSet{recordCount}, metricsdk.StatelessExportKindSelector())) assert.NoError(t, driver.ExportMetrics(ctx, stubCheckpointSet{recordCount}, metricsdk.StatelessExportKindSelector()))
assert.NoError(t, driver.ExportTraces(ctx, stubSpanSnapshot(spanCount))) assert.NoError(t, driver.ExportTraces(ctx, readonlyspans(spanCount)))
} }
t.Run("with metric/trace drivers configured", func(t *testing.T) { t.Run("with metric/trace drivers configured", func(t *testing.T) {
@ -385,7 +386,7 @@ func TestSplitDriver(t *testing.T) {
assert.NoError(t, driver.Start(ctx)) assert.NoError(t, driver.Start(ctx))
assert.NoError(t, driver.ExportMetrics(ctx, stubCheckpointSet{recordCount}, metricsdk.StatelessExportKindSelector())) assert.NoError(t, driver.ExportMetrics(ctx, stubCheckpointSet{recordCount}, metricsdk.StatelessExportKindSelector()))
assert.NoError(t, driver.ExportTraces(ctx, stubSpanSnapshot(spanCount))) assert.NoError(t, driver.ExportTraces(ctx, readonlyspans(spanCount)))
assert.NoError(t, driver.Stop(ctx)) assert.NoError(t, driver.Stop(ctx))
}) })

View File

@ -161,7 +161,7 @@ func (md *metricsDriver) uploadMetrics(ctx context.Context, protoMetrics []*metr
// ExportTraces implements otlp.ProtocolDriver. It transforms spans to // ExportTraces implements otlp.ProtocolDriver. It transforms spans to
// protobuf binary format and sends the result to the collector. // protobuf binary format and sends the result to the collector.
func (d *driver) ExportTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot) error { func (d *driver) ExportTraces(ctx context.Context, ss []tracesdk.ReadOnlySpan) error {
if !d.tracesDriver.connection.connected() { if !d.tracesDriver.connection.connected() {
return fmt.Errorf("traces exporter is disconnected from the server %s: %w", d.tracesDriver.connection.sCfg.Endpoint, d.tracesDriver.connection.lastConnectError()) return fmt.Errorf("traces exporter is disconnected from the server %s: %w", d.tracesDriver.connection.sCfg.Endpoint, d.tracesDriver.connection.lastConnectError())
} }
@ -170,7 +170,7 @@ func (d *driver) ExportTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot)
ctx, tCancel := context.WithTimeout(ctx, d.tracesDriver.connection.sCfg.Timeout) ctx, tCancel := context.WithTimeout(ctx, d.tracesDriver.connection.sCfg.Timeout)
defer tCancel() defer tCancel()
protoSpans := transform.SpanData(ss) protoSpans := transform.Spans(ss)
if len(protoSpans) == 0 { if len(protoSpans) == 0 {
return nil return nil
} }

View File

@ -38,9 +38,12 @@ import (
"go.opentelemetry.io/otel/exporters/otlp/internal/otlptest" "go.opentelemetry.io/otel/exporters/otlp/internal/otlptest"
"go.opentelemetry.io/otel/exporters/otlp/otlpgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpgrpc"
sdktrace "go.opentelemetry.io/otel/sdk/trace" sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
commonpb "go.opentelemetry.io/proto/otlp/common/v1" commonpb "go.opentelemetry.io/proto/otlp/common/v1"
) )
var roSpans = tracetest.SpanStubs{{Name: "Span 0"}}.Snapshots()
func TestNewExporter_endToEnd(t *testing.T) { func TestNewExporter_endToEnd(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -165,11 +168,11 @@ func TestNewExporter_collectorConnectionDiesThenReconnectsWhenInRestMode(t *test
// first export, it will send disconnected message to the channel on export failure, // first export, it will send disconnected message to the channel on export failure,
// trigger almost immediate reconnection // trigger almost immediate reconnection
require.Error(t, exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "in the midst"}})) require.Error(t, exp.ExportSpans(ctx, roSpans))
// second export, it will detect connection issue, change state of exporter to disconnected and // second export, it will detect connection issue, change state of exporter to disconnected and
// send message to disconnected channel but this time reconnection gouroutine will be in (rest mode, not listening to the disconnected channel) // send message to disconnected channel but this time reconnection gouroutine will be in (rest mode, not listening to the disconnected channel)
require.Error(t, exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "in the midst"}})) require.Error(t, exp.ExportSpans(ctx, roSpans))
// as a result we have exporter in disconnected state waiting for disconnection message to reconnect // as a result we have exporter in disconnected state waiting for disconnection message to reconnect
@ -184,12 +187,12 @@ func TestNewExporter_collectorConnectionDiesThenReconnectsWhenInRestMode(t *test
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
// when disconnected exp.ExportSpans doesnt send disconnected messages again // when disconnected exp.ExportSpans doesnt send disconnected messages again
// it just quits and return last connection error // it just quits and return last connection error
require.NoError(t, exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Resurrected"}})) require.NoError(t, exp.ExportSpans(ctx, roSpans))
} }
nmaSpans := nmc.getSpans() nmaSpans := nmc.getSpans()
// Expecting 10 SpanSnapshots that were sampled, given that // Expecting 10 spans that were sampled, given that
if g, w := len(nmaSpans), n; g != w { if g, w := len(nmaSpans), n; g != w {
t.Fatalf("Connected collector: spans: got %d want %d", g, w) t.Fatalf("Connected collector: spans: got %d want %d", g, w)
} }
@ -214,7 +217,7 @@ func TestExporterExportFailureAndRecoveryModes(t *testing.T) {
{ {
name: "Do not retry if succeeded", name: "Do not retry if succeeded",
fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) { fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) {
require.NoError(t, exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Spans"}})) require.NoError(t, exp.ExportSpans(ctx, roSpans))
span := mc.getSpans() span := mc.getSpans()
@ -228,7 +231,7 @@ func TestExporterExportFailureAndRecoveryModes(t *testing.T) {
status.Error(codes.OK, ""), status.Error(codes.OK, ""),
}, },
fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) { fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) {
require.NoError(t, exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Spans"}})) require.NoError(t, exp.ExportSpans(ctx, roSpans))
span := mc.getSpans() span := mc.getSpans()
@ -250,7 +253,7 @@ func TestExporterExportFailureAndRecoveryModes(t *testing.T) {
status.Error(codes.Unavailable, "backend under pressure"), status.Error(codes.Unavailable, "backend under pressure"),
}, },
fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) { fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) {
require.NoError(t, exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Spans"}})) require.NoError(t, exp.ExportSpans(ctx, roSpans))
span := mc.getSpans() span := mc.getSpans()
@ -270,7 +273,7 @@ func TestExporterExportFailureAndRecoveryModes(t *testing.T) {
status.Error(codes.InvalidArgument, "invalid arguments"), status.Error(codes.InvalidArgument, "invalid arguments"),
}, },
fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) { fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) {
require.Error(t, exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Spans"}})) require.Error(t, exp.ExportSpans(ctx, roSpans))
span := mc.getSpans() span := mc.getSpans()
@ -296,7 +299,7 @@ func TestExporterExportFailureAndRecoveryModes(t *testing.T) {
status.Error(codes.DataLoss, ""), status.Error(codes.DataLoss, ""),
}, },
fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) { fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) {
require.NoError(t, exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Spans"}})) require.NoError(t, exp.ExportSpans(ctx, roSpans))
span := mc.getSpans() span := mc.getSpans()
@ -319,7 +322,7 @@ func TestExporterExportFailureAndRecoveryModes(t *testing.T) {
newThrottlingError(codes.ResourceExhausted, time.Second*30), newThrottlingError(codes.ResourceExhausted, time.Second*30),
}, },
fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) { fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) {
err := exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Spans"}}) err := exp.ExportSpans(ctx, roSpans)
require.Error(t, err) require.Error(t, err)
require.Equal(t, "context deadline exceeded", err.Error()) require.Equal(t, "context deadline exceeded", err.Error())
@ -341,7 +344,7 @@ func TestExporterExportFailureAndRecoveryModes(t *testing.T) {
newThrottlingError(codes.ResourceExhausted, time.Minute), newThrottlingError(codes.ResourceExhausted, time.Minute),
}, },
fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) { fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) {
err := exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Spans"}}) err := exp.ExportSpans(ctx, roSpans)
require.Error(t, err) require.Error(t, err)
require.Equal(t, "max elapsed time expired when respecting server throttle: rpc error: code = ResourceExhausted desc = ", err.Error()) require.Equal(t, "max elapsed time expired when respecting server throttle: rpc error: code = ResourceExhausted desc = ", err.Error())
@ -368,7 +371,7 @@ func TestExporterExportFailureAndRecoveryModes(t *testing.T) {
status.Error(codes.Unavailable, "unavailable"), status.Error(codes.Unavailable, "unavailable"),
}, },
fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) { fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) {
err := exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Spans"}}) err := exp.ExportSpans(ctx, roSpans)
require.Error(t, err) require.Error(t, err)
require.Equal(t, "max elapsed time expired: rpc error: code = Unavailable desc = unavailable", err.Error()) require.Equal(t, "max elapsed time expired: rpc error: code = Unavailable desc = unavailable", err.Error())
@ -388,7 +391,7 @@ func TestExporterExportFailureAndRecoveryModes(t *testing.T) {
status.Error(codes.Unavailable, "unavailable"), status.Error(codes.Unavailable, "unavailable"),
}, },
fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) { fn: func(t *testing.T, ctx context.Context, exp *otlp.Exporter, mc *mockCollector) {
err := exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Spans"}}) err := exp.ExportSpans(ctx, roSpans)
require.Error(t, err) require.Error(t, err)
require.Equal(t, "rpc error: code = Unavailable desc = unavailable", err.Error()) require.Equal(t, "rpc error: code = Unavailable desc = unavailable", err.Error())
@ -451,7 +454,7 @@ func TestPermanentErrorsShouldNotBeRetried(t *testing.T) {
exp := newGRPCExporter(t, ctx, mc.endpoint) exp := newGRPCExporter(t, ctx, mc.endpoint)
err := exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Spans"}}) err := exp.ExportSpans(ctx, roSpans)
require.Error(t, err) require.Error(t, err)
require.Len(t, mc.getSpans(), 0) require.Len(t, mc.getSpans(), 0)
require.Equal(t, 1, mc.traceSvc.requests, "trace service must receive 1 permanent error requests.") require.Equal(t, 1, mc.traceSvc.requests, "trace service must receive 1 permanent error requests.")
@ -492,7 +495,7 @@ func TestNewExporter_collectorConnectionDiesThenReconnects(t *testing.T) {
for j := 0; j < 3; j++ { for j := 0; j < 3; j++ {
// No endpoint up. // No endpoint up.
require.Error(t, exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "in the midst"}})) require.Error(t, exp.ExportSpans(ctx, roSpans))
// Now resurrect the collector by making a new one but reusing the // Now resurrect the collector by making a new one but reusing the
// old endpoint, and the collector should reconnect automatically. // old endpoint, and the collector should reconnect automatically.
@ -503,11 +506,11 @@ func TestNewExporter_collectorConnectionDiesThenReconnects(t *testing.T) {
n := 10 n := 10
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
require.NoError(t, exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "Resurrected"}})) require.NoError(t, exp.ExportSpans(ctx, roSpans))
} }
nmaSpans := nmc.getSpans() nmaSpans := nmc.getSpans()
// Expecting 10 SpanSnapshots that were sampled, given that // Expecting 10 spans that were sampled, given that
if g, w := len(nmaSpans), n; g != w { if g, w := len(nmaSpans), n; g != w {
t.Fatalf("Round #%d: Connected collector: spans: got %d want %d", j, g, w) t.Fatalf("Round #%d: Connected collector: spans: got %d want %d", j, g, w)
} }
@ -565,7 +568,7 @@ func TestNewExporter_withHeaders(t *testing.T) {
ctx := context.Background() ctx := context.Background()
exp := newGRPCExporter(t, ctx, mc.endpoint, exp := newGRPCExporter(t, ctx, mc.endpoint,
otlpgrpc.WithHeaders(map[string]string{"header1": "value1"})) otlpgrpc.WithHeaders(map[string]string{"header1": "value1"}))
require.NoError(t, exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "in the midst"}})) require.NoError(t, exp.ExportSpans(ctx, roSpans))
defer func() { defer func() {
_ = exp.Shutdown(ctx) _ = exp.Shutdown(ctx)
@ -589,7 +592,7 @@ func TestNewExporter_WithTimeout(t *testing.T) {
{ {
name: "Timeout Spans", name: "Timeout Spans",
fn: func(exp *otlp.Exporter) error { fn: func(exp *otlp.Exporter) error {
return exp.ExportSpans(context.Background(), []*sdktrace.SpanSnapshot{{Name: "timed out"}}) return exp.ExportSpans(context.Background(), roSpans)
}, },
timeout: time.Millisecond * 100, timeout: time.Millisecond * 100,
code: codes.DeadlineExceeded, code: codes.DeadlineExceeded,
@ -608,7 +611,7 @@ func TestNewExporter_WithTimeout(t *testing.T) {
{ {
name: "No Timeout Spans", name: "No Timeout Spans",
fn: func(exp *otlp.Exporter) error { fn: func(exp *otlp.Exporter) error {
return exp.ExportSpans(context.Background(), []*sdktrace.SpanSnapshot{{Name: "timed out"}}) return exp.ExportSpans(context.Background(), roSpans)
}, },
timeout: time.Minute, timeout: time.Minute,
spans: 1, spans: 1,
@ -673,7 +676,7 @@ func TestNewExporter_withInvalidSecurityConfiguration(t *testing.T) {
t.Fatalf("failed to create a new collector exporter: %v", err) t.Fatalf("failed to create a new collector exporter: %v", err)
} }
err = exp.ExportSpans(ctx, []*sdktrace.SpanSnapshot{{Name: "misconfiguration"}}) err = exp.ExportSpans(ctx, roSpans)
expectedErr := fmt.Sprintf("traces exporter is disconnected from the server %s: grpc: no transport security set (use grpc.WithInsecure() explicitly or set credentials)", mc.endpoint) expectedErr := fmt.Sprintf("traces exporter is disconnected from the server %s: grpc: no transport security set (use grpc.WithInsecure() explicitly or set credentials)", mc.endpoint)
@ -833,7 +836,7 @@ func TestDisconnected(t *testing.T) {
}() }()
assert.Error(t, exp.Export(ctx, otlptest.OneRecordCheckpointSet{})) assert.Error(t, exp.Export(ctx, otlptest.OneRecordCheckpointSet{}))
assert.Error(t, exp.ExportSpans(ctx, otlptest.SingleSpanSnapshot())) assert.Error(t, exp.ExportSpans(ctx, otlptest.SingleReadOnlySpan()))
} }
func TestEmptyData(t *testing.T) { func TestEmptyData(t *testing.T) {

View File

@ -187,8 +187,8 @@ func (d *driver) ExportMetrics(ctx context.Context, cps metricsdk.CheckpointSet,
} }
// ExportTraces implements otlp.ProtocolDriver. // ExportTraces implements otlp.ProtocolDriver.
func (d *driver) ExportTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot) error { func (d *driver) ExportTraces(ctx context.Context, ss []tracesdk.ReadOnlySpan) error {
protoSpans := transform.SpanData(ss) protoSpans := transform.Spans(ss)
if len(protoSpans) == 0 { if len(protoSpans) == 0 {
return nil return nil
} }

View File

@ -165,7 +165,7 @@ func TestRetry(t *testing.T) {
defer func() { defer func() {
assert.NoError(t, exporter.Shutdown(ctx)) assert.NoError(t, exporter.Shutdown(ctx))
}() }()
err = exporter.ExportSpans(ctx, otlptest.SingleSpanSnapshot()) err = exporter.ExportSpans(ctx, otlptest.SingleReadOnlySpan())
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, mc.GetSpans(), 1) assert.Len(t, mc.GetSpans(), 1)
} }
@ -187,7 +187,7 @@ func TestTimeout(t *testing.T) {
defer func() { defer func() {
assert.NoError(t, exporter.Shutdown(ctx)) assert.NoError(t, exporter.Shutdown(ctx))
}() }()
err = exporter.ExportSpans(ctx, otlptest.SingleSpanSnapshot()) err = exporter.ExportSpans(ctx, otlptest.SingleReadOnlySpan())
assert.Equal(t, true, os.IsTimeout(err)) assert.Equal(t, true, os.IsTimeout(err))
} }
@ -212,7 +212,7 @@ func TestRetryFailed(t *testing.T) {
defer func() { defer func() {
assert.NoError(t, exporter.Shutdown(ctx)) assert.NoError(t, exporter.Shutdown(ctx))
}() }()
err = exporter.ExportSpans(ctx, otlptest.SingleSpanSnapshot()) err = exporter.ExportSpans(ctx, otlptest.SingleReadOnlySpan())
assert.Error(t, err) assert.Error(t, err)
assert.Empty(t, mc.GetSpans()) assert.Empty(t, mc.GetSpans())
} }
@ -237,7 +237,7 @@ func TestNoRetry(t *testing.T) {
defer func() { defer func() {
assert.NoError(t, exporter.Shutdown(ctx)) assert.NoError(t, exporter.Shutdown(ctx))
}() }()
err = exporter.ExportSpans(ctx, otlptest.SingleSpanSnapshot()) err = exporter.ExportSpans(ctx, otlptest.SingleReadOnlySpan())
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, fmt.Sprintf("failed to send traces to http://%s/v1/traces with HTTP status 400 Bad Request", mc.endpoint), err.Error()) assert.Equal(t, fmt.Sprintf("failed to send traces to http://%s/v1/traces with HTTP status 400 Bad Request", mc.endpoint), err.Error())
assert.Empty(t, mc.GetSpans()) assert.Empty(t, mc.GetSpans())
@ -325,7 +325,7 @@ func TestUnreasonableMaxAttempts(t *testing.T) {
defer func() { defer func() {
assert.NoError(t, exporter.Shutdown(ctx)) assert.NoError(t, exporter.Shutdown(ctx))
}() }()
err = exporter.ExportSpans(ctx, otlptest.SingleSpanSnapshot()) err = exporter.ExportSpans(ctx, otlptest.SingleReadOnlySpan())
assert.Error(t, err) assert.Error(t, err)
assert.Empty(t, mc.GetSpans()) assert.Empty(t, mc.GetSpans())
}) })
@ -361,7 +361,7 @@ func TestUnreasonableBackoff(t *testing.T) {
defer func() { defer func() {
assert.NoError(t, exporter.Shutdown(ctx)) assert.NoError(t, exporter.Shutdown(ctx))
}() }()
err = exporter.ExportSpans(ctx, otlptest.SingleSpanSnapshot()) err = exporter.ExportSpans(ctx, otlptest.SingleReadOnlySpan())
assert.Error(t, err) assert.Error(t, err)
assert.Empty(t, mc.GetSpans()) assert.Empty(t, mc.GetSpans())
} }
@ -381,7 +381,7 @@ func TestCancelledContext(t *testing.T) {
assert.NoError(t, exporter.Shutdown(ctx)) assert.NoError(t, exporter.Shutdown(ctx))
}() }()
cancel() cancel()
err = exporter.ExportSpans(ctx, otlptest.SingleSpanSnapshot()) err = exporter.ExportSpans(ctx, otlptest.SingleReadOnlySpan())
assert.Error(t, err) assert.Error(t, err)
assert.Empty(t, mc.GetSpans()) assert.Empty(t, mc.GetSpans())
} }
@ -409,7 +409,7 @@ func TestDeadlineContext(t *testing.T) {
}() }()
ctx, cancel := context.WithTimeout(ctx, time.Second) ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel() defer cancel()
err = exporter.ExportSpans(ctx, otlptest.SingleSpanSnapshot()) err = exporter.ExportSpans(ctx, otlptest.SingleReadOnlySpan())
assert.Error(t, err) assert.Error(t, err)
assert.Empty(t, mc.GetSpans()) assert.Empty(t, mc.GetSpans())
} }
@ -437,7 +437,7 @@ func TestStopWhileExporting(t *testing.T) {
}() }()
doneCh := make(chan struct{}) doneCh := make(chan struct{})
go func() { go func() {
err := exporter.ExportSpans(ctx, otlptest.SingleSpanSnapshot()) err := exporter.ExportSpans(ctx, otlptest.SingleReadOnlySpan())
assert.Error(t, err) assert.Error(t, err)
assert.Empty(t, mc.GetSpans()) assert.Empty(t, mc.GetSpans())
close(doneCh) close(doneCh)

View File

@ -48,7 +48,7 @@ type ProtocolDriver interface {
// format and send it to the collector. May be called // format and send it to the collector. May be called
// concurrently with ExportMetrics, so the manager needs to // concurrently with ExportMetrics, so the manager needs to
// take this into account by doing proper locking. // take this into account by doing proper locking.
ExportTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot) error ExportTraces(ctx context.Context, ss []tracesdk.ReadOnlySpan) error
} }
// SplitConfig is used to configure a split driver. // SplitConfig is used to configure a split driver.
@ -151,7 +151,7 @@ func (d *splitDriver) ExportMetrics(ctx context.Context, cps metricsdk.Checkpoin
// ExportTraces implements ProtocolDriver. It forwards the call to the // ExportTraces implements ProtocolDriver. It forwards the call to the
// driver used for sending spans. // driver used for sending spans.
func (d *splitDriver) ExportTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot) error { func (d *splitDriver) ExportTraces(ctx context.Context, ss []tracesdk.ReadOnlySpan) error {
return d.trace.ExportTraces(ctx, ss) return d.trace.ExportTraces(ctx, ss)
} }
@ -171,6 +171,6 @@ func (d *noopDriver) ExportMetrics(ctx context.Context, cps metricsdk.Checkpoint
} }
// ExportTraces does nothing. // ExportTraces does nothing.
func (d *noopDriver) ExportTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot) error { func (d *noopDriver) ExportTraces(ctx context.Context, ss []tracesdk.ReadOnlySpan) error {
return nil return nil
} }

View File

@ -21,6 +21,7 @@ import (
"sync" "sync"
"go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
) )
// Exporter is an implementation of trace.SpanSyncer that writes spans to stdout. // Exporter is an implementation of trace.SpanSyncer that writes spans to stdout.
@ -31,8 +32,8 @@ type traceExporter struct {
stopped bool stopped bool
} }
// ExportSpans writes SpanSnapshots in json format to stdout. // ExportSpans writes spans in json format to stdout.
func (e *traceExporter) ExportSpans(ctx context.Context, ss []*trace.SpanSnapshot) error { func (e *traceExporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error {
e.stoppedMu.RLock() e.stoppedMu.RLock()
stopped := e.stopped stopped := e.stopped
e.stoppedMu.RUnlock() e.stoppedMu.RUnlock()
@ -40,10 +41,10 @@ func (e *traceExporter) ExportSpans(ctx context.Context, ss []*trace.SpanSnapsho
return nil return nil
} }
if e.config.DisableTraceExport || len(ss) == 0 { if e.config.DisableTraceExport || len(spans) == 0 {
return nil return nil
} }
out, err := e.marshal(ss) out, err := e.marshal(tracetest.SpanStubsFromReadOnlySpans(spans))
if err != nil { if err != nil {
return err return err
} }

View File

@ -22,18 +22,21 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/exporters/stdout" "go.opentelemetry.io/otel/exporters/stdout"
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace" tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
func TestExporter_ExportSpan(t *testing.T) { func TestExporter_ExportSpan(t *testing.T) {
// write to buffer for testing // write to buffer for testing
var b bytes.Buffer var b bytes.Buffer
ex, err := stdout.NewExporter(stdout.WithWriter(&b)) ex, err := stdout.NewExporter(stdout.WithWriter(&b), stdout.WithPrettyPrint())
if err != nil { if err != nil {
t.Errorf("Error constructing stdout exporter %s", err) t.Errorf("Error constructing stdout exporter %s", err)
} }
@ -47,108 +50,139 @@ func TestExporter_ExportSpan(t *testing.T) {
doubleValue := 123.456 doubleValue := 123.456
resource := resource.NewWithAttributes(attribute.String("rk1", "rv11")) resource := resource.NewWithAttributes(attribute.String("rk1", "rv11"))
testSpan := &tracesdk.SpanSnapshot{ ro := tracetest.SpanStubs{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ {
TraceID: traceID, SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
SpanID: spanID, TraceID: traceID,
TraceState: traceState, SpanID: spanID,
}), TraceState: traceState,
Name: "/foo", }),
StartTime: now, Name: "/foo",
EndTime: now, StartTime: now,
Attributes: []attribute.KeyValue{ EndTime: now,
attribute.String("key", keyValue), Attributes: []attribute.KeyValue{
attribute.Float64("double", doubleValue), attribute.String("key", keyValue),
attribute.Float64("double", doubleValue),
},
Events: []tracesdk.Event{
{Name: "foo", Attributes: []attribute.KeyValue{attribute.String("key", keyValue)}, Time: now},
{Name: "bar", Attributes: []attribute.KeyValue{attribute.Float64("double", doubleValue)}, Time: now},
},
SpanKind: trace.SpanKindInternal,
Status: tracesdk.Status{
Code: codes.Error,
Description: "interesting",
},
Resource: resource,
}, },
MessageEvents: []tracesdk.Event{ }.Snapshots()
{Name: "foo", Attributes: []attribute.KeyValue{attribute.String("key", keyValue)}, Time: now}, if err := ex.ExportSpans(context.Background(), ro); err != nil {
{Name: "bar", Attributes: []attribute.KeyValue{attribute.Float64("double", doubleValue)}, Time: now},
},
SpanKind: trace.SpanKindInternal,
Status: tracesdk.Status{
Code: codes.Error,
Description: "interesting",
},
Resource: resource,
}
if err := ex.ExportSpans(context.Background(), []*tracesdk.SpanSnapshot{testSpan}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
expectedSerializedNow, _ := json.Marshal(now) expectedSerializedNow, _ := json.Marshal(now)
got := b.String() got := b.String()
expectedOutput := `[{"SpanContext":{` + expectedOutput := `[
`"TraceID":"0102030405060708090a0b0c0d0e0f10",` + {
`"SpanID":"0102030405060708","TraceFlags":"00",` + "Name": "/foo",
`"TraceState":[` + "SpanContext": {
`{` + "TraceID": "0102030405060708090a0b0c0d0e0f10",
`"Key":"key",` + "SpanID": "0102030405060708",
`"Value":{"Type":"STRING","Value":"val"}` + "TraceFlags": "00",
`}],"Remote":false},` + "TraceState": [
`"Parent":{` + {
`"TraceID":"00000000000000000000000000000000",` + "Key": "key",
`"SpanID":"0000000000000000",` + "Value": {
`"TraceFlags":"00",` + "Type": "STRING",
`"TraceState":null,` + "Value": "val"
`"Remote":false` + }
`},` + }
`"SpanKind":1,` + ],
`"Name":"/foo",` + "Remote": false
`"StartTime":` + string(expectedSerializedNow) + "," + },
`"EndTime":` + string(expectedSerializedNow) + "," + "Parent": {
`"Attributes":[` + "TraceID": "00000000000000000000000000000000",
`{` + "SpanID": "0000000000000000",
`"Key":"key",` + "TraceFlags": "00",
`"Value":{"Type":"STRING","Value":"value"}` + "TraceState": null,
`},` + "Remote": false
`{` + },
`"Key":"double",` + "SpanKind": 1,
`"Value":{"Type":"FLOAT64","Value":123.456}` + "StartTime": ` + string(expectedSerializedNow) + `,
`}],` + "EndTime": ` + string(expectedSerializedNow) + `,
`"MessageEvents":[` + "Attributes": [
`{` + {
`"Name":"foo",` + "Key": "key",
`"Attributes":[` + "Value": {
`{` + "Type": "STRING",
`"Key":"key",` + "Value": "value"
`"Value":{"Type":"STRING","Value":"value"}` + }
`}` + },
`],` + {
`"DroppedAttributeCount":0,` + "Key": "double",
`"Time":` + string(expectedSerializedNow) + "Value": {
`},` + "Type": "FLOAT64",
`{` + "Value": 123.456
`"Name":"bar",` + }
`"Attributes":[` + }
`{` + ],
`"Key":"double",` + "Events": [
`"Value":{"Type":"FLOAT64","Value":123.456}` + {
`}` + "Name": "foo",
`],` + "Attributes": [
`"DroppedAttributeCount":0,` + {
`"Time":` + string(expectedSerializedNow) + "Key": "key",
`}` + "Value": {
`],` + "Type": "STRING",
`"Links":null,` + "Value": "value"
`"Status":{"Code":"Error","Description":"interesting"},` + }
`"DroppedAttributeCount":0,` + }
`"DroppedMessageEventCount":0,` + ],
`"DroppedLinkCount":0,` + "DroppedAttributeCount": 0,
`"ChildSpanCount":0,` + "Time": ` + string(expectedSerializedNow) + `
`"Resource":[` + },
`{` + {
`"Key":"rk1",` + "Name": "bar",
`"Value":{"Type":"STRING","Value":"rv11"}` + "Attributes": [
`}],` + {
`"InstrumentationLibrary":{` + "Key": "double",
`"Name":"",` + "Value": {
`"Version":""` + "Type": "FLOAT64",
`}}]` + "\n" "Value": 123.456
}
if got != expectedOutput { }
t.Errorf("Want: %v but got: %v", expectedOutput, got) ],
"DroppedAttributeCount": 0,
"Time": ` + string(expectedSerializedNow) + `
}
],
"Links": null,
"Status": {
"Code": "Error",
"Description": "interesting"
},
"DroppedAttributes": 0,
"DroppedEvents": 0,
"DroppedLinks": 0,
"ChildSpanCount": 0,
"Resource": [
{
"Key": "rk1",
"Value": {
"Type": "STRING",
"Value": "rv11"
}
}
],
"InstrumentationLibrary": {
"Name": "",
"Version": ""
}
} }
]
`
assert.Equal(t, expectedOutput, got)
} }
func TestExporterShutdownHonorsTimeout(t *testing.T) { func TestExporterShutdownHonorsTimeout(t *testing.T) {

View File

@ -23,7 +23,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
tracesdk "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest"
) )
func TestNewAgentClientUDPWithParamsBadHostport(t *testing.T) { func TestNewAgentClientUDPWithParamsBadHostport(t *testing.T) {
@ -113,10 +113,7 @@ func TestJaegerAgentUDPLimitBatching(t *testing.T) {
// 1500 spans, size 79559, does not fit within one UDP packet with the default size of 65000. // 1500 spans, size 79559, does not fit within one UDP packet with the default size of 65000.
n := 1500 n := 1500
s := make([]*tracesdk.SpanSnapshot, n) s := make(tracetest.SpanStubs, n).Snapshots()
for i := 0; i < n; i++ {
s[i] = &tracesdk.SpanSnapshot{}
}
exp, err := NewRawExporter( exp, err := NewRawExporter(
WithAgentEndpoint(WithAgentHost("localhost"), WithAgentPort("6831")), WithAgentEndpoint(WithAgentHost("localhost"), WithAgentPort("6831")),
@ -129,11 +126,10 @@ func TestJaegerAgentUDPLimitBatching(t *testing.T) {
} }
// generateALargeSpan generates a span with a long name. // generateALargeSpan generates a span with a long name.
func generateALargeSpan() *tracesdk.SpanSnapshot { func generateALargeSpan() tracetest.SpanStub {
span := &tracesdk.SpanSnapshot{ return tracetest.SpanStub{
Name: "a-longer-name-that-makes-it-exceeds-limit", Name: "a-longer-name-that-makes-it-exceeds-limit",
} }
return span
} }
func TestSpanExceedsMaxPacketLimit(t *testing.T) { func TestSpanExceedsMaxPacketLimit(t *testing.T) {
@ -141,10 +137,9 @@ func TestSpanExceedsMaxPacketLimit(t *testing.T) {
// 106 is the serialized size of a span with default values. // 106 is the serialized size of a span with default values.
maxSize := 106 maxSize := 106
span := generateALargeSpan()
largeSpans := []*tracesdk.SpanSnapshot{span, {}} largeSpans := tracetest.SpanStubs{generateALargeSpan(), {}}.Snapshots()
normalSpans := []*tracesdk.SpanSnapshot{{}, {}} normalSpans := tracetest.SpanStubs{{}, {}}.Snapshots()
exp, err := NewRawExporter( exp, err := NewRawExporter(
WithAgentEndpoint(WithAgentHost("localhost"), WithAgentPort("6831"), WithMaxPacketSize(maxSize+1)), WithAgentEndpoint(WithAgentHost("localhost"), WithAgentPort("6831"), WithMaxPacketSize(maxSize+1)),
@ -161,7 +156,7 @@ func TestEmitBatchWithMultipleErrors(t *testing.T) {
otel.SetErrorHandler(errorHandler{t}) otel.SetErrorHandler(errorHandler{t})
span := generateALargeSpan() span := generateALargeSpan()
largeSpans := []*tracesdk.SpanSnapshot{span, span} largeSpans := tracetest.SpanStubs{span, span}.Snapshots()
// make max packet size smaller than span // make max packet size smaller than span
maxSize := len(span.Name) maxSize := len(span.Name)
exp, err := NewRawExporter( exp, err := NewRawExporter(

View File

@ -104,7 +104,7 @@ type Exporter struct {
var _ sdktrace.SpanExporter = (*Exporter)(nil) var _ sdktrace.SpanExporter = (*Exporter)(nil)
// ExportSpans transforms and exports OpenTelemetry spans to Jaeger. // ExportSpans transforms and exports OpenTelemetry spans to Jaeger.
func (e *Exporter) ExportSpans(ctx context.Context, spans []*sdktrace.SpanSnapshot) error { func (e *Exporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
// Return fast if context is already canceled or Exporter shutdown. // Return fast if context is already canceled or Exporter shutdown.
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -148,41 +148,42 @@ func (e *Exporter) Shutdown(ctx context.Context) error {
return e.uploader.shutdown(ctx) return e.uploader.shutdown(ctx)
} }
func spanSnapshotToThrift(ss *sdktrace.SpanSnapshot) *gen.Span { func spanToThrift(ss sdktrace.ReadOnlySpan) *gen.Span {
tags := make([]*gen.Tag, 0, len(ss.Attributes)) attr := ss.Attributes()
for _, kv := range ss.Attributes { tags := make([]*gen.Tag, 0, len(attr))
for _, kv := range attr {
tag := keyValueToTag(kv) tag := keyValueToTag(kv)
if tag != nil { if tag != nil {
tags = append(tags, tag) tags = append(tags, tag)
} }
} }
if il := ss.InstrumentationLibrary; il.Name != "" { if il := ss.InstrumentationLibrary(); il.Name != "" {
tags = append(tags, getStringTag(keyInstrumentationLibraryName, il.Name)) tags = append(tags, getStringTag(keyInstrumentationLibraryName, il.Name))
if il.Version != "" { if il.Version != "" {
tags = append(tags, getStringTag(keyInstrumentationLibraryVersion, il.Version)) tags = append(tags, getStringTag(keyInstrumentationLibraryVersion, il.Version))
} }
} }
if ss.SpanKind != trace.SpanKindInternal { if ss.SpanKind() != trace.SpanKindInternal {
tags = append(tags, tags = append(tags,
getStringTag(keySpanKind, ss.SpanKind.String()), getStringTag(keySpanKind, ss.SpanKind().String()),
) )
} }
if ss.Status.Code != codes.Unset { if ss.Status().Code != codes.Unset {
tags = append(tags, getInt64Tag(keyStatusCode, int64(ss.Status.Code))) tags = append(tags, getInt64Tag(keyStatusCode, int64(ss.Status().Code)))
if ss.Status.Description != "" { if ss.Status().Description != "" {
tags = append(tags, getStringTag(keyStatusMessage, ss.Status.Description)) tags = append(tags, getStringTag(keyStatusMessage, ss.Status().Description))
} }
if ss.Status.Code == codes.Error { if ss.Status().Code == codes.Error {
tags = append(tags, getBoolTag(keyError, true)) tags = append(tags, getBoolTag(keyError, true))
} }
} }
var logs []*gen.Log var logs []*gen.Log
for _, a := range ss.MessageEvents { for _, a := range ss.Events() {
nTags := len(a.Attributes) nTags := len(a.Attributes)
if a.Name != "" { if a.Name != "" {
nTags++ nTags++
@ -212,7 +213,7 @@ func spanSnapshotToThrift(ss *sdktrace.SpanSnapshot) *gen.Span {
} }
var refs []*gen.SpanRef var refs []*gen.SpanRef
for _, link := range ss.Links { for _, link := range ss.Links() {
tid := link.SpanContext.TraceID() tid := link.SpanContext.TraceID()
sid := link.SpanContext.SpanID() sid := link.SpanContext.SpanID()
refs = append(refs, &gen.SpanRef{ refs = append(refs, &gen.SpanRef{
@ -223,18 +224,18 @@ func spanSnapshotToThrift(ss *sdktrace.SpanSnapshot) *gen.Span {
}) })
} }
tid := ss.SpanContext.TraceID() tid := ss.SpanContext().TraceID()
sid := ss.SpanContext.SpanID() sid := ss.SpanContext().SpanID()
psid := ss.Parent.SpanID() psid := ss.Parent().SpanID()
return &gen.Span{ return &gen.Span{
TraceIdHigh: int64(binary.BigEndian.Uint64(tid[0:8])), TraceIdHigh: int64(binary.BigEndian.Uint64(tid[0:8])),
TraceIdLow: int64(binary.BigEndian.Uint64(tid[8:16])), TraceIdLow: int64(binary.BigEndian.Uint64(tid[8:16])),
SpanId: int64(binary.BigEndian.Uint64(sid[:])), SpanId: int64(binary.BigEndian.Uint64(sid[:])),
ParentSpanId: int64(binary.BigEndian.Uint64(psid[:])), ParentSpanId: int64(binary.BigEndian.Uint64(psid[:])),
OperationName: ss.Name, // TODO: if span kind is added then add prefix "Sent"/"Recv" OperationName: ss.Name(), // TODO: if span kind is added then add prefix "Sent"/"Recv"
Flags: int32(ss.SpanContext.TraceFlags()), Flags: int32(ss.SpanContext().TraceFlags()),
StartTime: ss.StartTime.UnixNano() / 1000, StartTime: ss.StartTime().UnixNano() / 1000,
Duration: ss.EndTime.Sub(ss.StartTime).Nanoseconds() / 1000, Duration: ss.EndTime().Sub(ss.StartTime()).Nanoseconds() / 1000,
Tags: tags, Tags: tags,
Logs: logs, Logs: logs,
References: refs, References: refs,
@ -308,9 +309,8 @@ func getBoolTag(k string, b bool) *gen.Tag {
} }
} }
// jaegerBatchList transforms a slice of SpanSnapshot into a slice of jaeger // jaegerBatchList transforms a slice of spans into a slice of jaeger Batch.
// Batch. func jaegerBatchList(ssl []sdktrace.ReadOnlySpan, defaultServiceName string) []*gen.Batch {
func jaegerBatchList(ssl []*sdktrace.SpanSnapshot, defaultServiceName string) []*gen.Batch {
if len(ssl) == 0 { if len(ssl) == 0 {
return nil return nil
} }
@ -322,15 +322,15 @@ func jaegerBatchList(ssl []*sdktrace.SpanSnapshot, defaultServiceName string) []
continue continue
} }
resourceKey := ss.Resource.Equivalent() resourceKey := ss.Resource().Equivalent()
batch, bOK := batchDict[resourceKey] batch, bOK := batchDict[resourceKey]
if !bOK { if !bOK {
batch = &gen.Batch{ batch = &gen.Batch{
Process: process(ss.Resource, defaultServiceName), Process: process(ss.Resource(), defaultServiceName),
Spans: []*gen.Span{}, Spans: []*gen.Span{},
} }
} }
batch.Spans = append(batch.Spans, spanSnapshotToThrift(ss)) batch.Spans = append(batch.Spans, spanToThrift(ss))
batchDict[resourceKey] = batch batchDict[resourceKey] = batch
} }

View File

@ -22,6 +22,7 @@ import (
"go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/instrumentation"
tracesdk "go.opentelemetry.io/otel/sdk/trace" tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
@ -49,12 +50,12 @@ func init() {
}) })
} }
func spans(n int) []*tracesdk.SpanSnapshot { func spans(n int) []tracesdk.ReadOnlySpan {
now := time.Now() now := time.Now()
s := make([]*tracesdk.SpanSnapshot, n) s := make(tracetest.SpanStubs, n)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
name := fmt.Sprintf("span %d", i) name := fmt.Sprintf("span %d", i)
s[i] = &tracesdk.SpanSnapshot{ s[i] = tracetest.SpanStub{
SpanContext: spanContext, SpanContext: spanContext,
Name: name, Name: name,
StartTime: now, StartTime: now,
@ -65,7 +66,7 @@ func spans(n int) []*tracesdk.SpanSnapshot {
}, },
} }
} }
return s return s.Snapshots()
} }
func benchmarkExportSpans(b *testing.B, o EndpointOption, i int) { func benchmarkExportSpans(b *testing.B, o EndpointOption, i int) {

View File

@ -36,6 +36,7 @@ import (
"go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace" sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/semconv" "go.opentelemetry.io/otel/semconv"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
@ -219,12 +220,12 @@ func Test_spanSnapshotToThrift(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
data *sdktrace.SpanSnapshot data tracetest.SpanStub
want *gen.Span want *gen.Span
}{ }{
{ {
name: "no status description", name: "no status description",
data: &sdktrace.SpanSnapshot{ data: tracetest.SpanStub{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID, TraceID: traceID,
SpanID: spanID, SpanID: spanID,
@ -258,7 +259,7 @@ func Test_spanSnapshotToThrift(t *testing.T) {
}, },
{ {
name: "no parent", name: "no parent",
data: &sdktrace.SpanSnapshot{ data: tracetest.SpanStub{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID, TraceID: traceID,
SpanID: spanID, SpanID: spanID,
@ -279,7 +280,7 @@ func Test_spanSnapshotToThrift(t *testing.T) {
attribute.Float64("double", doubleValue), attribute.Float64("double", doubleValue),
attribute.Int64("int", intValue), attribute.Int64("int", intValue),
}, },
MessageEvents: []sdktrace.Event{ Events: []sdktrace.Event{
{ {
Name: eventNameValue, Name: eventNameValue,
Attributes: []attribute.KeyValue{attribute.String("k1", keyValue)}, Attributes: []attribute.KeyValue{attribute.String("k1", keyValue)},
@ -349,7 +350,7 @@ func Test_spanSnapshotToThrift(t *testing.T) {
}, },
{ {
name: "with parent", name: "with parent",
data: &sdktrace.SpanSnapshot{ data: tracetest.SpanStub{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID, TraceID: traceID,
SpanID: spanID, SpanID: spanID,
@ -408,7 +409,7 @@ func Test_spanSnapshotToThrift(t *testing.T) {
}, },
{ {
name: "resources do not affect the tags", name: "resources do not affect the tags",
data: &sdktrace.SpanSnapshot{ data: tracetest.SpanStub{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID, TraceID: traceID,
SpanID: spanID, SpanID: spanID,
@ -452,7 +453,7 @@ func Test_spanSnapshotToThrift(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := spanSnapshotToThrift(tt.data) got := spanToThrift(tt.data.Snapshot())
sort.Slice(got.Tags, func(i, j int) bool { sort.Slice(got.Tags, func(i, j int) bool {
return got.Tags[i].Key < got.Tags[j].Key return got.Tags[i].Key < got.Tags[j].Key
}) })
@ -496,7 +497,7 @@ func TestExporterExportSpansHonorsCancel(t *testing.T) {
e, err := NewRawExporter(withTestCollectorEndpoint()) e, err := NewRawExporter(withTestCollectorEndpoint())
require.NoError(t, err) require.NoError(t, err)
now := time.Now() now := time.Now()
ss := []*sdktrace.SpanSnapshot{ ss := tracetest.SpanStubs{
{ {
Name: "s1", Name: "s1",
Resource: resource.NewWithAttributes( Resource: resource.NewWithAttributes(
@ -519,14 +520,14 @@ func TestExporterExportSpansHonorsCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
assert.EqualError(t, e.ExportSpans(ctx, ss), context.Canceled.Error()) assert.EqualError(t, e.ExportSpans(ctx, ss.Snapshots()), context.Canceled.Error())
} }
func TestExporterExportSpansHonorsTimeout(t *testing.T) { func TestExporterExportSpansHonorsTimeout(t *testing.T) {
e, err := NewRawExporter(withTestCollectorEndpoint()) e, err := NewRawExporter(withTestCollectorEndpoint())
require.NoError(t, err) require.NoError(t, err)
now := time.Now() now := time.Now()
ss := []*sdktrace.SpanSnapshot{ ss := tracetest.SpanStubs{
{ {
Name: "s1", Name: "s1",
Resource: resource.NewWithAttributes( Resource: resource.NewWithAttributes(
@ -550,7 +551,7 @@ func TestExporterExportSpansHonorsTimeout(t *testing.T) {
defer cancel() defer cancel()
<-ctx.Done() <-ctx.Done()
assert.EqualError(t, e.ExportSpans(ctx, ss), context.DeadlineExceeded.Error()) assert.EqualError(t, e.ExportSpans(ctx, ss.Snapshots()), context.DeadlineExceeded.Error())
} }
func TestJaegerBatchList(t *testing.T) { func TestJaegerBatchList(t *testing.T) {
@ -562,19 +563,19 @@ func TestJaegerBatchList(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
spanSnapshotList []*sdktrace.SpanSnapshot roSpans []sdktrace.ReadOnlySpan
defaultServiceName string defaultServiceName string
expectedBatchList []*gen.Batch expectedBatchList []*gen.Batch
}{ }{
{ {
name: "no span shots", name: "no span shots",
spanSnapshotList: nil, roSpans: nil,
expectedBatchList: nil, expectedBatchList: nil,
}, },
{ {
name: "span's snapshot contains nil span", name: "span's snapshot contains nil span",
spanSnapshotList: []*sdktrace.SpanSnapshot{ roSpans: []sdktrace.ReadOnlySpan{
{ tracetest.SpanStub{
Name: "s1", Name: "s1",
Resource: resource.NewWithAttributes( Resource: resource.NewWithAttributes(
semconv.ServiceNameKey.String("name"), semconv.ServiceNameKey.String("name"),
@ -582,7 +583,7 @@ func TestJaegerBatchList(t *testing.T) {
), ),
StartTime: now, StartTime: now,
EndTime: now, EndTime: now,
}, }.Snapshot(),
nil, nil,
}, },
expectedBatchList: []*gen.Batch{ expectedBatchList: []*gen.Batch{
@ -607,7 +608,7 @@ func TestJaegerBatchList(t *testing.T) {
}, },
{ {
name: "merge spans that have the same resources", name: "merge spans that have the same resources",
spanSnapshotList: []*sdktrace.SpanSnapshot{ roSpans: tracetest.SpanStubs{
{ {
Name: "s1", Name: "s1",
Resource: resource.NewWithAttributes( Resource: resource.NewWithAttributes(
@ -635,7 +636,7 @@ func TestJaegerBatchList(t *testing.T) {
StartTime: now, StartTime: now,
EndTime: now, EndTime: now,
}, },
}, }.Snapshots(),
expectedBatchList: []*gen.Batch{ expectedBatchList: []*gen.Batch{
{ {
Process: &gen.Process{ Process: &gen.Process{
@ -682,7 +683,7 @@ func TestJaegerBatchList(t *testing.T) {
}, },
{ {
name: "no service name in spans", name: "no service name in spans",
spanSnapshotList: []*sdktrace.SpanSnapshot{ roSpans: tracetest.SpanStubs{
{ {
Name: "s1", Name: "s1",
Resource: resource.NewWithAttributes( Resource: resource.NewWithAttributes(
@ -691,8 +692,7 @@ func TestJaegerBatchList(t *testing.T) {
StartTime: now, StartTime: now,
EndTime: now, EndTime: now,
}, },
nil, }.Snapshots(),
},
defaultServiceName: "default service name", defaultServiceName: "default service name",
expectedBatchList: []*gen.Batch{ expectedBatchList: []*gen.Batch{
{ {
@ -718,7 +718,7 @@ func TestJaegerBatchList(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
batchList := jaegerBatchList(tc.spanSnapshotList, tc.defaultServiceName) batchList := jaegerBatchList(tc.roSpans, tc.defaultServiceName)
assert.ElementsMatch(t, tc.expectedBatchList, batchList) assert.ElementsMatch(t, tc.expectedBatchList, batchList)
}) })

View File

@ -51,7 +51,7 @@ func init() {
} }
} }
func toZipkinSpanModels(batch []*tracesdk.SpanSnapshot) []zkmodel.SpanModel { func toZipkinSpanModels(batch []tracesdk.ReadOnlySpan) []zkmodel.SpanModel {
models := make([]zkmodel.SpanModel, 0, len(batch)) models := make([]zkmodel.SpanModel, 0, len(batch))
for _, data := range batch { for _, data := range batch {
models = append(models, toZipkinSpanModel(data)) models = append(models, toZipkinSpanModel(data))
@ -69,28 +69,28 @@ func getServiceName(attrs []attribute.KeyValue) string {
return defaultServiceName return defaultServiceName
} }
func toZipkinSpanModel(data *tracesdk.SpanSnapshot) zkmodel.SpanModel { func toZipkinSpanModel(data tracesdk.ReadOnlySpan) zkmodel.SpanModel {
return zkmodel.SpanModel{ return zkmodel.SpanModel{
SpanContext: toZipkinSpanContext(data), SpanContext: toZipkinSpanContext(data),
Name: data.Name, Name: data.Name(),
Kind: toZipkinKind(data.SpanKind), Kind: toZipkinKind(data.SpanKind()),
Timestamp: data.StartTime, Timestamp: data.StartTime(),
Duration: data.EndTime.Sub(data.StartTime), Duration: data.EndTime().Sub(data.StartTime()),
Shared: false, Shared: false,
LocalEndpoint: &zkmodel.Endpoint{ LocalEndpoint: &zkmodel.Endpoint{
ServiceName: getServiceName(data.Resource.Attributes()), ServiceName: getServiceName(data.Resource().Attributes()),
}, },
RemoteEndpoint: toZipkinRemoteEndpoint(data), RemoteEndpoint: toZipkinRemoteEndpoint(data),
Annotations: toZipkinAnnotations(data.MessageEvents), Annotations: toZipkinAnnotations(data.Events()),
Tags: toZipkinTags(data), Tags: toZipkinTags(data),
} }
} }
func toZipkinSpanContext(data *tracesdk.SpanSnapshot) zkmodel.SpanContext { func toZipkinSpanContext(data tracesdk.ReadOnlySpan) zkmodel.SpanContext {
return zkmodel.SpanContext{ return zkmodel.SpanContext{
TraceID: toZipkinTraceID(data.SpanContext.TraceID()), TraceID: toZipkinTraceID(data.SpanContext().TraceID()),
ID: toZipkinID(data.SpanContext.SpanID()), ID: toZipkinID(data.SpanContext().SpanID()),
ParentID: toZipkinParentID(data.Parent.SpanID()), ParentID: toZipkinParentID(data.Parent().SpanID()),
Debug: false, Debug: false,
Sampled: nil, Sampled: nil,
Err: nil, Err: nil,
@ -174,9 +174,10 @@ var extraZipkinTags = []string{
keyInstrumentationLibraryVersion, keyInstrumentationLibraryVersion,
} }
func toZipkinTags(data *tracesdk.SpanSnapshot) map[string]string { func toZipkinTags(data tracesdk.ReadOnlySpan) map[string]string {
m := make(map[string]string, len(data.Attributes)+len(extraZipkinTags)) attr := data.Attributes()
for _, kv := range data.Attributes { m := make(map[string]string, len(attr)+len(extraZipkinTags))
for _, kv := range attr {
switch kv.Value.Type() { switch kv.Value.Type() {
// For array attributes, serialize as JSON list string. // For array attributes, serialize as JSON list string.
case attribute.ARRAY: case attribute.ARRAY:
@ -187,17 +188,17 @@ func toZipkinTags(data *tracesdk.SpanSnapshot) map[string]string {
} }
} }
if data.Status.Code != codes.Unset { if data.Status().Code != codes.Unset {
m["otel.status_code"] = data.Status.Code.String() m["otel.status_code"] = data.Status().Code.String()
} }
if data.Status.Code == codes.Error { if data.Status().Code == codes.Error {
m["error"] = data.Status.Description m["error"] = data.Status().Description
} else { } else {
delete(m, "error") delete(m, "error")
} }
if il := data.InstrumentationLibrary; il.Name != "" { if il := data.InstrumentationLibrary(); il.Name != "" {
m[keyInstrumentationLibraryName] = il.Name m[keyInstrumentationLibraryName] = il.Name
if il.Version != "" { if il.Version != "" {
m[keyInstrumentationLibraryVersion] = il.Version m[keyInstrumentationLibraryVersion] = il.Version
@ -223,15 +224,15 @@ var remoteEndpointKeyRank = map[attribute.Key]int{
semconv.DBNameKey: 6, semconv.DBNameKey: 6,
} }
func toZipkinRemoteEndpoint(data *sdktrace.SpanSnapshot) *zkmodel.Endpoint { func toZipkinRemoteEndpoint(data sdktrace.ReadOnlySpan) *zkmodel.Endpoint {
// Should be set only for client or producer kind // Should be set only for client or producer kind
if data.SpanKind != trace.SpanKindClient && if sk := data.SpanKind(); sk != trace.SpanKindClient && sk != trace.SpanKindProducer {
data.SpanKind != trace.SpanKindProducer {
return nil return nil
} }
attr := data.Attributes()
var endpointAttr attribute.KeyValue var endpointAttr attribute.KeyValue
for _, kv := range data.Attributes { for _, kv := range attr {
rank, ok := remoteEndpointKeyRank[kv.Key] rank, ok := remoteEndpointKeyRank[kv.Key]
if !ok { if !ok {
continue continue
@ -256,7 +257,7 @@ func toZipkinRemoteEndpoint(data *sdktrace.SpanSnapshot) *zkmodel.Endpoint {
} }
} }
return remoteEndpointPeerIPWithPort(endpointAttr.Value.AsString(), data.Attributes) return remoteEndpointPeerIPWithPort(endpointAttr.Value.AsString(), attr)
} }
// Handles `net.peer.ip` remote endpoint separately (should include `net.peer.ip` // Handles `net.peer.ip` remote endpoint separately (should include `net.peer.ip`

View File

@ -31,6 +31,7 @@ import (
"go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace" tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/semconv" "go.opentelemetry.io/otel/semconv"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
@ -40,7 +41,7 @@ func TestModelConversion(t *testing.T) {
semconv.ServiceNameKey.String("model-test"), semconv.ServiceNameKey.String("model-test"),
) )
inputBatch := []*tracesdk.SpanSnapshot{ inputBatch := tracetest.SpanStubs{
// typical span data // typical span data
{ {
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
@ -60,7 +61,7 @@ func TestModelConversion(t *testing.T) {
attribute.String("attr2", "bar"), attribute.String("attr2", "bar"),
attribute.Array("attr3", []int{0, 1, 2}), attribute.Array("attr3", []int{0, 1, 2}),
}, },
MessageEvents: []tracesdk.Event{ Events: []tracesdk.Event{
{ {
Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC), Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC),
Name: "ev1", Name: "ev1",
@ -95,7 +96,7 @@ func TestModelConversion(t *testing.T) {
attribute.Int64("attr1", 42), attribute.Int64("attr1", 42),
attribute.String("attr2", "bar"), attribute.String("attr2", "bar"),
}, },
MessageEvents: []tracesdk.Event{ Events: []tracesdk.Event{
{ {
Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC), Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC),
Name: "ev1", Name: "ev1",
@ -133,7 +134,7 @@ func TestModelConversion(t *testing.T) {
attribute.Int64("attr1", 42), attribute.Int64("attr1", 42),
attribute.String("attr2", "bar"), attribute.String("attr2", "bar"),
}, },
MessageEvents: []tracesdk.Event{ Events: []tracesdk.Event{
{ {
Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC), Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC),
Name: "ev1", Name: "ev1",
@ -171,7 +172,7 @@ func TestModelConversion(t *testing.T) {
attribute.Int64("attr1", 42), attribute.Int64("attr1", 42),
attribute.String("attr2", "bar"), attribute.String("attr2", "bar"),
}, },
MessageEvents: []tracesdk.Event{ Events: []tracesdk.Event{
{ {
Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC), Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC),
Name: "ev1", Name: "ev1",
@ -212,7 +213,7 @@ func TestModelConversion(t *testing.T) {
attribute.String("net.peer.ip", "1.2.3.4"), attribute.String("net.peer.ip", "1.2.3.4"),
attribute.Int64("net.peer.port", 9876), attribute.Int64("net.peer.port", 9876),
}, },
MessageEvents: []tracesdk.Event{ Events: []tracesdk.Event{
{ {
Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC), Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC),
Name: "ev1", Name: "ev1",
@ -250,7 +251,7 @@ func TestModelConversion(t *testing.T) {
attribute.Int64("attr1", 42), attribute.Int64("attr1", 42),
attribute.String("attr2", "bar"), attribute.String("attr2", "bar"),
}, },
MessageEvents: []tracesdk.Event{ Events: []tracesdk.Event{
{ {
Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC), Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC),
Name: "ev1", Name: "ev1",
@ -288,7 +289,7 @@ func TestModelConversion(t *testing.T) {
attribute.Int64("attr1", 42), attribute.Int64("attr1", 42),
attribute.String("attr2", "bar"), attribute.String("attr2", "bar"),
}, },
MessageEvents: []tracesdk.Event{ Events: []tracesdk.Event{
{ {
Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC), Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC),
Name: "ev1", Name: "ev1",
@ -326,7 +327,7 @@ func TestModelConversion(t *testing.T) {
attribute.Int64("attr1", 42), attribute.Int64("attr1", 42),
attribute.String("attr2", "bar"), attribute.String("attr2", "bar"),
}, },
MessageEvents: nil, Events: nil,
Status: tracesdk.Status{ Status: tracesdk.Status{
Code: codes.Error, Code: codes.Error,
Description: "404, file not found", Description: "404, file not found",
@ -350,7 +351,7 @@ func TestModelConversion(t *testing.T) {
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
attribute.String("error", "false"), attribute.String("error", "false"),
}, },
MessageEvents: []tracesdk.Event{ Events: []tracesdk.Event{
{ {
Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC), Time: time.Date(2020, time.March, 11, 19, 24, 30, 0, time.UTC),
Name: "ev1", Name: "ev1",
@ -366,7 +367,7 @@ func TestModelConversion(t *testing.T) {
}, },
Resource: resource, Resource: resource,
}, },
} }.Snapshots()
expectedOutputBatch := []zkmodel.SpanModel{ expectedOutputBatch := []zkmodel.SpanModel{
// model for typical span data // model for typical span data
@ -733,12 +734,12 @@ func TestTagsTransformation(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
data *tracesdk.SpanSnapshot data tracetest.SpanStub
want map[string]string want map[string]string
}{ }{
{ {
name: "attributes", name: "attributes",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
attribute.String("key", keyValue), attribute.String("key", keyValue),
attribute.Float64("double", doubleValue), attribute.Float64("double", doubleValue),
@ -755,12 +756,12 @@ func TestTagsTransformation(t *testing.T) {
}, },
{ {
name: "no attributes", name: "no attributes",
data: &tracesdk.SpanSnapshot{}, data: tracetest.SpanStub{},
want: nil, want: nil,
}, },
{ {
name: "omit-noerror", name: "omit-noerror",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
attribute.Bool("error", false), attribute.Bool("error", false),
}, },
@ -769,7 +770,7 @@ func TestTagsTransformation(t *testing.T) {
}, },
{ {
name: "statusCode", name: "statusCode",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
attribute.String("key", keyValue), attribute.String("key", keyValue),
attribute.Bool("error", true), attribute.Bool("error", true),
@ -787,14 +788,14 @@ func TestTagsTransformation(t *testing.T) {
}, },
{ {
name: "instrLib-empty", name: "instrLib-empty",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
InstrumentationLibrary: instrumentation.Library{}, InstrumentationLibrary: instrumentation.Library{},
}, },
want: nil, want: nil,
}, },
{ {
name: "instrLib-noversion", name: "instrLib-noversion",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
Attributes: []attribute.KeyValue{}, Attributes: []attribute.KeyValue{},
InstrumentationLibrary: instrumentation.Library{ InstrumentationLibrary: instrumentation.Library{
Name: instrLibName, Name: instrLibName,
@ -806,7 +807,7 @@ func TestTagsTransformation(t *testing.T) {
}, },
{ {
name: "instrLib-with-version", name: "instrLib-with-version",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
Attributes: []attribute.KeyValue{}, Attributes: []attribute.KeyValue{},
InstrumentationLibrary: instrumentation.Library{ InstrumentationLibrary: instrumentation.Library{
Name: instrLibName, Name: instrLibName,
@ -821,7 +822,7 @@ func TestTagsTransformation(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := toZipkinTags(tt.data) got := toZipkinTags(tt.data.Snapshot())
if diff := cmp.Diff(got, tt.want); diff != "" { if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("Diff%v", diff) t.Errorf("Diff%v", diff)
} }
@ -832,12 +833,12 @@ func TestTagsTransformation(t *testing.T) {
func TestRemoteEndpointTransformation(t *testing.T) { func TestRemoteEndpointTransformation(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
data *tracesdk.SpanSnapshot data tracetest.SpanStub
want *zkmodel.Endpoint want *zkmodel.Endpoint
}{ }{
{ {
name: "nil-not-applicable", name: "nil-not-applicable",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
SpanKind: trace.SpanKindClient, SpanKind: trace.SpanKindClient,
Attributes: []attribute.KeyValue{}, Attributes: []attribute.KeyValue{},
}, },
@ -845,7 +846,7 @@ func TestRemoteEndpointTransformation(t *testing.T) {
}, },
{ {
name: "nil-not-found", name: "nil-not-found",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
SpanKind: trace.SpanKindConsumer, SpanKind: trace.SpanKindConsumer,
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
attribute.String("attr", "test"), attribute.String("attr", "test"),
@ -855,7 +856,7 @@ func TestRemoteEndpointTransformation(t *testing.T) {
}, },
{ {
name: "peer-service-rank", name: "peer-service-rank",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
SpanKind: trace.SpanKindProducer, SpanKind: trace.SpanKindProducer,
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
semconv.PeerServiceKey.String("peer-service-test"), semconv.PeerServiceKey.String("peer-service-test"),
@ -869,7 +870,7 @@ func TestRemoteEndpointTransformation(t *testing.T) {
}, },
{ {
name: "http-host-rank", name: "http-host-rank",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
SpanKind: trace.SpanKindProducer, SpanKind: trace.SpanKindProducer,
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
semconv.HTTPHostKey.String("http-host-test"), semconv.HTTPHostKey.String("http-host-test"),
@ -882,7 +883,7 @@ func TestRemoteEndpointTransformation(t *testing.T) {
}, },
{ {
name: "db-name-rank", name: "db-name-rank",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
SpanKind: trace.SpanKindProducer, SpanKind: trace.SpanKindProducer,
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
attribute.String("foo", "bar"), attribute.String("foo", "bar"),
@ -895,7 +896,7 @@ func TestRemoteEndpointTransformation(t *testing.T) {
}, },
{ {
name: "peer-hostname-rank", name: "peer-hostname-rank",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
SpanKind: trace.SpanKindProducer, SpanKind: trace.SpanKindProducer,
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
keyPeerHostname.String("peer-hostname-test"), keyPeerHostname.String("peer-hostname-test"),
@ -910,7 +911,7 @@ func TestRemoteEndpointTransformation(t *testing.T) {
}, },
{ {
name: "peer-address-rank", name: "peer-address-rank",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
SpanKind: trace.SpanKindProducer, SpanKind: trace.SpanKindProducer,
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
keyPeerAddress.String("peer-address-test"), keyPeerAddress.String("peer-address-test"),
@ -924,7 +925,7 @@ func TestRemoteEndpointTransformation(t *testing.T) {
}, },
{ {
name: "net-peer-invalid-ip", name: "net-peer-invalid-ip",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
SpanKind: trace.SpanKindProducer, SpanKind: trace.SpanKindProducer,
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
semconv.NetPeerIPKey.String("INVALID"), semconv.NetPeerIPKey.String("INVALID"),
@ -934,7 +935,7 @@ func TestRemoteEndpointTransformation(t *testing.T) {
}, },
{ {
name: "net-peer-ipv6-no-port", name: "net-peer-ipv6-no-port",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
SpanKind: trace.SpanKindProducer, SpanKind: trace.SpanKindProducer,
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
semconv.NetPeerIPKey.String("0:0:1:5ee:bad:c0de:0:0"), semconv.NetPeerIPKey.String("0:0:1:5ee:bad:c0de:0:0"),
@ -946,7 +947,7 @@ func TestRemoteEndpointTransformation(t *testing.T) {
}, },
{ {
name: "net-peer-ipv4-port", name: "net-peer-ipv4-port",
data: &tracesdk.SpanSnapshot{ data: tracetest.SpanStub{
SpanKind: trace.SpanKindProducer, SpanKind: trace.SpanKindProducer,
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
semconv.NetPeerIPKey.String("1.2.3.4"), semconv.NetPeerIPKey.String("1.2.3.4"),
@ -961,7 +962,7 @@ func TestRemoteEndpointTransformation(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := toZipkinRemoteEndpoint(tt.data) got := toZipkinRemoteEndpoint(tt.data.Snapshot())
if diff := cmp.Diff(got, tt.want); diff != "" { if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("Diff%v", diff) t.Errorf("Diff%v", diff)
} }

View File

@ -31,9 +31,7 @@ import (
sdktrace "go.opentelemetry.io/otel/sdk/trace" sdktrace "go.opentelemetry.io/otel/sdk/trace"
) )
// Exporter exports SpanSnapshots to the zipkin collector. It implements // Exporter exports spans to the zipkin collector.
// the SpanBatcher interface, so it needs to be used together with the
// WithBatcher option when setting up the exporter pipeline.
type Exporter struct { type Exporter struct {
url string url string
client *http.Client client *http.Client
@ -133,8 +131,8 @@ func InstallNewPipeline(collectorURL string, opts ...Option) error {
return nil return nil
} }
// ExportSpans exports SpanSnapshots to a Zipkin receiver. // ExportSpans exports spans to a Zipkin receiver.
func (e *Exporter) ExportSpans(ctx context.Context, ss []*sdktrace.SpanSnapshot) error { func (e *Exporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
e.stoppedMu.RLock() e.stoppedMu.RLock()
stopped := e.stopped stopped := e.stopped
e.stoppedMu.RUnlock() e.stoppedMu.RUnlock()
@ -143,11 +141,11 @@ func (e *Exporter) ExportSpans(ctx context.Context, ss []*sdktrace.SpanSnapshot)
return nil return nil
} }
if len(ss) == 0 { if len(spans) == 0 {
e.logf("no spans to export") e.logf("no spans to export")
return nil return nil
} }
models := toZipkinSpanModels(ss) models := toZipkinSpanModels(spans)
body, err := json.Marshal(models) body, err := json.Marshal(models)
if err != nil { if err != nil {
return e.errf("failed to serialize zipkin models to JSON: %v", err) return e.errf("failed to serialize zipkin models to JSON: %v", err)

View File

@ -34,6 +34,7 @@ import (
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace" sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/semconv" "go.opentelemetry.io/otel/semconv"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
@ -233,19 +234,19 @@ func TestExportSpans(t *testing.T) {
semconv.ServiceNameKey.String("exporter-test"), semconv.ServiceNameKey.String("exporter-test"),
) )
spans := []*sdktrace.SpanSnapshot{ spans := tracetest.SpanStubs{
// parent // parent
{ {
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: trace.TraceID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}, TraceID: trace.TraceID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F},
SpanID: trace.SpanID{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8}, SpanID: trace.SpanID{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8},
}), }),
SpanKind: trace.SpanKindServer, SpanKind: trace.SpanKindServer,
Name: "foo", Name: "foo",
StartTime: time.Date(2020, time.March, 11, 19, 24, 0, 0, time.UTC), StartTime: time.Date(2020, time.March, 11, 19, 24, 0, 0, time.UTC),
EndTime: time.Date(2020, time.March, 11, 19, 25, 0, 0, time.UTC), EndTime: time.Date(2020, time.March, 11, 19, 25, 0, 0, time.UTC),
Attributes: nil, Attributes: nil,
MessageEvents: nil, Events: nil,
Status: sdktrace.Status{ Status: sdktrace.Status{
Code: codes.Error, Code: codes.Error,
Description: "404, file not found", Description: "404, file not found",
@ -262,19 +263,19 @@ func TestExportSpans(t *testing.T) {
TraceID: trace.TraceID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}, TraceID: trace.TraceID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F},
SpanID: trace.SpanID{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8}, SpanID: trace.SpanID{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8},
}), }),
SpanKind: trace.SpanKindServer, SpanKind: trace.SpanKindServer,
Name: "bar", Name: "bar",
StartTime: time.Date(2020, time.March, 11, 19, 24, 15, 0, time.UTC), StartTime: time.Date(2020, time.March, 11, 19, 24, 15, 0, time.UTC),
EndTime: time.Date(2020, time.March, 11, 19, 24, 45, 0, time.UTC), EndTime: time.Date(2020, time.March, 11, 19, 24, 45, 0, time.UTC),
Attributes: nil, Attributes: nil,
MessageEvents: nil, Events: nil,
Status: sdktrace.Status{ Status: sdktrace.Status{
Code: codes.Error, Code: codes.Error,
Description: "403, forbidden", Description: "403, forbidden",
}, },
Resource: resource, Resource: resource,
}, },
} }.Snapshots()
models := []zkmodel.SpanModel{ models := []zkmodel.SpanModel{
// model of parent // model of parent
{ {

View File

@ -63,15 +63,15 @@ type BatchSpanProcessorOptions struct {
} }
// batchSpanProcessor is a SpanProcessor that batches asynchronously-received // batchSpanProcessor is a SpanProcessor that batches asynchronously-received
// SpanSnapshots and sends them to a trace.Exporter when complete. // spans and sends them to a trace.Exporter when complete.
type batchSpanProcessor struct { type batchSpanProcessor struct {
e SpanExporter e SpanExporter
o BatchSpanProcessorOptions o BatchSpanProcessorOptions
queue chan *SpanSnapshot queue chan ReadOnlySpan
dropped uint32 dropped uint32
batch []*SpanSnapshot batch []ReadOnlySpan
batchMutex sync.Mutex batchMutex sync.Mutex
timer *time.Timer timer *time.Timer
stopWait sync.WaitGroup stopWait sync.WaitGroup
@ -98,9 +98,9 @@ func NewBatchSpanProcessor(exporter SpanExporter, options ...BatchSpanProcessorO
bsp := &batchSpanProcessor{ bsp := &batchSpanProcessor{
e: exporter, e: exporter,
o: o, o: o,
batch: make([]*SpanSnapshot, 0, o.MaxExportBatchSize), batch: make([]ReadOnlySpan, 0, o.MaxExportBatchSize),
timer: time.NewTimer(o.BatchTimeout), timer: time.NewTimer(o.BatchTimeout),
queue: make(chan *SpanSnapshot, o.MaxQueueSize), queue: make(chan ReadOnlySpan, o.MaxQueueSize),
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
} }
@ -123,7 +123,7 @@ func (bsp *batchSpanProcessor) OnEnd(s ReadOnlySpan) {
if bsp.e == nil { if bsp.e == nil {
return return
} }
bsp.enqueue(s.Snapshot()) bsp.enqueue(s)
} }
// Shutdown flushes the queue and waits until all spans are processed. // Shutdown flushes the queue and waits until all spans are processed.
@ -294,8 +294,8 @@ func (bsp *batchSpanProcessor) drainQueue() {
} }
} }
func (bsp *batchSpanProcessor) enqueue(sd *SpanSnapshot) { func (bsp *batchSpanProcessor) enqueue(sd ReadOnlySpan) {
if !sd.SpanContext.IsSampled() { if !sd.SpanContext().IsSampled() {
return return
} }

View File

@ -32,7 +32,7 @@ import (
type testBatchExporter struct { type testBatchExporter struct {
mu sync.Mutex mu sync.Mutex
spans []*sdktrace.SpanSnapshot spans []sdktrace.ReadOnlySpan
sizes []int sizes []int
batchCount int batchCount int
shutdownCount int shutdownCount int
@ -43,12 +43,12 @@ type testBatchExporter struct {
err error err error
} }
func (t *testBatchExporter) ExportSpans(ctx context.Context, ss []*sdktrace.SpanSnapshot) error { func (t *testBatchExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
if t.idx < len(t.errors) { if t.idx < len(t.errors) {
t.droppedCount += len(ss) t.droppedCount += len(spans)
err := t.errors[t.idx] err := t.errors[t.idx]
t.idx++ t.idx++
return err return err
@ -63,8 +63,8 @@ func (t *testBatchExporter) ExportSpans(ctx context.Context, ss []*sdktrace.Span
default: default:
} }
t.spans = append(t.spans, ss...) t.spans = append(t.spans, spans...)
t.sizes = append(t.sizes, len(ss)) t.sizes = append(t.sizes, len(spans))
t.batchCount++ t.batchCount++
return nil return nil
} }
@ -421,7 +421,7 @@ func assertMaxSpanDiff(t *testing.T, want, got, maxDif int) {
type indefiniteExporter struct{} type indefiniteExporter struct{}
func (indefiniteExporter) Shutdown(context.Context) error { return nil } func (indefiniteExporter) Shutdown(context.Context) error { return nil }
func (indefiniteExporter) ExportSpans(ctx context.Context, _ []*sdktrace.SpanSnapshot) error { func (indefiniteExporter) ExportSpans(ctx context.Context, _ []sdktrace.ReadOnlySpan) error {
<-ctx.Done() <-ctx.Done()
return ctx.Err() return ctx.Err()
} }

View File

@ -55,8 +55,7 @@ func (ssp *simpleSpanProcessor) OnEnd(s ReadOnlySpan) {
defer ssp.exporterMu.RUnlock() defer ssp.exporterMu.RUnlock()
if ssp.exporter != nil && s.SpanContext().TraceFlags().IsSampled() { if ssp.exporter != nil && s.SpanContext().TraceFlags().IsSampled() {
ss := s.Snapshot() if err := ssp.exporter.ExportSpans(context.Background(), []ReadOnlySpan{s}); err != nil {
if err := ssp.exporter.ExportSpans(context.Background(), []*SpanSnapshot{ss}); err != nil {
otel.Handle(err) otel.Handle(err)
} }
} }

View File

@ -31,12 +31,12 @@ var (
) )
type testExporter struct { type testExporter struct {
spans []*sdktrace.SpanSnapshot spans []sdktrace.ReadOnlySpan
shutdown bool shutdown bool
} }
func (t *testExporter) ExportSpans(ctx context.Context, ss []*sdktrace.SpanSnapshot) error { func (t *testExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
t.spans = append(t.spans, ss...) t.spans = append(t.spans, spans...)
return nil return nil
} }
@ -80,7 +80,7 @@ func TestSimpleSpanProcessorOnEnd(t *testing.T) {
startSpan(tp).End() startSpan(tp).End()
wantTraceID := tid wantTraceID := tid
gotTraceID := te.spans[0].SpanContext.TraceID() gotTraceID := te.spans[0].SpanContext().TraceID()
if wantTraceID != gotTraceID { if wantTraceID != gotTraceID {
t.Errorf("SimplerSpanProcessor OnEnd() check: got %+v, want %+v\n", gotTraceID, wantTraceID) t.Errorf("SimplerSpanProcessor OnEnd() check: got %+v, want %+v\n", gotTraceID, wantTraceID)
} }

138
sdk/trace/snapshot.go Normal file
View File

@ -0,0 +1,138 @@
// 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 (
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/trace"
)
// snapshot is an record of a spans state at a particular checkpointed time.
// It is used as a read-only representation of that state.
type snapshot struct {
name string
spanContext trace.SpanContext
parent trace.SpanContext
spanKind trace.SpanKind
startTime time.Time
endTime time.Time
attributes []attribute.KeyValue
events []Event
links []trace.Link
status Status
childSpanCount int
droppedAttributeCount int
droppedEventCount int
droppedLinkCount int
resource *resource.Resource
instrumentationLibrary instrumentation.Library
}
var _ ReadOnlySpan = snapshot{}
func (s snapshot) private() {}
// Name returns the name of the span.
func (s snapshot) Name() string {
return s.name
}
// SpanContext returns the unique SpanContext that identifies the span.
func (s snapshot) SpanContext() trace.SpanContext {
return s.spanContext
}
// Parent returns the unique SpanContext that identifies the parent of the
// span if one exists. If the span has no parent the returned SpanContext
// will be invalid.
func (s snapshot) Parent() trace.SpanContext {
return s.parent
}
// SpanKind returns the role the span plays in a Trace.
func (s snapshot) SpanKind() trace.SpanKind {
return s.spanKind
}
// StartTime returns the time the span started recording.
func (s snapshot) StartTime() time.Time {
return s.startTime
}
// EndTime returns the time the span stopped recording. It will be zero if
// the span has not ended.
func (s snapshot) EndTime() time.Time {
return s.endTime
}
// Attributes returns the defining attributes of the span.
func (s snapshot) Attributes() []attribute.KeyValue {
return s.attributes
}
// Links returns all the links the span has to other spans.
func (s snapshot) Links() []trace.Link {
return s.links
}
// Events returns all the events that occurred within in the spans
// lifetime.
func (s snapshot) Events() []Event {
return s.events
}
// Status returns the spans status.
func (s snapshot) Status() Status {
return s.status
}
// InstrumentationLibrary returns information about the instrumentation
// library that created the span.
func (s snapshot) InstrumentationLibrary() instrumentation.Library {
return s.instrumentationLibrary
}
// Resource returns information about the entity that produced the span.
func (s snapshot) Resource() *resource.Resource {
return s.resource
}
// DroppedAttributes returns the number of attributes dropped by the span
// due to limits being reached.
func (s snapshot) DroppedAttributes() int {
return s.droppedAttributeCount
}
// DroppedLinks returns the number of links dropped by the span due to limits
// being reached.
func (s snapshot) DroppedLinks() int {
return s.droppedLinkCount
}
// DroppedEvents returns the number of events dropped by the span due to
// limits being reached.
func (s snapshot) DroppedEvents() int {
return s.droppedEventCount
}
// ChildSpanCount returns the count of spans that consider the span a
// direct parent.
func (s snapshot) ChildSpanCount() int {
return s.childSpanCount
}

View File

@ -34,24 +34,48 @@ import (
// ReadOnlySpan allows reading information from the data structure underlying a // ReadOnlySpan allows reading information from the data structure underlying a
// trace.Span. It is used in places where reading information from a span is // trace.Span. It is used in places where reading information from a span is
// necessary but changing the span isn't necessary or allowed. // necessary but changing the span isn't necessary or allowed.
// TODO: Should we make the methods unexported? The purpose of this interface
// is controlling access to `span` fields, not having multiple implementations.
type ReadOnlySpan interface { type ReadOnlySpan interface {
// Name returns the name of the span.
Name() string Name() string
// SpanContext returns the unique SpanContext that identifies the span.
SpanContext() trace.SpanContext SpanContext() trace.SpanContext
// Parent returns the unique SpanContext that identifies the parent of the
// span if one exists. If the span has no parent the returned SpanContext
// will be invalid.
Parent() trace.SpanContext Parent() trace.SpanContext
// SpanKind returns the role the span plays in a Trace.
SpanKind() trace.SpanKind SpanKind() trace.SpanKind
// StartTime returns the time the span started recording.
StartTime() time.Time StartTime() time.Time
// EndTime returns the time the span stopped recording. It will be zero if
// the span has not ended.
EndTime() time.Time EndTime() time.Time
// Attributes returns the defining attributes of the span.
Attributes() []attribute.KeyValue Attributes() []attribute.KeyValue
// Links returns all the links the span has to other spans.
Links() []trace.Link Links() []trace.Link
// Events returns all the events that occurred within in the spans
// lifetime.
Events() []Event Events() []Event
// Status returns the spans status.
Status() Status Status() Status
Tracer() trace.Tracer // InstrumentationLibrary returns information about the instrumentation
IsRecording() bool // library that created the span.
InstrumentationLibrary() instrumentation.Library InstrumentationLibrary() instrumentation.Library
// Resource returns information about the entity that produced the span.
Resource() *resource.Resource Resource() *resource.Resource
Snapshot() *SpanSnapshot // DroppedAttributes returns the number of attributes dropped by the span
// due to limits being reached.
DroppedAttributes() int
// DroppedLinks returns the number of links dropped by the span due to
// limits being reached.
DroppedLinks() int
// DroppedEvents returns the number of events dropped by the span due to
// limits being reached.
DroppedEvents() int
// ChildSpanCount returns the count of spans that consider the span a
// direct parent.
ChildSpanCount() int
// A private method to prevent users implementing the // A private method to prevent users implementing the
// interface and so future additions to it will not // interface and so future additions to it will not
@ -112,8 +136,8 @@ type span struct {
// an oldest entry is removed to create room for a new entry. // an oldest entry is removed to create room for a new entry.
attributes *attributesMap attributes *attributesMap
// messageEvents are stored in FIFO queue capped by configured limit. // events are stored in FIFO queue capped by configured limit.
messageEvents *evictedQueue events *evictedQueue
// links are stored in FIFO queue capped by configured limit. // links are stored in FIFO queue capped by configured limit.
links *evictedQueue links *evictedQueue
@ -238,7 +262,7 @@ func (s *span) End(options ...trace.SpanOption) {
mustExportOrProcess := ok && len(sps) > 0 mustExportOrProcess := ok && len(sps) > 0
if mustExportOrProcess { if mustExportOrProcess {
for _, sp := range sps { for _, sp := range sps {
sp.sp.OnEnd(s) sp.sp.OnEnd(s.snapshot())
} }
} }
} }
@ -294,7 +318,7 @@ func (s *span) addEvent(name string, o ...trace.EventOption) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.messageEvents.add(Event{ s.events.add(Event{
Name: name, Name: name,
Attributes: c.Attributes, Attributes: c.Attributes,
DroppedAttributeCount: discarded, DroppedAttributeCount: discarded,
@ -374,10 +398,10 @@ func (s *span) Links() []trace.Link {
func (s *span) Events() []Event { func (s *span) Events() []Event {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if len(s.messageEvents.queue) == 0 { if len(s.events.queue) == 0 {
return []Event{} return []Event{}
} }
return s.interfaceArrayToMessageEventArray() return s.interfaceArrayToEventArray()
} }
// Status returns the status of this span. // Status returns the status of this span.
@ -419,35 +443,66 @@ func (s *span) addLink(link trace.Link) {
s.links.add(link) s.links.add(link)
} }
// Snapshot creates a snapshot representing the current state of the span as an // DroppedAttributes returns the number of attributes dropped by the span
// export.SpanSnapshot and returns a pointer to it. // due to limits being reached.
func (s *span) Snapshot() *SpanSnapshot { func (s *span) DroppedAttributes() int {
var sd SpanSnapshot s.mu.Lock()
defer s.mu.Unlock()
return s.attributes.droppedCount
}
// DroppedLinks returns the number of links dropped by the span due to limits
// being reached.
func (s *span) DroppedLinks() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.links.droppedCount
}
// DroppedEvents returns the number of events dropped by the span due to
// limits being reached.
func (s *span) DroppedEvents() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.events.droppedCount
}
// ChildSpanCount returns the count of spans that consider the span a
// direct parent.
func (s *span) ChildSpanCount() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.childSpanCount
}
// snapshot creates a read-only copy of the current state of the span.
func (s *span) snapshot() ReadOnlySpan {
var sd snapshot
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
sd.ChildSpanCount = s.childSpanCount sd.endTime = s.endTime
sd.EndTime = s.endTime sd.instrumentationLibrary = s.instrumentationLibrary
sd.InstrumentationLibrary = s.instrumentationLibrary sd.name = s.name
sd.Name = s.name sd.parent = s.parent
sd.Parent = s.parent sd.resource = s.resource
sd.Resource = s.resource sd.spanContext = s.spanContext
sd.SpanContext = s.spanContext sd.spanKind = s.spanKind
sd.SpanKind = s.spanKind sd.startTime = s.startTime
sd.StartTime = s.startTime sd.status = s.status
sd.Status = s.status sd.childSpanCount = s.childSpanCount
if s.attributes.evictList.Len() > 0 { if s.attributes.evictList.Len() > 0 {
sd.Attributes = s.attributes.toKeyValue() sd.attributes = s.attributes.toKeyValue()
sd.DroppedAttributeCount = s.attributes.droppedCount sd.droppedAttributeCount = s.attributes.droppedCount
} }
if len(s.messageEvents.queue) > 0 { if len(s.events.queue) > 0 {
sd.MessageEvents = s.interfaceArrayToMessageEventArray() sd.events = s.interfaceArrayToEventArray()
sd.DroppedMessageEventCount = s.messageEvents.droppedCount sd.droppedEventCount = s.events.droppedCount
} }
if len(s.links.queue) > 0 { if len(s.links.queue) > 0 {
sd.Links = s.interfaceArrayToLinksArray() sd.links = s.interfaceArrayToLinksArray()
sd.DroppedLinkCount = s.links.droppedCount sd.droppedLinkCount = s.links.droppedCount
} }
return &sd return &sd
} }
@ -460,12 +515,12 @@ func (s *span) interfaceArrayToLinksArray() []trace.Link {
return linkArr return linkArr
} }
func (s *span) interfaceArrayToMessageEventArray() []Event { func (s *span) interfaceArrayToEventArray() []Event {
messageEventArr := make([]Event, 0) eventArr := make([]Event, 0)
for _, value := range s.messageEvents.queue { for _, value := range s.events.queue {
messageEventArr = append(messageEventArr, value.(Event)) eventArr = append(eventArr, value.(Event))
} }
return messageEventArr return eventArr
} }
func (s *span) copyToCappedAttributes(attributes ...attribute.KeyValue) { func (s *span) copyToCappedAttributes(attributes ...attribute.KeyValue) {
@ -517,7 +572,7 @@ func startSpanInternal(ctx context.Context, tr *tracer, name string, o *trace.Sp
spanLimits := provider.spanLimits spanLimits := provider.spanLimits
span.attributes = newAttributesMap(spanLimits.AttributeCountLimit) span.attributes = newAttributesMap(spanLimits.AttributeCountLimit)
span.messageEvents = newEvictedQueue(spanLimits.EventCountLimit) span.events = newEvictedQueue(spanLimits.EventCountLimit)
span.links = newEvictedQueue(spanLimits.LinkCountLimit) span.links = newEvictedQueue(spanLimits.LinkCountLimit)
span.spanLimits = spanLimits span.spanLimits = spanLimits
@ -573,44 +628,9 @@ func isSampled(s SamplingResult) bool {
// Status is the classified state of a Span. // Status is the classified state of a Span.
type Status struct { type Status struct {
// Code is an identifier of a Span's state classification. // Code is an identifier of a Spans state classification.
Code codes.Code Code codes.Code
// Message is a user hint about why the status was set. It is only // Message is a user hint about why that status was set. It is only
// applicable when Code is Error. // applicable when Code is Error.
Description string Description string
} }
// SpanSnapshot is a snapshot of a span which contains all the information
// collected by the span. Its main purpose is exporting completed spans.
// Although SpanSnapshot fields can be accessed and potentially modified,
// SpanSnapshot should be treated as immutable. Changes to the span from which
// the SpanSnapshot was created are NOT reflected in the SpanSnapshot.
type SpanSnapshot struct {
SpanContext trace.SpanContext
Parent trace.SpanContext
SpanKind trace.SpanKind
Name string
StartTime time.Time
// The wall clock time of EndTime will be adjusted to always be offset
// from StartTime by the duration of the span.
EndTime time.Time
Attributes []attribute.KeyValue
MessageEvents []Event
Links []trace.Link
Status Status
// DroppedAttributeCount contains dropped attributes for the span itself.
DroppedAttributeCount int
DroppedMessageEventCount int
DroppedLinkCount int
// ChildSpanCount holds the number of child span created for this span.
ChildSpanCount int
// Resource contains attributes representing an entity that produced this span.
Resource *resource.Resource
// InstrumentationLibrary defines the instrumentation library used to
// provide instrumentation.
InstrumentationLibrary instrumentation.Library
}

View File

@ -16,10 +16,10 @@ package trace // import "go.opentelemetry.io/otel/sdk/trace"
import "context" import "context"
// SpanExporter handles the delivery of SpanSnapshot structs to external // SpanExporter handles the delivery of spans to external receivers. This is
// receivers. This is the final component in the trace export pipeline. // the final component in the trace export pipeline.
type SpanExporter interface { type SpanExporter interface {
// ExportSpans exports a batch of SpanSnapshots. // ExportSpans exports a batch of spans.
// //
// This function is called synchronously, so there is no concurrency // This function is called synchronously, so there is no concurrency
// safety requirement. However, due to the synchronous calling pattern, // safety requirement. However, due to the synchronous calling pattern,
@ -30,7 +30,7 @@ type SpanExporter interface {
// calls this function will not implement any retry logic. All errors // calls this function will not implement any retry logic. All errors
// returned by this function are considered unrecoverable and will be // returned by this function are considered unrecoverable and will be
// reported to a configured error Handler. // reported to a configured error Handler.
ExportSpans(ctx context.Context, ss []*SpanSnapshot) error ExportSpans(ctx context.Context, spans []ReadOnlySpan) error
// Shutdown notifies the exporter of a pending halt to operations. The // Shutdown notifies the exporter of a pending halt to operations. The
// exporter is expected to preform any cleanup or synchronization it // exporter is expected to preform any cleanup or synchronization it
// requires while honoring all timeouts and cancellations contained in // requires while honoring all timeouts and cancellations contained in

View File

@ -76,8 +76,8 @@ func (f InstrumentationBlacklist) OnEnd(s ReadOnlySpan) {
type noopExporter struct{} type noopExporter struct{}
func (noopExporter) ExportSpans(context.Context, []*SpanSnapshot) error { return nil } func (noopExporter) ExportSpans(context.Context, []ReadOnlySpan) error { return nil }
func (noopExporter) Shutdown(context.Context) error { return nil } func (noopExporter) Shutdown(context.Context) error { return nil }
func ExampleSpanProcessor_filtered() { func ExampleSpanProcessor_filtered() {
exportSP := NewSimpleSpanProcessor(noopExporter{}) exportSP := NewSimpleSpanProcessor(noopExporter{})

View File

@ -103,36 +103,36 @@ func TestTracerFollowsExpectedAPIBehaviour(t *testing.T) {
type testExporter struct { type testExporter struct {
mu sync.RWMutex mu sync.RWMutex
idx map[string]int idx map[string]int
spans []*SpanSnapshot spans []*snapshot
} }
func NewTestExporter() *testExporter { func NewTestExporter() *testExporter {
return &testExporter{idx: make(map[string]int)} return &testExporter{idx: make(map[string]int)}
} }
func (te *testExporter) ExportSpans(_ context.Context, ss []*SpanSnapshot) error { func (te *testExporter) ExportSpans(_ context.Context, spans []ReadOnlySpan) error {
te.mu.Lock() te.mu.Lock()
defer te.mu.Unlock() defer te.mu.Unlock()
i := len(te.spans) i := len(te.spans)
for _, s := range ss { for _, s := range spans {
te.idx[s.Name] = i te.idx[s.Name()] = i
te.spans = append(te.spans, s) te.spans = append(te.spans, s.(*snapshot))
i++ i++
} }
return nil return nil
} }
func (te *testExporter) Spans() []*SpanSnapshot { func (te *testExporter) Spans() []*snapshot {
te.mu.RLock() te.mu.RLock()
defer te.mu.RUnlock() defer te.mu.RUnlock()
cp := make([]*SpanSnapshot, len(te.spans)) cp := make([]*snapshot, len(te.spans))
copy(cp, te.spans) copy(cp, te.spans)
return cp return cp
} }
func (te *testExporter) GetSpan(name string) (*SpanSnapshot, bool) { func (te *testExporter) GetSpan(name string) (*snapshot, bool) {
te.mu.RLock() te.mu.RLock()
defer te.mu.RUnlock() defer te.mu.RUnlock()
i, ok := te.idx[name] i, ok := te.idx[name]
@ -389,19 +389,19 @@ func TestSetSpanAttributesOnStart(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
Attributes: []attribute.KeyValue{ attributes: []attribute.KeyValue{
attribute.String("key1", "value1"), attribute.String("key1", "value1"),
attribute.String("key2", "value2"), attribute.String("key2", "value2"),
}, },
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "StartSpanAttribute"}, instrumentationLibrary: instrumentation.Library{Name: "StartSpanAttribute"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanAttributesOnStart: -got +want %s", diff) t.Errorf("SetSpanAttributesOnStart: -got +want %s", diff)
@ -418,18 +418,18 @@ func TestSetSpanAttributes(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
Attributes: []attribute.KeyValue{ attributes: []attribute.KeyValue{
attribute.String("key1", "value1"), attribute.String("key1", "value1"),
}, },
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "SpanAttribute"}, instrumentationLibrary: instrumentation.Library{Name: "SpanAttribute"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanAttributes: -got +want %s", diff) t.Errorf("SetSpanAttributes: -got +want %s", diff)
@ -454,8 +454,8 @@ func TestSamplerAttributesLocalChildSpan(t *testing.T) {
gotSpan0, gotSpan1 := got[0], got[1] gotSpan0, gotSpan1 := got[0], got[1]
// Ensure sampler is called for local child spans by verifying the // Ensure sampler is called for local child spans by verifying the
// attributes set by the sampler are set on the child span. // 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", 2)}, gotSpan0.Attributes())
assert.Equal(t, []attribute.KeyValue{attribute.Int("callCount", 1)}, gotSpan1.Attributes) assert.Equal(t, []attribute.KeyValue{attribute.Int("callCount", 1)}, gotSpan1.Attributes())
} }
func TestSetSpanAttributesOverLimit(t *testing.T) { func TestSetSpanAttributesOverLimit(t *testing.T) {
@ -474,20 +474,20 @@ func TestSetSpanAttributesOverLimit(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
Attributes: []attribute.KeyValue{ attributes: []attribute.KeyValue{
attribute.Bool("key1", false), attribute.Bool("key1", false),
attribute.Int64("key4", 4), attribute.Int64("key4", 4),
}, },
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
DroppedAttributeCount: 1, droppedAttributeCount: 1,
InstrumentationLibrary: instrumentation.Library{Name: "SpanAttributesOverLimit"}, instrumentationLibrary: instrumentation.Library{Name: "SpanAttributesOverLimit"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanAttributesOverLimit: -got +want %s", diff) t.Errorf("SetSpanAttributesOverLimit: -got +want %s", diff)
@ -508,19 +508,19 @@ func TestSetSpanAttributesWithInvalidKey(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
Attributes: []attribute.KeyValue{ attributes: []attribute.KeyValue{
attribute.Bool("key1", false), attribute.Bool("key1", false),
}, },
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
DroppedAttributeCount: 0, droppedAttributeCount: 0,
InstrumentationLibrary: instrumentation.Library{Name: "SpanToSetInvalidKeyOrValue"}, instrumentationLibrary: instrumentation.Library{Name: "SpanToSetInvalidKeyOrValue"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanAttributesWithInvalidKey: -got +want %s", diff) t.Errorf("SetSpanAttributesWithInvalidKey: -got +want %s", diff)
@ -546,25 +546,25 @@ func TestEvents(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for i := range got.MessageEvents { for i := range got.Events() {
if !checkTime(&got.MessageEvents[i].Time) { if !checkTime(&got.Events()[i].Time) {
t.Error("exporting span: expected nonzero Event Time") t.Error("exporting span: expected nonzero Event Time")
} }
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
MessageEvents: []Event{ events: []Event{
{Name: "foo", Attributes: []attribute.KeyValue{k1v1}}, {Name: "foo", Attributes: []attribute.KeyValue{k1v1}},
{Name: "bar", Attributes: []attribute.KeyValue{k2v2, k3v3}}, {Name: "bar", Attributes: []attribute.KeyValue{k2v2, k3v3}},
}, },
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "Events"}, instrumentationLibrary: instrumentation.Library{Name: "Events"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("Message Events: -got +want %s", diff) t.Errorf("Message Events: -got +want %s", diff)
@ -595,26 +595,26 @@ func TestEventsOverLimit(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for i := range got.MessageEvents { for i := range got.Events() {
if !checkTime(&got.MessageEvents[i].Time) { if !checkTime(&got.Events()[i].Time) {
t.Error("exporting span: expected nonzero Event Time") t.Error("exporting span: expected nonzero Event Time")
} }
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
MessageEvents: []Event{ events: []Event{
{Name: "foo", Attributes: []attribute.KeyValue{k1v1}}, {Name: "foo", Attributes: []attribute.KeyValue{k1v1}},
{Name: "bar", Attributes: []attribute.KeyValue{k2v2, k3v3}}, {Name: "bar", Attributes: []attribute.KeyValue{k2v2, k3v3}},
}, },
DroppedMessageEventCount: 2, droppedEventCount: 2,
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "EventsOverLimit"}, instrumentationLibrary: instrumentation.Library{Name: "EventsOverLimit"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("Message Event over limit: -got +want %s", diff) t.Errorf("Message Event over limit: -got +want %s", diff)
@ -643,16 +643,16 @@ func TestLinks(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
Links: links, links: links,
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "Links"}, instrumentationLibrary: instrumentation.Library{Name: "Links"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("Link: -got +want %s", diff) t.Errorf("Link: -got +want %s", diff)
@ -684,20 +684,20 @@ func TestLinksOverLimit(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
Links: []trace.Link{ links: []trace.Link{
{SpanContext: sc2, Attributes: []attribute.KeyValue{k2v2}}, {SpanContext: sc2, Attributes: []attribute.KeyValue{k2v2}},
{SpanContext: sc3, Attributes: []attribute.KeyValue{k3v3}}, {SpanContext: sc3, Attributes: []attribute.KeyValue{k3v3}},
}, },
DroppedLinkCount: 1, droppedLinkCount: 1,
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "LinksOverLimit"}, instrumentationLibrary: instrumentation.Library{Name: "LinksOverLimit"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("Link over limit: -got +want %s", diff) t.Errorf("Link over limit: -got +want %s", diff)
@ -717,8 +717,8 @@ func TestSetSpanName(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if got.Name != want { if got.Name() != want {
t.Errorf("span.Name: got %q; want %q", got.Name, want) t.Errorf("span.Name: got %q; want %q", got.Name(), want)
} }
} }
@ -733,19 +733,19 @@ func TestSetSpanStatus(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
Status: Status{ status: Status{
Code: codes.Error, Code: codes.Error,
Description: "Error", Description: "Error",
}, },
InstrumentationLibrary: instrumentation.Library{Name: "SpanStatus"}, instrumentationLibrary: instrumentation.Library{Name: "SpanStatus"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanStatus: -got +want %s", diff) t.Errorf("SetSpanStatus: -got +want %s", diff)
@ -763,19 +763,19 @@ func TestSetSpanStatusWithoutMessageWhenStatusIsNotError(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
Status: Status{ status: Status{
Code: codes.Ok, Code: codes.Ok,
Description: "", Description: "",
}, },
InstrumentationLibrary: instrumentation.Library{Name: "SpanStatus"}, instrumentationLibrary: instrumentation.Library{Name: "SpanStatus"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanStatus: -got +want %s", diff) t.Errorf("SetSpanStatus: -got +want %s", diff)
@ -784,6 +784,7 @@ func TestSetSpanStatusWithoutMessageWhenStatusIsNotError(t *testing.T) {
func cmpDiff(x, y interface{}) string { func cmpDiff(x, y interface{}) string {
return cmp.Diff(x, y, return cmp.Diff(x, y,
cmp.AllowUnexported(snapshot{}),
cmp.AllowUnexported(attribute.Value{}), cmp.AllowUnexported(attribute.Value{}),
cmp.AllowUnexported(Event{}), cmp.AllowUnexported(Event{}),
cmp.AllowUnexported(trace.TraceState{})) cmp.AllowUnexported(trace.TraceState{}))
@ -843,16 +844,15 @@ func startLocalSpan(tp *TracerProvider, ctx context.Context, trName, name string
} }
// endSpan is a test utility function that ends the span in the context and // endSpan is a test utility function that ends the span in the context and
// returns the exported export.SpanSnapshot. // returns the exported span.
// It requires that span be sampled using one of these methods // It requires that span be sampled using one of these methods
// 1. Passing parent span context in context // 1. Passing parent span context in context
// 2. Use WithSampler(AlwaysSample()) // 2. Use WithSampler(AlwaysSample())
// 3. Configuring AlwaysSample() as default sampler // 3. Configuring AlwaysSample() as default sampler
// //
// It also does some basic tests on the span. // It also does some basic tests on the span.
// It also clears spanID in the export.SpanSnapshot to make the comparison // It also clears spanID in the to make the comparison easier.
// easier. func endSpan(te *testExporter, span trace.Span) (*snapshot, error) {
func endSpan(te *testExporter, span trace.Span) (*SpanSnapshot, error) {
if !span.IsRecording() { if !span.IsRecording() {
return nil, fmt.Errorf("IsRecording: got false, want true") return nil, fmt.Errorf("IsRecording: got false, want true")
} }
@ -864,14 +864,14 @@ func endSpan(te *testExporter, span trace.Span) (*SpanSnapshot, error) {
return nil, fmt.Errorf("got %d exported spans, want one span", te.Len()) return nil, fmt.Errorf("got %d exported spans, want one span", te.Len())
} }
got := te.Spans()[0] got := te.Spans()[0]
if !got.SpanContext.SpanID().IsValid() { if !got.SpanContext().SpanID().IsValid() {
return nil, fmt.Errorf("exporting span: expected nonzero SpanID") return nil, fmt.Errorf("exporting span: expected nonzero SpanID")
} }
got.SpanContext = got.SpanContext.WithSpanID(trace.SpanID{}) got.spanContext = got.SpanContext().WithSpanID(trace.SpanID{})
if !checkTime(&got.StartTime) { if !checkTime(&got.startTime) {
return nil, fmt.Errorf("exporting span: expected nonzero StartTime") return nil, fmt.Errorf("exporting span: expected nonzero StartTime")
} }
if !checkTime(&got.EndTime) { if !checkTime(&got.endTime) {
return nil, fmt.Errorf("exporting span: expected nonzero EndTime") return nil, fmt.Errorf("exporting span: expected nonzero EndTime")
} }
return got, nil return got, nil
@ -939,16 +939,16 @@ func TestStartSpanAfterEnd(t *testing.T) {
t.Fatal("span-2 not recorded") t.Fatal("span-2 not recorded")
} }
if got, want := gotSpan1.SpanContext.TraceID(), gotParent.SpanContext.TraceID(); got != want { if got, want := gotSpan1.SpanContext().TraceID(), gotParent.SpanContext().TraceID(); got != want {
t.Errorf("span-1.TraceID=%q; want %q", got, want) t.Errorf("span-1.TraceID=%q; want %q", got, want)
} }
if got, want := gotSpan2.SpanContext.TraceID(), gotParent.SpanContext.TraceID(); got != want { if got, want := gotSpan2.SpanContext().TraceID(), gotParent.SpanContext().TraceID(); got != want {
t.Errorf("span-2.TraceID=%q; want %q", got, want) t.Errorf("span-2.TraceID=%q; want %q", got, want)
} }
if got, want := gotSpan1.Parent.SpanID(), gotParent.SpanContext.SpanID(); 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) t.Errorf("span-1.ParentSpanID=%q; want %q (parent.SpanID)", got, want)
} }
if got, want := gotSpan2.Parent.SpanID(), gotSpan1.SpanContext.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) t.Errorf("span-2.ParentSpanID=%q; want %q (span1.SpanID)", got, want)
} }
} }
@ -988,16 +988,16 @@ func TestChildSpanCount(t *testing.T) {
t.Fatal("span-3 not recorded") t.Fatal("span-3 not recorded")
} }
if got, want := gotSpan3.ChildSpanCount, 0; got != want { if got, want := gotSpan3.ChildSpanCount(), 0; got != want {
t.Errorf("span-3.ChildSpanCount=%d; want %d", got, want) t.Errorf("span-3.ChildSpanCount=%d; want %d", got, want)
} }
if got, want := gotSpan2.ChildSpanCount, 0; got != want { if got, want := gotSpan2.ChildSpanCount(), 0; got != want {
t.Errorf("span-2.ChildSpanCount=%d; want %d", got, want) t.Errorf("span-2.ChildSpanCount=%d; want %d", got, want)
} }
if got, want := gotSpan1.ChildSpanCount, 1; got != want { if got, want := gotSpan1.ChildSpanCount(), 1; got != want {
t.Errorf("span-1.ChildSpanCount=%d; want %d", got, want) t.Errorf("span-1.ChildSpanCount=%d; want %d", got, want)
} }
if got, want := gotParent.ChildSpanCount, 2; got != want { if got, want := gotParent.ChildSpanCount(), 2; got != want {
t.Errorf("parent.ChildSpanCount=%d; want %d", got, want) t.Errorf("parent.ChildSpanCount=%d; want %d", got, want)
} }
} }
@ -1075,11 +1075,11 @@ func TestCustomStartEndTime(t *testing.T) {
t.Fatalf("got %d exported spans, want one span", te.Len()) t.Fatalf("got %d exported spans, want one span", te.Len())
} }
got := te.Spans()[0] got := te.Spans()[0]
if got.StartTime != startTime { if got.StartTime() != startTime {
t.Errorf("expected start time to be %s, got %s", startTime, got.StartTime) t.Errorf("expected start time to be %s, got %s", startTime, got.StartTime())
} }
if got.EndTime != endTime { if got.EndTime() != endTime {
t.Errorf("expected end time to be %s, got %s", endTime, got.EndTime) t.Errorf("expected end time to be %s, got %s", endTime, got.EndTime())
} }
} }
@ -1114,16 +1114,16 @@ func TestRecordError(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
Status: Status{Code: codes.Unset}, status: Status{Code: codes.Unset},
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
MessageEvents: []Event{ events: []Event{
{ {
Name: semconv.ExceptionEventName, Name: semconv.ExceptionEventName,
Time: errTime, Time: errTime,
@ -1133,7 +1133,7 @@ func TestRecordError(t *testing.T) {
}, },
}, },
}, },
InstrumentationLibrary: instrumentation.Library{Name: "RecordError"}, instrumentationLibrary: instrumentation.Library{Name: "RecordError"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SpanErrorOptions: -got +want %s", diff) t.Errorf("SpanErrorOptions: -got +want %s", diff)
@ -1153,19 +1153,19 @@ func TestRecordErrorNil(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
Status: Status{ status: Status{
Code: codes.Unset, Code: codes.Unset,
Description: "", Description: "",
}, },
InstrumentationLibrary: instrumentation.Library{Name: "RecordErrorNil"}, instrumentationLibrary: instrumentation.Library{Name: "RecordErrorNil"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SpanErrorOptions: -got +want %s", diff) t.Errorf("SpanErrorOptions: -got +want %s", diff)
@ -1183,8 +1183,8 @@ func TestWithSpanKind(t *testing.T) {
t.Error(err.Error()) t.Error(err.Error())
} }
if spanData.SpanKind != trace.SpanKindInternal { if spanData.SpanKind() != trace.SpanKindInternal {
t.Errorf("Default value of Spankind should be Internal: got %+v, want %+v\n", 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{ sks := []trace.SpanKind{
@ -1204,8 +1204,8 @@ func TestWithSpanKind(t *testing.T) {
t.Error(err.Error()) t.Error(err.Error())
} }
if spanData.SpanKind != sk { if spanData.SpanKind() != sk {
t.Errorf("WithSpanKind check: got %+v, want %+v\n", spanData.SpanKind, sks) t.Errorf("WithSpanKind check: got %+v, want %+v\n", spanData.SpanKind(), sks)
} }
} }
} }
@ -1263,19 +1263,19 @@ func TestWithResource(t *testing.T) {
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
Attributes: []attribute.KeyValue{ attributes: []attribute.KeyValue{
attribute.String("key1", "value1"), attribute.String("key1", "value1"),
}, },
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
Resource: tc.want, resource: tc.want,
InstrumentationLibrary: instrumentation.Library{Name: "WithResource"}, instrumentationLibrary: instrumentation.Library{Name: "WithResource"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("WithResource:\n -got +want %s", diff) t.Errorf("WithResource:\n -got +want %s", diff)
@ -1299,15 +1299,15 @@ func TestWithInstrumentationVersion(t *testing.T) {
t.Error(err.Error()) t.Error(err.Error())
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{ instrumentationLibrary: instrumentation.Library{
Name: "WithInstrumentationVersion", Name: "WithInstrumentationVersion",
Version: "v0.1.0", Version: "v0.1.0",
}, },
@ -1332,9 +1332,9 @@ func TestSpanCapturesPanic(t *testing.T) {
require.PanicsWithError(t, "error message", f) require.PanicsWithError(t, "error message", f)
spans := te.Spans() spans := te.Spans()
require.Len(t, spans, 1) require.Len(t, spans, 1)
require.Len(t, spans[0].MessageEvents, 1) require.Len(t, spans[0].Events(), 1)
assert.Equal(t, spans[0].MessageEvents[0].Name, semconv.ExceptionEventName) assert.Equal(t, spans[0].Events()[0].Name, semconv.ExceptionEventName)
assert.Equal(t, spans[0].MessageEvents[0].Attributes, []attribute.KeyValue{ assert.Equal(t, spans[0].Events()[0].Attributes, []attribute.KeyValue{
semconv.ExceptionTypeKey.String("*errors.errorString"), semconv.ExceptionTypeKey.String("*errors.errorString"),
semconv.ExceptionMessageKey.String("error message"), semconv.ExceptionMessageKey.String("error message"),
}) })
@ -1365,14 +1365,14 @@ func TestReadOnlySpan(t *testing.T) {
}) })
st := time.Now() st := time.Now()
ctx, span := tr.Start(ctx, "foo", trace.WithTimestamp(st), ctx, s := tr.Start(ctx, "foo", trace.WithTimestamp(st),
trace.WithLinks(trace.Link{SpanContext: linked})) trace.WithLinks(trace.Link{SpanContext: linked}))
span.SetAttributes(kv) s.SetAttributes(kv)
span.AddEvent("foo", trace.WithAttributes(kv)) s.AddEvent("foo", trace.WithAttributes(kv))
span.SetStatus(codes.Ok, "foo") s.SetStatus(codes.Ok, "foo")
// Verify span implements ReadOnlySpan. // Verify span implements ReadOnlySpan.
ro, ok := span.(ReadOnlySpan) ro, ok := s.(ReadOnlySpan)
require.True(t, ok) require.True(t, ok)
assert.Equal(t, "foo", ro.Name()) assert.Equal(t, "foo", ro.Name())
@ -1394,21 +1394,21 @@ func TestReadOnlySpan(t *testing.T) {
assert.Equal(t, kv.Value, ro.Resource().Attributes()[0].Value) assert.Equal(t, kv.Value, ro.Resource().Attributes()[0].Value)
// Verify changes to the original span are reflected in the ReadOnlySpan. // Verify changes to the original span are reflected in the ReadOnlySpan.
span.SetName("bar") s.SetName("bar")
assert.Equal(t, "bar", ro.Name()) assert.Equal(t, "bar", ro.Name())
// Verify Snapshot() returns snapshots that are independent from the // Verify snapshot() returns snapshots that are independent from the
// original span and from one another. // original span and from one another.
d1 := ro.Snapshot() d1 := s.(*span).snapshot()
span.AddEvent("baz") s.AddEvent("baz")
d2 := ro.Snapshot() d2 := s.(*span).snapshot()
for _, e := range d1.MessageEvents { for _, e := range d1.Events() {
if e.Name == "baz" { if e.Name == "baz" {
t.Errorf("Didn't expect to find 'baz' event") t.Errorf("Didn't expect to find 'baz' event")
} }
} }
var exists bool var exists bool
for _, e := range d2.MessageEvents { for _, e := range d2.Events() {
if e.Name == "baz" { if e.Name == "baz" {
exists = true exists = true
} }
@ -1418,7 +1418,7 @@ func TestReadOnlySpan(t *testing.T) {
} }
et := st.Add(time.Millisecond) et := st.Add(time.Millisecond)
span.End(trace.WithTimestamp(et)) s.End(trace.WithTimestamp(et))
assert.Equal(t, et, ro.EndTime()) assert.Equal(t, et, ro.EndTime())
} }
@ -1481,21 +1481,21 @@ func TestAddEventsWithMoreAttributesThanLimit(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for i := range got.MessageEvents { for i := range got.Events() {
if !checkTime(&got.MessageEvents[i].Time) { if !checkTime(&got.Events()[i].Time) {
t.Error("exporting span: expected nonzero Event Time") t.Error("exporting span: expected nonzero Event Time")
} }
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
Attributes: nil, attributes: nil,
MessageEvents: []Event{ events: []Event{
{ {
Name: "test1", Name: "test1",
Attributes: []attribute.KeyValue{ Attributes: []attribute.KeyValue{
@ -1512,8 +1512,8 @@ func TestAddEventsWithMoreAttributesThanLimit(t *testing.T) {
DroppedAttributeCount: 2, DroppedAttributeCount: 2,
}, },
}, },
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "AddSpanEventWithOverLimitedAttributes"}, instrumentationLibrary: instrumentation.Library{Name: "AddSpanEventWithOverLimitedAttributes"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("SetSpanAttributesOverLimit: -got +want %s", diff) t.Errorf("SetSpanAttributesOverLimit: -got +want %s", diff)
@ -1546,14 +1546,14 @@ func TestAddLinksWithMoreAttributesThanLimit(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := &SpanSnapshot{ want := &snapshot{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ spanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid, TraceID: tid,
TraceFlags: 0x1, TraceFlags: 0x1,
}), }),
Parent: sc.WithRemote(true), parent: sc.WithRemote(true),
Name: "span0", name: "span0",
Links: []trace.Link{ links: []trace.Link{
{ {
SpanContext: sc1, SpanContext: sc1,
Attributes: []attribute.KeyValue{k1v1}, Attributes: []attribute.KeyValue{k1v1},
@ -1565,8 +1565,8 @@ func TestAddLinksWithMoreAttributesThanLimit(t *testing.T) {
DroppedAttributeCount: 2, DroppedAttributeCount: 2,
}, },
}, },
SpanKind: trace.SpanKindInternal, spanKind: trace.SpanKindInternal,
InstrumentationLibrary: instrumentation.Library{Name: "Links"}, instrumentationLibrary: instrumentation.Library{Name: "Links"},
} }
if diff := cmpDiff(got, want); diff != "" { if diff := cmpDiff(got, want); diff != "" {
t.Errorf("Link: -got +want %s", diff) t.Errorf("Link: -got +want %s", diff)
@ -1706,7 +1706,7 @@ func TestSamplerTraceState(t *testing.T) {
return return
} }
receivedState := got[0].SpanContext.TraceState() receivedState := got[0].SpanContext().TraceState()
if diff := cmpDiff(receivedState, ts.want); diff != "" { if diff := cmpDiff(receivedState, ts.want); diff != "" {
t.Errorf("TraceState not propagated: -got +want %s", diff) t.Errorf("TraceState not propagated: -got +want %s", diff)

163
sdk/trace/tracetest/span.go Normal file
View File

@ -0,0 +1,163 @@
// 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 tracetest // import "go.opentelemetry.io/otel/sdk/trace/tracetest"
import (
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
)
type SpanStubs []SpanStub
// SpanStubFromReadOnlySpan returns SpanStubs populated from ro.
func SpanStubsFromReadOnlySpans(ro []tracesdk.ReadOnlySpan) SpanStubs {
if len(ro) == 0 {
return nil
}
s := make(SpanStubs, 0, len(ro))
for _, r := range ro {
s = append(s, SpanStubFromReadOnlySpan(r))
}
return s
}
// Snapshots returns s as a slice of ReadOnlySpans.
func (s SpanStubs) Snapshots() []tracesdk.ReadOnlySpan {
if len(s) == 0 {
return nil
}
ro := make([]tracesdk.ReadOnlySpan, len(s))
for i := 0; i < len(s); i++ {
ro[i] = s[i].Snapshot()
}
return ro
}
// SpanStub is a stand-in for a Span.
type SpanStub struct {
Name string
SpanContext trace.SpanContext
Parent trace.SpanContext
SpanKind trace.SpanKind
StartTime time.Time
EndTime time.Time
Attributes []attribute.KeyValue
Events []tracesdk.Event
Links []trace.Link
Status tracesdk.Status
DroppedAttributes int
DroppedEvents int
DroppedLinks int
ChildSpanCount int
Resource *resource.Resource
InstrumentationLibrary instrumentation.Library
}
// SpanStubFromReadOnlySpan returns a SpanStub populated from ro.
func SpanStubFromReadOnlySpan(ro tracesdk.ReadOnlySpan) SpanStub {
if ro == nil {
return SpanStub{}
}
return SpanStub{
Name: ro.Name(),
SpanContext: ro.SpanContext(),
Parent: ro.Parent(),
SpanKind: ro.SpanKind(),
StartTime: ro.StartTime(),
EndTime: ro.EndTime(),
Attributes: ro.Attributes(),
Events: ro.Events(),
Links: ro.Links(),
Status: ro.Status(),
DroppedAttributes: ro.DroppedAttributes(),
DroppedEvents: ro.DroppedEvents(),
DroppedLinks: ro.DroppedLinks(),
ChildSpanCount: ro.ChildSpanCount(),
Resource: ro.Resource(),
InstrumentationLibrary: ro.InstrumentationLibrary(),
}
}
// Snapshot returns a read-only copy of the SpanStub.
func (s SpanStub) Snapshot() tracesdk.ReadOnlySpan {
return spanSnapshot{
name: s.Name,
spanContext: s.SpanContext,
parent: s.Parent,
spanKind: s.SpanKind,
startTime: s.StartTime,
endTime: s.EndTime,
attributes: s.Attributes,
events: s.Events,
links: s.Links,
status: s.Status,
droppedAttributes: s.DroppedAttributes,
droppedEvents: s.DroppedEvents,
droppedLinks: s.DroppedLinks,
childSpanCount: s.ChildSpanCount,
resource: s.Resource,
instrumentationLibrary: s.InstrumentationLibrary,
}
}
type spanSnapshot struct {
// Embed the interface to implement the private method.
tracesdk.ReadOnlySpan
name string
spanContext trace.SpanContext
parent trace.SpanContext
spanKind trace.SpanKind
startTime time.Time
endTime time.Time
attributes []attribute.KeyValue
events []tracesdk.Event
links []trace.Link
status tracesdk.Status
droppedAttributes int
droppedEvents int
droppedLinks int
childSpanCount int
resource *resource.Resource
instrumentationLibrary instrumentation.Library
}
func (s spanSnapshot) Name() string { return s.name }
func (s spanSnapshot) SpanContext() trace.SpanContext { return s.spanContext }
func (s spanSnapshot) Parent() trace.SpanContext { return s.parent }
func (s spanSnapshot) SpanKind() trace.SpanKind { return s.spanKind }
func (s spanSnapshot) StartTime() time.Time { return s.startTime }
func (s spanSnapshot) EndTime() time.Time { return s.endTime }
func (s spanSnapshot) Attributes() []attribute.KeyValue { return s.attributes }
func (s spanSnapshot) Links() []trace.Link { return s.links }
func (s spanSnapshot) Events() []tracesdk.Event { return s.events }
func (s spanSnapshot) Status() tracesdk.Status { return s.status }
func (s spanSnapshot) DroppedAttributes() int { return s.droppedAttributes }
func (s spanSnapshot) DroppedLinks() int { return s.droppedLinks }
func (s spanSnapshot) DroppedEvents() int { return s.droppedEvents }
func (s spanSnapshot) ChildSpanCount() int { return s.childSpanCount }
func (s spanSnapshot) Resource() *resource.Resource { return s.resource }
func (s spanSnapshot) InstrumentationLibrary() instrumentation.Library {
return s.instrumentationLibrary
}

View File

@ -31,12 +31,12 @@ func NewNoopExporter() *NoopExporter {
return new(NoopExporter) return new(NoopExporter)
} }
// NoopExporter is an exporter that drops all received SpanSnapshots and // NoopExporter is an exporter that drops all received spans and performs no
// performs no action. // action.
type NoopExporter struct{} type NoopExporter struct{}
// ExportSpans handles export of SpanSnapshots by dropping them. // ExportSpans handles export of spans by dropping them.
func (nsb *NoopExporter) ExportSpans(context.Context, []*trace.SpanSnapshot) error { return nil } func (nsb *NoopExporter) ExportSpans(context.Context, []trace.ReadOnlySpan) error { return nil }
// Shutdown stops the exporter by doing nothing. // Shutdown stops the exporter by doing nothing.
func (nsb *NoopExporter) Shutdown(context.Context) error { return nil } func (nsb *NoopExporter) Shutdown(context.Context) error { return nil }
@ -51,18 +51,18 @@ func NewInMemoryExporter() *InMemoryExporter {
// InMemoryExporter is an exporter that stores all received spans in-memory. // InMemoryExporter is an exporter that stores all received spans in-memory.
type InMemoryExporter struct { type InMemoryExporter struct {
mu sync.Mutex mu sync.Mutex
ss []*trace.SpanSnapshot ss SpanStubs
} }
// ExportSpans handles export of SpanSnapshots by storing them in memory. // ExportSpans handles export of spans by storing them in memory.
func (imsb *InMemoryExporter) ExportSpans(_ context.Context, ss []*trace.SpanSnapshot) error { func (imsb *InMemoryExporter) ExportSpans(_ context.Context, spans []trace.ReadOnlySpan) error {
imsb.mu.Lock() imsb.mu.Lock()
defer imsb.mu.Unlock() defer imsb.mu.Unlock()
imsb.ss = append(imsb.ss, ss...) imsb.ss = append(imsb.ss, SpanStubsFromReadOnlySpans(spans)...)
return nil return nil
} }
// Shutdown stops the exporter by clearing SpanSnapshots held in memory. // Shutdown stops the exporter by clearing spans held in memory.
func (imsb *InMemoryExporter) Shutdown(context.Context) error { func (imsb *InMemoryExporter) Shutdown(context.Context) error {
imsb.Reset() imsb.Reset()
return nil return nil
@ -76,10 +76,10 @@ func (imsb *InMemoryExporter) Reset() {
} }
// GetSpans returns the current in-memory stored spans. // GetSpans returns the current in-memory stored spans.
func (imsb *InMemoryExporter) GetSpans() []*trace.SpanSnapshot { func (imsb *InMemoryExporter) GetSpans() SpanStubs {
imsb.mu.Lock() imsb.mu.Lock()
defer imsb.mu.Unlock() defer imsb.mu.Unlock()
ret := make([]*trace.SpanSnapshot, len(imsb.ss)) ret := make(SpanStubs, len(imsb.ss))
copy(ret, imsb.ss) copy(ret, imsb.ss)
return ret return ret
} }

View File

@ -16,12 +16,11 @@ package tracetest
import ( import (
"context" "context"
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/sdk/trace"
) )
// TestNoop tests only that the no-op does not crash in different scenarios. // TestNoop tests only that the no-op does not crash in different scenarios.
@ -29,8 +28,8 @@ func TestNoop(t *testing.T) {
nsb := NewNoopExporter() nsb := NewNoopExporter()
require.NoError(t, nsb.ExportSpans(context.Background(), nil)) require.NoError(t, nsb.ExportSpans(context.Background(), nil))
require.NoError(t, nsb.ExportSpans(context.Background(), make([]*trace.SpanSnapshot, 10))) require.NoError(t, nsb.ExportSpans(context.Background(), make(SpanStubs, 10).Snapshots()))
require.NoError(t, nsb.ExportSpans(context.Background(), make([]*trace.SpanSnapshot, 0, 10))) require.NoError(t, nsb.ExportSpans(context.Background(), make(SpanStubs, 0, 10).Snapshots()))
} }
func TestNewInMemoryExporter(t *testing.T) { func TestNewInMemoryExporter(t *testing.T) {
@ -39,23 +38,23 @@ func TestNewInMemoryExporter(t *testing.T) {
require.NoError(t, imsb.ExportSpans(context.Background(), nil)) require.NoError(t, imsb.ExportSpans(context.Background(), nil))
assert.Len(t, imsb.GetSpans(), 0) assert.Len(t, imsb.GetSpans(), 0)
input := make([]*trace.SpanSnapshot, 10) input := make(SpanStubs, 10)
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
input[i] = new(trace.SpanSnapshot) input[i] = SpanStub{Name: fmt.Sprintf("span %d", i)}
} }
require.NoError(t, imsb.ExportSpans(context.Background(), input)) require.NoError(t, imsb.ExportSpans(context.Background(), input.Snapshots()))
sds := imsb.GetSpans() sds := imsb.GetSpans()
assert.Len(t, sds, 10) assert.Len(t, sds, 10)
for i, sd := range sds { for i, sd := range sds {
assert.Same(t, input[i], sd) assert.Equal(t, input[i], sd)
} }
imsb.Reset() imsb.Reset()
// Ensure that operations on the internal storage does not change the previously returned value. // Ensure that operations on the internal storage does not change the previously returned value.
assert.Len(t, sds, 10) assert.Len(t, sds, 10)
assert.Len(t, imsb.GetSpans(), 0) assert.Len(t, imsb.GetSpans(), 0)
require.NoError(t, imsb.ExportSpans(context.Background(), input[0:1])) require.NoError(t, imsb.ExportSpans(context.Background(), input.Snapshots()[0:1]))
sds = imsb.GetSpans() sds = imsb.GetSpans()
assert.Len(t, sds, 1) assert.Len(t, sds, 1)
assert.Same(t, input[0], sds[0]) assert.Equal(t, input[0], sds[0])
} }