diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index c392312ada04..ca9a079d0c00 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -35,6 +35,5 @@ jobs: external-data-json-path: ./benchmarks/data.json github-token: ${{ secrets.GITHUB_TOKEN }} gh-pages-branch: benchmarks - auto-push: true - fail-on-alert: false + fail-on-alert: true alert-threshold: "400%" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e07e5e60aec..f90592cc565b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added +- Add `AddLink` method to the `Span` interface in `go.opentelemetry.io/otel/trace`. (#5032) - Add `WithProxy` option in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#4906) - Add `WithProxy` option in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlptracehttp`. (#4906) - The `Enabled` method is added to the `Logger` interface in `go.opentelemetry.io/otel/log`. @@ -22,6 +23,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm At which point, users will be required to migrage their code, and this package will be deprecated then removed. (#5085) - Add support for `Summary` metrics in the `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp` and `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc` exporters. (#5100) - Add `otel.scope.name` and `otel.scope.version` tags to spans exported by `go.opentelemetry.io/otel/exporters/zipkin`. (#5108) +- Add support for `AddLink` to `go.opentelemetry.io/otel/bridge/opencensus`. (#5116) - Add Exemplar support to `go.opentelemetry.io/otel/exporters/prometheus`. (#5111) ### Changed diff --git a/baggage/baggage.go b/baggage/baggage.go index f3e870f8a8e6..94285d95935f 100644 --- a/baggage/baggage.go +++ b/baggage/baggage.go @@ -56,10 +56,10 @@ func NewKeyProperty(key string) (Property, error) { // NewKeyValueProperty returns a new Property for key with value. // // The passed key must be compliant with W3C Baggage specification. -// The passed value must be precent-encoded as defined in W3C Baggage specification. +// The passed value must be percent-encoded as defined in W3C Baggage specification. // // Notice: Consider using [NewKeyValuePropertyRaw] instead -// that does not require precent-encoding of the value. +// that does not require percent-encoding of the value. func NewKeyValueProperty(key, value string) (Property, error) { if !validateValue(value) { return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, value) @@ -224,10 +224,10 @@ type Member struct { // NewMemberRaw returns a new Member from the passed arguments. // // The passed key must be compliant with W3C Baggage specification. -// The passed value must be precent-encoded as defined in W3C Baggage specification. +// The passed value must be percent-encoded as defined in W3C Baggage specification. // // Notice: Consider using [NewMemberRaw] instead -// that does not require precent-encoding of the value. +// that does not require percent-encoding of the value. func NewMember(key, value string, props ...Property) (Member, error) { if !validateValue(value) { return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value) @@ -298,7 +298,7 @@ func parseMember(member string) (Member, error) { return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, v) } - // Decode a precent-encoded value. + // Decode a percent-encoded value. value, err := url.PathUnescape(val) if err != nil { return newInvalidMember(), fmt.Errorf("%w: %v", errInvalidValue, err) @@ -605,7 +605,7 @@ func parsePropertyInternal(s string) (p Property, ok bool) { return } - // Decode a precent-encoded value. + // Decode a percent-encoded value. value, err := url.PathUnescape(s[valueStart:valueEnd]) if err != nil { return diff --git a/bridge/opencensus/doc.go b/bridge/opencensus/doc.go index 8d363ce4dbcc..0f5d4abb8cf4 100644 --- a/bridge/opencensus/doc.go +++ b/bridge/opencensus/doc.go @@ -37,8 +37,6 @@ // // There are known limitations to the trace bridge: // -// - The AddLink method for OpenCensus Spans is ignored, and an error is sent -// to the OpenTelemetry ErrorHandler. // - The NewContext method of the OpenCensus Tracer cannot embed an OpenCensus // Span in a context unless that Span was created by that Tracer. // - Conversion of custom OpenCensus Samplers to OpenTelemetry is not diff --git a/bridge/opencensus/internal/oc2otel/attributes.go b/bridge/opencensus/internal/oc2otel/attributes.go index 1b9e931e0733..7c6ae45d5831 100644 --- a/bridge/opencensus/internal/oc2otel/attributes.go +++ b/bridge/opencensus/internal/oc2otel/attributes.go @@ -20,6 +20,17 @@ func Attributes(attr []octrace.Attribute) []attribute.KeyValue { return otelAttr } +func AttributesFromMap(attr map[string]interface{}) []attribute.KeyValue { + otelAttr := make([]attribute.KeyValue, 0, len(attr)) + for k, v := range attr { + otelAttr = append(otelAttr, attribute.KeyValue{ + Key: attribute.Key(k), + Value: AttributeValue(v), + }) + } + return otelAttr +} + func AttributeValue(ocval interface{}) attribute.Value { switch v := ocval.(type) { case bool: diff --git a/bridge/opencensus/internal/oc2otel/attributes_test.go b/bridge/opencensus/internal/oc2otel/attributes_test.go index 8c1447341fd9..7e3efeed723c 100644 --- a/bridge/opencensus/internal/oc2otel/attributes_test.go +++ b/bridge/opencensus/internal/oc2otel/attributes_test.go @@ -37,6 +37,29 @@ func TestAttributes(t *testing.T) { } } +func TestAttributesFromMap(t *testing.T) { + in := map[string]interface{}{ + "bool": true, + "int64": int64(49), + "float64": float64(1.618), + "key": "val", + } + + want := []attribute.KeyValue{ + attribute.Bool("bool", true), + attribute.Int64("int64", 49), + attribute.Float64("float64", 1.618), + attribute.String("key", "val"), + } + got := AttributesFromMap(in) + + gotAttributeSet := attribute.NewSet(got...) + wantAttributeSet := attribute.NewSet(want...) + if !gotAttributeSet.Equals(&wantAttributeSet) { + t.Errorf("Attributes conversion want %v, got %v", wantAttributeSet.Encoded(attribute.DefaultEncoder()), gotAttributeSet.Encoded(attribute.DefaultEncoder())) + } +} + func TestAttributeValueUnknown(t *testing.T) { got := AttributeValue([]byte{}) if got != attribute.StringValue("unknown") { diff --git a/bridge/opencensus/internal/span.go b/bridge/opencensus/internal/span.go index e3b76064bfc0..9e7ee39fb5b9 100644 --- a/bridge/opencensus/internal/span.go +++ b/bridge/opencensus/internal/span.go @@ -110,8 +110,20 @@ func (s *Span) AddMessageReceiveEvent(messageID, uncompressedByteSize, compresse } // AddLink adds a link to this span. +// This drops the OpenCensus LinkType because there is no such concept in OpenTelemetry. func (s *Span) AddLink(l octrace.Link) { - Handle(fmt.Errorf("ignoring OpenCensus link %+v for span %q because OpenTelemetry doesn't support setting links after creation", l, s.String())) + s.otelSpan.AddLink(trace.Link{ + SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID(l.TraceID), + SpanID: trace.SpanID(l.SpanID), + // We don't know if this was sampled or not. + // Mark it as sampled, since sampled means + // "the caller may have recorded trace data": + // https://www.w3.org/TR/trace-context/#sampled-flag + TraceFlags: trace.FlagsSampled, + }), + Attributes: oc2otel.AttributesFromMap(l.Attributes), + }) } // String prints a string representation of this span. diff --git a/bridge/opencensus/internal/span_test.go b/bridge/opencensus/internal/span_test.go index e11632e0252a..949018b8f0c7 100644 --- a/bridge/opencensus/internal/span_test.go +++ b/bridge/opencensus/internal/span_test.go @@ -28,6 +28,7 @@ type span struct { attrs []attribute.KeyValue eName string eOpts []trace.EventOption + links []trace.Link } func (s *span) IsRecording() bool { return s.recording } @@ -37,6 +38,7 @@ func (s *span) SetName(n string) { s.name = n } func (s *span) SetStatus(c codes.Code, d string) { s.sCode, s.sMsg = c, d } func (s *span) SetAttributes(a ...attribute.KeyValue) { s.attrs = a } func (s *span) AddEvent(n string, o ...trace.EventOption) { s.eName, s.eOpts = n, o } +func (s *span) AddLink(l trace.Link) { s.links = append(s.links, l) } func TestSpanIsRecordingEvents(t *testing.T) { s := &span{recording: true} @@ -230,16 +232,51 @@ func TestSpanAddMessageReceiveEvent(t *testing.T) { } func TestSpanAddLinkFails(t *testing.T) { - h, restore := withHandler() - defer restore() - // OpenCensus does not try to set links if not recording. s := &span{recording: true} ocS := internal.NewSpan(s) ocS.AddLink(octrace.Link{}) + ocS.AddLink(octrace.Link{ + TraceID: octrace.TraceID([16]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), + SpanID: octrace.SpanID([8]byte{2, 0, 0, 0, 0, 0, 0, 0}), + Attributes: map[string]interface{}{ + "foo": "bar", + "number": int64(3), + }, + }) + + wantLinks := []trace.Link{ + { + SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ + TraceFlags: trace.FlagsSampled, + }), + }, + { + SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), + SpanID: trace.SpanID([]byte{2, 0, 0, 0, 0, 0, 0, 0}), + TraceFlags: trace.FlagsSampled, + }), + Attributes: []attribute.KeyValue{ + attribute.String("foo", "bar"), + attribute.Int64("number", 3), + }, + }, + } + + if len(s.links) != len(wantLinks) { + t.Fatalf("got wrong number of links; want %v, got %v", len(wantLinks), len(s.links)) + } - if h.err == nil { - t.Error("span.AddLink failed to raise an error") + for i, l := range s.links { + if !l.SpanContext.Equal(wantLinks[i].SpanContext) { + t.Errorf("link[%v] has the wrong span context; want %+v, got %+v", i, wantLinks[i].SpanContext, l.SpanContext) + } + gotAttributeSet := attribute.NewSet(l.Attributes...) + wantAttributeSet := attribute.NewSet(wantLinks[i].Attributes...) + if !gotAttributeSet.Equals(&wantAttributeSet) { + t.Errorf("link[%v] has the wrong attributes; want %v, got %v", i, wantAttributeSet.Encoded(attribute.DefaultEncoder()), gotAttributeSet.Encoded(attribute.DefaultEncoder())) + } } } diff --git a/bridge/opentracing/internal/mock.go b/bridge/opentracing/internal/mock.go index ba92206f4193..434f6a7bc817 100644 --- a/bridge/opentracing/internal/mock.go +++ b/bridge/opentracing/internal/mock.go @@ -176,6 +176,11 @@ type MockEvent struct { Attributes []attribute.KeyValue } +type MockLink struct { + SpanContext trace.SpanContext + Attributes []attribute.KeyValue +} + type MockSpan struct { embedded.Span @@ -190,6 +195,7 @@ type MockSpan struct { EndTime time.Time ParentSpanID trace.SpanID Events []MockEvent + Links []MockLink } var ( @@ -286,6 +292,13 @@ func (s *MockSpan) AddEvent(name string, o ...trace.EventOption) { }) } +func (s *MockSpan) AddLink(link trace.Link) { + s.Links = append(s.Links, MockLink{ + SpanContext: link.SpanContext, + Attributes: link.Attributes, + }) +} + func (s *MockSpan) OverrideTracer(tracer trace.Tracer) { s.officialTracer = tracer } diff --git a/example/otel-collector/go.mod b/example/otel-collector/go.mod index 734bf3feb04b..5939e5432518 100644 --- a/example/otel-collector/go.mod +++ b/example/otel-collector/go.mod @@ -16,7 +16,7 @@ require ( ) require ( - github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect diff --git a/example/otel-collector/go.sum b/example/otel-collector/go.sum index d55df3777320..e311b64812be 100644 --- a/example/otel-collector/go.sum +++ b/example/otel-collector/go.sum @@ -1,5 +1,5 @@ -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/exporters/otlp/otlpmetric/otlpmetricgrpc/go.mod b/exporters/otlp/otlpmetric/otlpmetricgrpc/go.mod index 47d752d9355a..344b5e5008d3 100644 --- a/exporters/otlp/otlpmetric/otlpmetricgrpc/go.mod +++ b/exporters/otlp/otlpmetric/otlpmetricgrpc/go.mod @@ -5,7 +5,7 @@ go 1.21 retract v0.32.2 // Contains unresolvable dependencies. require ( - github.com/cenkalti/backoff/v4 v4.2.1 + github.com/cenkalti/backoff/v4 v4.3.0 github.com/google/go-cmp v0.6.0 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/otel v1.24.0 diff --git a/exporters/otlp/otlpmetric/otlpmetricgrpc/go.sum b/exporters/otlp/otlpmetric/otlpmetricgrpc/go.sum index 3282f6f0ccf0..66bea4ce5ad0 100644 --- a/exporters/otlp/otlpmetric/otlpmetricgrpc/go.sum +++ b/exporters/otlp/otlpmetric/otlpmetricgrpc/go.sum @@ -1,5 +1,5 @@ -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod b/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod index 1d35f3b54b7c..b2fdae8974cf 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod @@ -5,7 +5,7 @@ go 1.21 retract v0.32.2 // Contains unresolvable dependencies. require ( - github.com/cenkalti/backoff/v4 v4.2.1 + github.com/cenkalti/backoff/v4 v4.3.0 github.com/google/go-cmp v0.6.0 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/otel v1.24.0 diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/go.sum b/exporters/otlp/otlpmetric/otlpmetrichttp/go.sum index 3282f6f0ccf0..66bea4ce5ad0 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/go.sum +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/go.sum @@ -1,5 +1,5 @@ -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/exporters/otlp/otlptrace/otlptracegrpc/go.mod b/exporters/otlp/otlptrace/otlptracegrpc/go.mod index 3ee7bae8e604..c087652c45b2 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/go.mod +++ b/exporters/otlp/otlptrace/otlptracegrpc/go.mod @@ -3,7 +3,7 @@ module go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc go 1.21 require ( - github.com/cenkalti/backoff/v4 v4.2.1 + github.com/cenkalti/backoff/v4 v4.3.0 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/otel v1.24.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 diff --git a/exporters/otlp/otlptrace/otlptracegrpc/go.sum b/exporters/otlp/otlptrace/otlptracegrpc/go.sum index 0a51e7e9a60b..9aecdf4f0387 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/go.sum +++ b/exporters/otlp/otlptrace/otlptracegrpc/go.sum @@ -1,5 +1,5 @@ -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/exporters/otlp/otlptrace/otlptracehttp/go.mod b/exporters/otlp/otlptrace/otlptracehttp/go.mod index a3d6a6084d01..4e2e875ad698 100644 --- a/exporters/otlp/otlptrace/otlptracehttp/go.mod +++ b/exporters/otlp/otlptrace/otlptracehttp/go.mod @@ -3,7 +3,7 @@ module go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp go 1.21 require ( - github.com/cenkalti/backoff/v4 v4.2.1 + github.com/cenkalti/backoff/v4 v4.3.0 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/otel v1.24.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 diff --git a/exporters/otlp/otlptrace/otlptracehttp/go.sum b/exporters/otlp/otlptrace/otlptracehttp/go.sum index 04a357bd0a9f..7113a3c58be1 100644 --- a/exporters/otlp/otlptrace/otlptracehttp/go.sum +++ b/exporters/otlp/otlptrace/otlptracehttp/go.sum @@ -1,5 +1,5 @@ -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/global/trace.go b/internal/global/trace.go index 2e765c6d07e6..596f716f40c0 100644 --- a/internal/global/trace.go +++ b/internal/global/trace.go @@ -182,6 +182,9 @@ func (nonRecordingSpan) RecordError(error, ...trace.EventOption) {} // AddEvent does nothing. func (nonRecordingSpan) AddEvent(string, ...trace.EventOption) {} +// AddLink does nothing. +func (nonRecordingSpan) AddLink(trace.Link) {} + // SetName does nothing. func (nonRecordingSpan) SetName(string) {} diff --git a/sdk/log/exporter.go b/sdk/log/exporter.go index dff4dc9c28d4..9f85f8a1fd92 100644 --- a/sdk/log/exporter.go +++ b/sdk/log/exporter.go @@ -5,6 +5,7 @@ package log // import "go.opentelemetry.io/otel/sdk/log" import ( "context" + "time" "go.opentelemetry.io/otel" ) @@ -53,6 +54,32 @@ func (noopExporter) Shutdown(context.Context) error { return nil } func (noopExporter) ForceFlush(context.Context) error { return nil } +// timeoutExporter wraps an Exporter and ensures any call to Export will have a +// timeout for the context. +type timeoutExporter struct { + Exporter + + // timeout is the maximum time an export is attempted. + timeout time.Duration +} + +// newTimeoutExporter wraps exporter with an Exporter that limits the context +// lifetime passed to Export to be timeout. If timeout is less than or equal to +// zero, exporter will be returned directly. +func newTimeoutExporter(exp Exporter, timeout time.Duration) Exporter { + if timeout <= 0 { + return exp + } + return &timeoutExporter{Exporter: exp, timeout: timeout} +} + +// Export sets the timeout of ctx before calling the Exporter e wraps. +func (e *timeoutExporter) Export(ctx context.Context, records []Record) error { + ctx, cancel := context.WithTimeout(ctx, e.timeout) + defer cancel() + return e.Exporter.Export(ctx, records) +} + // exportSync exports all data from input using exporter in a spawned // goroutine. The returned chan will be closed when the spawned goroutine // completes. diff --git a/sdk/log/exporter_test.go b/sdk/log/exporter_test.go index 4eb2056d1b25..3c37b83ad384 100644 --- a/sdk/log/exporter_test.go +++ b/sdk/log/exporter_test.go @@ -25,6 +25,9 @@ type instruction struct { type testExporter struct { // Err is the error returned by all methods of the testExporter. Err error + // ExportTrigger is read from prior to returning from the Export method if + // non-nil. + ExportTrigger chan struct{} // Counts of method calls. exportN, shutdownN, forceFlushN *int32 @@ -74,6 +77,13 @@ func (e *testExporter) Records() [][]Record { func (e *testExporter) Export(ctx context.Context, r []Record) error { atomic.AddInt32(e.exportN, 1) + if e.ExportTrigger != nil { + select { + case <-e.ExportTrigger: + case <-ctx.Done(): + return ctx.Err() + } + } e.input <- instruction{Record: &r} return e.Err } @@ -196,3 +206,40 @@ func TestExportSync(t *testing.T) { assert.ElementsMatch(t, want, got, "record bodies") }) } + +func TestTimeoutExporter(t *testing.T) { + t.Run("ZeroTimeout", func(t *testing.T) { + exp := newTestExporter(nil) + t.Cleanup(exp.Stop) + e := newTimeoutExporter(exp, 0) + assert.Same(t, exp, e) + }) + + t.Run("Timeout", func(t *testing.T) { + trigger := make(chan struct{}) + t.Cleanup(func() { close(trigger) }) + + exp := newTestExporter(nil) + t.Cleanup(exp.Stop) + exp.ExportTrigger = trigger + e := newTimeoutExporter(exp, time.Nanosecond) + + out := make(chan error, 1) + go func() { + out <- e.Export(context.Background(), make([]Record, 1)) + }() + + var err error + assert.Eventually(t, func() bool { + select { + case err = <-out: + return true + default: + return false + } + }, 2*time.Second, time.Microsecond) + + assert.ErrorIs(t, err, context.DeadlineExceeded) + close(out) + }) +} diff --git a/sdk/trace/span.go b/sdk/trace/span.go index 7a1ff3a2ea57..c44f6b926aa7 100644 --- a/sdk/trace/span.go +++ b/sdk/trace/span.go @@ -629,7 +629,7 @@ func (s *recordingSpan) Resource() *resource.Resource { return s.tracer.provider.resource } -func (s *recordingSpan) addLink(link trace.Link) { +func (s *recordingSpan) AddLink(link trace.Link) { if !s.IsRecording() || !link.SpanContext.IsValid() { return } @@ -803,6 +803,9 @@ func (nonRecordingSpan) RecordError(error, ...trace.EventOption) {} // AddEvent does nothing. func (nonRecordingSpan) AddEvent(string, ...trace.EventOption) {} +// AddLink does nothing. +func (nonRecordingSpan) AddLink(trace.Link) {} + // SetName does nothing. func (nonRecordingSpan) SetName(string) {} diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go index 0918dadcd2a8..615f4d58a1b0 100644 --- a/sdk/trace/trace_test.go +++ b/sdk/trace/trace_test.go @@ -1976,3 +1976,81 @@ func TestEmptyRecordingSpanAttributes(t *testing.T) { func TestEmptyRecordingSpanDroppedAttributes(t *testing.T) { assert.Equal(t, 0, (&recordingSpan{}).DroppedAttributes()) } + +func TestAddLinkWithInvalidSpanContext(t *testing.T) { + te := NewTestExporter() + sl := NewSpanLimits() + tp := NewTracerProvider( + WithSpanLimits(sl), + WithSyncer(te), + WithResource(resource.Empty()), + ) + span := startSpan(tp, "AddSpanWithInvalidSpanContext") + inValidContext := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID([16]byte{}), + SpanID: [8]byte{}, + }) + attrs := []attribute.KeyValue{{Key: "k", Value: attribute.StringValue("v")}} + span.AddLink(trace.Link{ + SpanContext: inValidContext, + Attributes: attrs, + }) + + want := &snapshot{ + name: "span0", + spanContext: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: tid, + TraceFlags: 0x1, + }), + parent: sc.WithRemote(true), + links: nil, + spanKind: trace.SpanKindInternal, + instrumentationScope: instrumentation.Scope{Name: "AddSpanWithInvalidSpanContext"}, + } + got, err := endSpan(te, span) + if err != nil { + t.Fatal(err) + } + if diff := cmpDiff(got, want); diff != "" { + t.Errorf("AddLinkWithInvalidSpanContext: -got +want %s", diff) + } +} + +func TestAddLink(t *testing.T) { + te := NewTestExporter() + sl := NewSpanLimits() + tp := NewTracerProvider( + WithSpanLimits(sl), + WithSyncer(te), + WithResource(resource.Empty()), + ) + attrs := []attribute.KeyValue{{Key: "k", Value: attribute.StringValue("v")}} + span := startSpan(tp, "AddSpan") + + link := trace.Link{SpanContext: sc, Attributes: attrs} + span.AddLink(link) + + want := &snapshot{ + name: "span0", + spanContext: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: tid, + TraceFlags: 0x1, + }), + parent: sc.WithRemote(true), + links: []Link{ + { + SpanContext: sc, + Attributes: attrs, + }, + }, + spanKind: trace.SpanKindInternal, + instrumentationScope: instrumentation.Scope{Name: "AddSpan"}, + } + got, err := endSpan(te, span) + if err != nil { + t.Fatal(err) + } + if diff := cmpDiff(got, want); diff != "" { + t.Errorf("AddLink: -got +want %s", diff) + } +} diff --git a/sdk/trace/tracer.go b/sdk/trace/tracer.go index 3e9d7452d4c9..3668b1387d0b 100644 --- a/sdk/trace/tracer.go +++ b/sdk/trace/tracer.go @@ -138,7 +138,7 @@ func (tr *tracer) newRecordingSpan(psc, sc trace.SpanContext, name string, sr Sa } for _, l := range config.Links() { - s.addLink(l) + s.AddLink(l) } s.SetAttributes(sr.Attributes...) diff --git a/trace/noop.go b/trace/noop.go index 84c775492ba8..ca20e9997aba 100644 --- a/trace/noop.go +++ b/trace/noop.go @@ -75,6 +75,9 @@ func (noopSpan) RecordError(error, ...EventOption) {} // AddEvent does nothing. func (noopSpan) AddEvent(string, ...EventOption) {} +// AddLink does nothing. +func (noopSpan) AddLink(Link) {} + // SetName does nothing. func (noopSpan) SetName(string) {} diff --git a/trace/noop/noop.go b/trace/noop/noop.go index a358993c1ee9..1dfa52c5216c 100644 --- a/trace/noop/noop.go +++ b/trace/noop/noop.go @@ -100,6 +100,9 @@ func (Span) RecordError(error, ...trace.EventOption) {} // AddEvent does nothing. func (Span) AddEvent(string, ...trace.EventOption) {} +// AddLink does nothing. +func (Span) AddLink(trace.Link) {} + // SetName does nothing. func (Span) SetName(string) {} diff --git a/trace/trace.go b/trace/trace.go index 019da4d78615..28877d4ab4df 100644 --- a/trace/trace.go +++ b/trace/trace.go @@ -350,6 +350,12 @@ type Span interface { // AddEvent adds an event with the provided name and options. AddEvent(name string, options ...EventOption) + // AddLink adds a link. + // Adding links at span creation using WithLinks is preferred to calling AddLink + // later, for contexts that are available during span creation, because head + // sampling decisions can only consider information present during span creation. + AddLink(link Link) + // IsRecording returns the recording state of the Span. It will return // true if the Span is active and events can be recorded. IsRecording() bool