diff --git a/CHANGELOG.md b/CHANGELOG.md index 58f1514f06d..2bb7a96733e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,16 +12,32 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `go.opentelemetry.io/otel/bridge/opencensus.InstallTraceBridge`, which installs the OpenCensus trace bridge, and replaces `opencensus.NewTracer`. (#4567) - Add scope version to trace and metric bridges in `go.opentelemetry.io/otel/bridge/opencensus`. (#4584) +- Add the `go.opentelemetry.io/otel/trace/embedded` package to be embedded in the exported trace API interfaces. (#4620) +- Add the `go.opentelemetry.io/otel/trace/noop` package as a default no-op implementation of the trace API. (#4620) - Add context propagation in `go.opentelemetry.io/otel/example/dice`. (#4644) ### Deprecated - Deprecate `go.opentelemetry.io/otel/bridge/opencensus.NewTracer` in favor of `opencensus.InstallTraceBridge`. (#4567) - Deprecate `go.opentelemetry.io/otel/example/fib` package is in favor of `go.opentelemetry.io/otel/example/dice`. (#4618) +- Deprecate `go.opentelemetry.io/otel/trace.NewNoopTracerProvider`. + Use the added `NewTracerProvider` function in `go.opentelemetry.io/otel/trace/noop` instead. (#4620) ### Changed - `go.opentelemetry.io/otel/bridge/opencensus.NewMetricProducer` returns a `*MetricProducer` struct instead of the metric.Producer interface. (#4583) +- The `TracerProvider` in `go.opentelemetry.io/otel/trace` now embeds the `go.opentelemetry.io/otel/trace/embedded.TracerProvider` type. + This extends the `TracerProvider` interface and is is a breaking change for any existing implementation. + Implementors need to update their implementations based on what they want the default behavior of the interface to be. + See the "API Implementations" section of the `go.opentelemetry.io/otel/trace` package documentation for more informatoin about how to accomplish this. (#4620) +- The `Tracer` in `go.opentelemetry.io/otel/trace` now embeds the `go.opentelemetry.io/otel/trace/embedded.Tracer` type. + This extends the `Tracer` interface and is is a breaking change for any existing implementation. + Implementors need to update their implementations based on what they want the default behavior of the interface to be. + See the "API Implementations" section of the `go.opentelemetry.io/otel/trace` package documentation for more informatoin about how to accomplish this. (#4620) +- The `Span` in `go.opentelemetry.io/otel/trace` now embeds the `go.opentelemetry.io/otel/trace/embedded.Span` type. + This extends the `Span` interface and is is a breaking change for any existing implementation. + Implementors need to update their implementations based on what they want the default behavior of the interface to be. + See the "API Implementations" section of the `go.opentelemetry.io/otel/trace` package documentation for more informatoin about how to accomplish this. (#4620) ## [1.19.0/0.42.0/0.0.7] 2023-09-28 diff --git a/bridge/opencensus/config_test.go b/bridge/opencensus/config_test.go index bad2dab8019..5a2011f0c98 100644 --- a/bridge/opencensus/config_test.go +++ b/bridge/opencensus/config_test.go @@ -20,12 +20,12 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) func TestNewTraceConfig(t *testing.T) { - globalTP := trace.NewNoopTracerProvider() - customTP := trace.NewNoopTracerProvider() + globalTP := noop.NewTracerProvider() + customTP := noop.NewTracerProvider() otel.SetTracerProvider(globalTP) for _, tc := range []struct { desc string diff --git a/bridge/opencensus/internal/tracer_test.go b/bridge/opencensus/internal/tracer_test.go index 1e3518a7efe..618a7b81567 100644 --- a/bridge/opencensus/internal/tracer_test.go +++ b/bridge/opencensus/internal/tracer_test.go @@ -24,6 +24,8 @@ import ( "go.opentelemetry.io/otel/bridge/opencensus/internal/oc2otel" "go.opentelemetry.io/otel/bridge/opencensus/internal/otel2oc" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" + "go.opentelemetry.io/otel/trace/noop" ) type handler struct{ err error } @@ -38,6 +40,8 @@ func withHandler() (*handler, func()) { } type tracer struct { + embedded.Tracer + ctx context.Context name string opts []trace.SpanStartOption @@ -45,8 +49,8 @@ type tracer struct { func (t *tracer) Start(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { t.ctx, t.name, t.opts = ctx, name, opts - noop := trace.NewNoopTracerProvider().Tracer("testing") - return noop.Start(ctx, name, opts...) + sub := noop.NewTracerProvider().Tracer("testing") + return sub.Start(ctx, name, opts...) } type ctxKey string @@ -110,11 +114,11 @@ func TestTracerFromContext(t *testing.T) { }) ctx := trace.ContextWithSpanContext(context.Background(), sc) - noop := trace.NewNoopTracerProvider().Tracer("TestTracerFromContext") + tracer := noop.NewTracerProvider().Tracer("TestTracerFromContext") // Test using the fact that the No-Op span will propagate a span context . - ctx, _ = noop.Start(ctx, "test") + ctx, _ = tracer.Start(ctx, "test") - got := internal.NewTracer(noop).FromContext(ctx).SpanContext() + got := internal.NewTracer(tracer).FromContext(ctx).SpanContext() // Do not test the convedsion, only that the propagtion. want := otel2oc.SpanContext(sc) if got != want { @@ -129,11 +133,11 @@ func TestTracerNewContext(t *testing.T) { }) ctx := trace.ContextWithSpanContext(context.Background(), sc) - noop := trace.NewNoopTracerProvider().Tracer("TestTracerNewContext") + tracer := noop.NewTracerProvider().Tracer("TestTracerNewContext") // Test using the fact that the No-Op span will propagate a span context . - _, s := noop.Start(ctx, "test") + _, s := tracer.Start(ctx, "test") - ocTracer := internal.NewTracer(noop) + ocTracer := internal.NewTracer(tracer) ctx = ocTracer.NewContext(context.Background(), internal.NewSpan(s)) got := trace.SpanContextFromContext(ctx) diff --git a/bridge/opentracing/bridge.go b/bridge/opentracing/bridge.go index 7dc81f56a32..cc3d7d19c7f 100644 --- a/bridge/opentracing/bridge.go +++ b/bridge/opentracing/bridge.go @@ -33,10 +33,11 @@ import ( iBaggage "go.opentelemetry.io/otel/internal/baggage" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) var ( - noopTracer = trace.NewNoopTracerProvider().Tracer("") + noopTracer = noop.NewTracerProvider().Tracer("") noopSpan = func() trace.Span { _, s := noopTracer.Start(context.Background(), "") return s diff --git a/bridge/opentracing/internal/mock.go b/bridge/opentracing/internal/mock.go index e9d90750a39..d3a8d91a30a 100644 --- a/bridge/opentracing/internal/mock.go +++ b/bridge/opentracing/internal/mock.go @@ -26,6 +26,8 @@ import ( "go.opentelemetry.io/otel/codes" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" + "go.opentelemetry.io/otel/trace/noop" ) //nolint:revive // ignoring missing comments for unexported global variables in an internal package. @@ -44,6 +46,8 @@ type MockContextKeyValue struct { } type MockTracer struct { + embedded.Tracer + FinishedSpans []*MockSpan SpareTraceIDs []trace.TraceID SpareSpanIDs []trace.SpanID @@ -184,6 +188,8 @@ type MockEvent struct { } type MockSpan struct { + embedded.Span + mockTracer *MockTracer officialTracer trace.Tracer spanContext trace.SpanContext @@ -295,4 +301,4 @@ func (s *MockSpan) OverrideTracer(tracer trace.Tracer) { s.officialTracer = tracer } -func (s *MockSpan) TracerProvider() trace.TracerProvider { return trace.NewNoopTracerProvider() } +func (s *MockSpan) TracerProvider() trace.TracerProvider { return noop.NewTracerProvider() } diff --git a/bridge/opentracing/provider.go b/bridge/opentracing/provider.go index 941e277baf8..90bad0bd516 100644 --- a/bridge/opentracing/provider.go +++ b/bridge/opentracing/provider.go @@ -18,11 +18,14 @@ import ( "sync" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" ) // TracerProvider is an OpenTelemetry TracerProvider that wraps an OpenTracing // Tracer. type TracerProvider struct { + embedded.TracerProvider + bridge *BridgeTracer provider trace.TracerProvider diff --git a/bridge/opentracing/provider_test.go b/bridge/opentracing/provider_test.go index 8af3796e031..1a4dd5ca5b5 100644 --- a/bridge/opentracing/provider_test.go +++ b/bridge/opentracing/provider_test.go @@ -19,6 +19,7 @@ import ( "go.opentelemetry.io/otel/bridge/opentracing/internal" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" ) type namedMockTracer struct { @@ -26,7 +27,7 @@ type namedMockTracer struct { *internal.MockTracer } -type namedMockTracerProvider struct{} +type namedMockTracerProvider struct{ embedded.TracerProvider } var _ trace.TracerProvider = (*namedMockTracerProvider)(nil) diff --git a/bridge/opentracing/wrapper.go b/bridge/opentracing/wrapper.go index c251f8e2c19..1e065e3bc62 100644 --- a/bridge/opentracing/wrapper.go +++ b/bridge/opentracing/wrapper.go @@ -19,6 +19,7 @@ import ( "go.opentelemetry.io/otel/bridge/opentracing/migration" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" ) // WrapperTracerProvider is an OpenTelemetry TracerProvider that wraps an @@ -26,6 +27,8 @@ import ( // // Deprecated: Use the TracerProvider from NewTracerProvider(...) instead. type WrapperTracerProvider struct { + embedded.TracerProvider + wTracer *WrapperTracer } @@ -56,6 +59,8 @@ func NewWrappedTracerProvider(bridge *BridgeTracer, tracer trace.Tracer) *Wrappe // aware how to operate in environment where OpenTracing API is also // used. type WrapperTracer struct { + embedded.Tracer + bridge *BridgeTracer tracer trace.Tracer } diff --git a/internal/global/state_test.go b/internal/global/state_test.go index 93bf6b8aae2..5a049edfeed 100644 --- a/internal/global/state_test.go +++ b/internal/global/state_test.go @@ -20,9 +20,10 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/metric/noop" + metricnoop "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" + tracenoop "go.opentelemetry.io/otel/trace/noop" ) type nonComparableTracerProvider struct { @@ -55,7 +56,7 @@ func TestSetTracerProvider(t *testing.T) { t.Run("First Set() should replace the delegate", func(t *testing.T) { ResetForTest(t) - SetTracerProvider(trace.NewNoopTracerProvider()) + SetTracerProvider(tracenoop.NewTracerProvider()) _, ok := TracerProvider().(*tracerProvider) if ok { @@ -67,7 +68,7 @@ func TestSetTracerProvider(t *testing.T) { ResetForTest(t) tp := TracerProvider() - SetTracerProvider(trace.NewNoopTracerProvider()) + SetTracerProvider(tracenoop.NewTracerProvider()) ntp := tp.(*tracerProvider) @@ -153,7 +154,7 @@ func TestSetMeterProvider(t *testing.T) { t.Run("First Set() should replace the delegate", func(t *testing.T) { ResetForTest(t) - SetMeterProvider(noop.NewMeterProvider()) + SetMeterProvider(metricnoop.NewMeterProvider()) _, ok := MeterProvider().(*meterProvider) if ok { @@ -166,7 +167,7 @@ func TestSetMeterProvider(t *testing.T) { mp := MeterProvider() - SetMeterProvider(noop.NewMeterProvider()) + SetMeterProvider(metricnoop.NewMeterProvider()) dmp := mp.(*meterProvider) diff --git a/internal/global/trace.go b/internal/global/trace.go index 5f008d0982b..3f61ec12a34 100644 --- a/internal/global/trace.go +++ b/internal/global/trace.go @@ -39,6 +39,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" ) // tracerProvider is a placeholder for a configured SDK TracerProvider. @@ -46,6 +47,8 @@ import ( // All TracerProvider functionality is forwarded to a delegate once // configured. type tracerProvider struct { + embedded.TracerProvider + mtx sync.Mutex tracers map[il]*tracer delegate trace.TracerProvider @@ -119,6 +122,8 @@ type il struct { // All Tracer functionality is forwarded to a delegate once configured. // Otherwise, all functionality is forwarded to a NoopTracer. type tracer struct { + embedded.Tracer + name string opts []trace.TracerOption provider *tracerProvider @@ -156,6 +161,8 @@ func (t *tracer) Start(ctx context.Context, name string, opts ...trace.SpanStart // SpanContext. It performs no operations other than to return the wrapped // SpanContext. type nonRecordingSpan struct { + embedded.Span + sc trace.SpanContext tracer *tracer } diff --git a/internal/global/trace_test.go b/internal/global/trace_test.go index f8f9149d9e0..d9807493854 100644 --- a/internal/global/trace_test.go +++ b/internal/global/trace_test.go @@ -23,9 +23,13 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" + "go.opentelemetry.io/otel/trace/noop" ) type fnTracerProvider struct { + embedded.TracerProvider + tracer func(string, ...trace.TracerOption) trace.Tracer } @@ -34,6 +38,8 @@ func (fn fnTracerProvider) Tracer(instrumentationName string, opts ...trace.Trac } type fnTracer struct { + embedded.Tracer + start func(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) } @@ -72,7 +78,7 @@ func TestTraceProviderDelegation(t *testing.T) { assert.Equal(t, want, spanName) } } - return trace.NewNoopTracerProvider().Tracer(name).Start(ctx, spanName) + return noop.NewTracerProvider().Tracer(name).Start(ctx, spanName) }, } }, @@ -107,7 +113,7 @@ func TestTraceProviderDelegates(t *testing.T) { tracer: func(name string, opts ...trace.TracerOption) trace.Tracer { called = true assert.Equal(t, "abc", name) - return trace.NewNoopTracerProvider().Tracer("") + return noop.NewTracerProvider().Tracer("") }, }) @@ -148,7 +154,7 @@ func TestTraceProviderDelegatesConcurrentSafe(t *testing.T) { // Signal the goroutine to finish. close(quit) } - return trace.NewNoopTracerProvider().Tracer("") + return noop.NewTracerProvider().Tracer("") }, }) @@ -195,7 +201,7 @@ func TestTracerDelegatesConcurrentSafe(t *testing.T) { // Signal the goroutine to finish. close(quit) } - return trace.NewNoopTracerProvider().Tracer("").Start(ctx, spanName) + return noop.NewTracerProvider().Tracer("").Start(ctx, spanName) }, } }, @@ -218,7 +224,7 @@ func TestTraceProviderDelegatesSameInstance(t *testing.T) { SetTracerProvider(fnTracerProvider{ tracer: func(name string, opts ...trace.TracerOption) trace.Tracer { - return trace.NewNoopTracerProvider().Tracer("") + return noop.NewTracerProvider().Tracer("") }, }) diff --git a/sdk/trace/provider.go b/sdk/trace/provider.go index 0a018c14ded..7d46c4b48e5 100644 --- a/sdk/trace/provider.go +++ b/sdk/trace/provider.go @@ -25,6 +25,8 @@ import ( "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" + "go.opentelemetry.io/otel/trace/noop" ) const ( @@ -73,6 +75,8 @@ func (cfg tracerProviderConfig) MarshalLog() interface{} { // TracerProvider is an OpenTelemetry TracerProvider. It provides Tracers to // instrumentation so it can trace operational flow through a system. type TracerProvider struct { + embedded.TracerProvider + mu sync.Mutex namedTracer map[instrumentation.Scope]*tracer spanProcessors atomic.Pointer[spanProcessorStates] @@ -139,7 +143,7 @@ func NewTracerProvider(opts ...TracerProviderOption) *TracerProvider { func (p *TracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.Tracer { // This check happens before the mutex is acquired to avoid deadlocking if Tracer() is called from within Shutdown(). if p.isShutdown.Load() { - return trace.NewNoopTracerProvider().Tracer(name, opts...) + return noop.NewTracerProvider().Tracer(name, opts...) } c := trace.NewTracerConfig(opts...) if name == "" { @@ -157,7 +161,7 @@ func (p *TracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.T // Must check the flag after acquiring the mutex to avoid returning a valid tracer if Shutdown() ran // after the first check above but before we acquired the mutex. if p.isShutdown.Load() { - return trace.NewNoopTracerProvider().Tracer(name, opts...), true + return noop.NewTracerProvider().Tracer(name, opts...), true } t, ok := p.namedTracer[is] if !ok { diff --git a/sdk/trace/span.go b/sdk/trace/span.go index 68ac1b01616..36dbf67764b 100644 --- a/sdk/trace/span.go +++ b/sdk/trace/span.go @@ -32,6 +32,7 @@ import ( "go.opentelemetry.io/otel/sdk/resource" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" ) // ReadOnlySpan allows reading information from the data structure underlying a @@ -108,6 +109,8 @@ type ReadWriteSpan interface { // recordingSpan is an implementation of the OpenTelemetry Span API // representing the individual component of a trace that is sampled. type recordingSpan struct { + embedded.Span + // mu protects the contents of this span. mu sync.Mutex @@ -774,6 +777,8 @@ func (s *recordingSpan) runtimeTrace(ctx context.Context) context.Context { // that wraps a SpanContext. It performs no operations other than to return // the wrapped SpanContext or TracerProvider that created it. type nonRecordingSpan struct { + embedded.Span + // tracer is the SDK tracer that created this span. tracer *tracer sc trace.SpanContext diff --git a/sdk/trace/tracer.go b/sdk/trace/tracer.go index 85a71227f3f..301e1a7abcc 100644 --- a/sdk/trace/tracer.go +++ b/sdk/trace/tracer.go @@ -20,9 +20,12 @@ import ( "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" ) type tracer struct { + embedded.Tracer + provider *TracerProvider instrumentationScope instrumentation.Scope } diff --git a/trace/doc.go b/trace/doc.go index ab0346f9664..440f3d7565a 100644 --- a/trace/doc.go +++ b/trace/doc.go @@ -62,5 +62,69 @@ a default. defer span.End() // ... } + +# API Implementations + +This package does not conform to the standard Go versioning policy; all of its +interfaces may have methods added to them without a package major version bump. +This non-standard API evolution could surprise an uninformed implementation +author. They could unknowingly build their implementation in a way that would +result in a runtime panic for their users that update to the new API. + +The API is designed to help inform an instrumentation author about this +non-standard API evolution. It requires them to choose a default behavior for +unimplemented interface methods. There are three behavior choices they can +make: + + - Compilation failure + - Panic + - Default to another implementation + +All interfaces in this API embed a corresponding interface from +[go.opentelemetry.io/otel/trace/embedded]. If an author wants the default +behavior of their implementations to be a compilation failure, signaling to +their users they need to update to the latest version of that implementation, +they need to embed the corresponding interface from +[go.opentelemetry.io/otel/trace/embedded] in their implementation. For +example, + + import "go.opentelemetry.io/otel/trace/embedded" + + type TracerProvider struct { + embedded.TracerProvider + // ... + } + +If an author wants the default behavior of their implementations to panic, they +can embed the API interface directly. + + import "go.opentelemetry.io/otel/trace" + + type TracerProvider struct { + trace.TracerProvider + // ... + } + +This option is not recommended. It will lead to publishing packages that +contain runtime panics when users update to newer versions of +[go.opentelemetry.io/otel/trace], which may be done with a trasitive +dependency. + +Finally, an author can embed another implementation in theirs. The embedded +implementation will be used for methods not defined by the author. For example, +an author who wants to default to silently dropping the call can use +[go.opentelemetry.io/otel/trace/noop]: + + import "go.opentelemetry.io/otel/trace/noop" + + type TracerProvider struct { + noop.TracerProvider + // ... + } + +It is strongly recommended that authors only embed +[go.opentelemetry.io/otel/trace/noop] if they choose this default behavior. +That implementation is the only one OpenTelemetry authors can guarantee will +fully implement all the API interfaces when a user updates their API. */ package trace // import "go.opentelemetry.io/otel/trace" diff --git a/trace/embedded/embedded.go b/trace/embedded/embedded.go new file mode 100644 index 00000000000..898db5a7546 --- /dev/null +++ b/trace/embedded/embedded.go @@ -0,0 +1,56 @@ +// 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 embedded provides interfaces embedded within the [OpenTelemetry +// trace API]. +// +// Implementers of the [OpenTelemetry trace API] can embed the relevant type +// from this package into their implementation directly. Doing so will result +// in a compilation error for users when the [OpenTelemetry trace API] is +// extended (which is something that can happen without a major version bump of +// the API package). +// +// [OpenTelemetry trace API]: https://pkg.go.dev/go.opentelemetry.io/otel/trace +package embedded // import "go.opentelemetry.io/otel/trace/embedded" + +// TracerProvider is embedded in +// [go.opentelemetry.io/otel/trace.TracerProvider]. +// +// Embed this interface in your implementation of the +// [go.opentelemetry.io/otel/trace.TracerProvider] if you want users to +// experience a compilation error, signaling they need to update to your latest +// implementation, when the [go.opentelemetry.io/otel/trace.TracerProvider] +// interface is extended (which is something that can happen without a major +// version bump of the API package). +type TracerProvider interface{ tracerProvider() } + +// Tracer is embedded in [go.opentelemetry.io/otel/trace.Tracer]. +// +// Embed this interface in your implementation of the +// [go.opentelemetry.io/otel/trace.Tracer] if you want users to experience a +// compilation error, signaling they need to update to your latest +// implementation, when the [go.opentelemetry.io/otel/trace.Tracer] interface +// is extended (which is something that can happen without a major version bump +// of the API package). +type Tracer interface{ tracer() } + +// Span is embedded in [go.opentelemetry.io/otel/trace.Span]. +// +// Embed this interface in your implementation of the +// [go.opentelemetry.io/otel/trace.Span] if you want users to experience a +// compilation error, signaling they need to update to your latest +// implementation, when the [go.opentelemetry.io/otel/trace.Span] interface is +// extended (which is something that can happen without a major version bump of +// the API package). +type Span interface{ span() } diff --git a/trace/noop.go b/trace/noop.go index 7cf6c7f3ef9..c125491caeb 100644 --- a/trace/noop.go +++ b/trace/noop.go @@ -19,16 +19,20 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace/embedded" ) // NewNoopTracerProvider returns an implementation of TracerProvider that // performs no operations. The Tracer and Spans created from the returned // TracerProvider also perform no operations. +// +// Deprecated: Use [go.opentelemetry.io/otel/trace/noop.NewTracerProvider] +// instead. func NewNoopTracerProvider() TracerProvider { return noopTracerProvider{} } -type noopTracerProvider struct{} +type noopTracerProvider struct{ embedded.TracerProvider } var _ TracerProvider = noopTracerProvider{} @@ -38,7 +42,7 @@ func (p noopTracerProvider) Tracer(string, ...TracerOption) Tracer { } // noopTracer is an implementation of Tracer that performs no operations. -type noopTracer struct{} +type noopTracer struct{ embedded.Tracer } var _ Tracer = noopTracer{} @@ -54,7 +58,7 @@ func (t noopTracer) Start(ctx context.Context, name string, _ ...SpanStartOption } // noopSpan is an implementation of Span that performs no operations. -type noopSpan struct{} +type noopSpan struct{ embedded.Span } var _ Span = noopSpan{} diff --git a/trace/noop/noop.go b/trace/noop/noop.go new file mode 100644 index 00000000000..7f485543c47 --- /dev/null +++ b/trace/noop/noop.go @@ -0,0 +1,118 @@ +// 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 noop provides an implementation of the OpenTelemetry trace API that +// produces no telemetry and minimizes used computation resources. +// +// Using this package to implement the OpenTelemetry trace API will effectively +// disable OpenTelemetry. +// +// This implementation can be embedded in other implementations of the +// OpenTelemetry trace API. Doing so will mean the implementation defaults to +// no operation for methods it does not implement. +package noop // import "go.opentelemetry.io/otel/trace/noop" + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" +) + +var ( + // Compile-time check this implements the OpenTelemetry API. + + _ trace.TracerProvider = TracerProvider{} + _ trace.Tracer = Tracer{} + _ trace.Span = Span{} +) + +// TracerProvider is an OpenTelemetry No-Op TracerProvider. +type TracerProvider struct{ embedded.TracerProvider } + +// NewTracerProvider returns a TracerProvider that does not record any telemetry. +func NewTracerProvider() TracerProvider { + return TracerProvider{} +} + +// Tracer returns an OpenTelemetry Tracer that does not record any telemetry. +func (TracerProvider) Tracer(string, ...trace.TracerOption) trace.Tracer { + return Tracer{} +} + +// Tracer is an OpenTelemetry No-Op Tracer. +type Tracer struct{ embedded.Tracer } + +// Start creates a span. The created span will be set in a child context of ctx +// and returned with the span. +// +// If ctx contains a span context, the returned span will also contain that +// span context. If the span context in ctx is for a non-recording span, that +// span instance will be returned directly. +func (t Tracer) Start(ctx context.Context, _ string, _ ...trace.SpanStartOption) (context.Context, trace.Span) { + span := trace.SpanFromContext(ctx) + + // If the parent context contains a non-zero span context, that span + // context needs to be returned as a non-recording span + // (https://github.com/open-telemetry/opentelemetry-specification/blob/3a1dde966a4ce87cce5adf464359fe369741bbea/specification/trace/api.md#behavior-of-the-api-in-the-absence-of-an-installed-sdk). + var zeroSC trace.SpanContext + if sc := span.SpanContext(); !sc.Equal(zeroSC) { + if !span.IsRecording() { + // If the span is not recording return it directly. + return ctx, span + } + // Otherwise, return the span context needs in a non-recording span. + span = Span{sc: sc} + } else { + // No parent, return a No-Op span with an empty span context. + span = Span{} + } + return trace.ContextWithSpan(ctx, span), span +} + +// Span is an OpenTelemetry No-Op Span. +type Span struct { + embedded.Span + + sc trace.SpanContext +} + +// SpanContext returns an empty span context. +func (s Span) SpanContext() trace.SpanContext { return s.sc } + +// IsRecording always returns false. +func (Span) IsRecording() bool { return false } + +// SetStatus does nothing. +func (Span) SetStatus(codes.Code, string) {} + +// SetAttributes does nothing. +func (Span) SetAttributes(...attribute.KeyValue) {} + +// End does nothing. +func (Span) End(...trace.SpanEndOption) {} + +// RecordError does nothing. +func (Span) RecordError(error, ...trace.EventOption) {} + +// AddEvent does nothing. +func (Span) AddEvent(string, ...trace.EventOption) {} + +// SetName does nothing. +func (Span) SetName(string) {} + +// TracerProvider returns a No-Op TracerProvider. +func (Span) TracerProvider() trace.TracerProvider { return TracerProvider{} } diff --git a/trace/noop/noop_test.go b/trace/noop/noop_test.go new file mode 100644 index 00000000000..2b0f7818e20 --- /dev/null +++ b/trace/noop/noop_test.go @@ -0,0 +1,117 @@ +// 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 noop // import "go.opentelemetry.io/otel/trace/noop" + +import ( + "context" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/trace" +) + +func TestImplementationNoPanics(t *testing.T) { + // Check that if type has an embedded interface and that interface has + // methods added to it than the No-Op implementation implements them. + t.Run("TracerProvider", assertAllExportedMethodNoPanic( + reflect.ValueOf(TracerProvider{}), + reflect.TypeOf((*trace.TracerProvider)(nil)).Elem(), + )) + t.Run("Meter", assertAllExportedMethodNoPanic( + reflect.ValueOf(Tracer{}), + reflect.TypeOf((*trace.Tracer)(nil)).Elem(), + )) + t.Run("Span", assertAllExportedMethodNoPanic( + reflect.ValueOf(Span{}), + reflect.TypeOf((*trace.Span)(nil)).Elem(), + )) +} + +func assertAllExportedMethodNoPanic(rVal reflect.Value, rType reflect.Type) func(*testing.T) { + return func(t *testing.T) { + for n := 0; n < rType.NumMethod(); n++ { + mType := rType.Method(n) + if !mType.IsExported() { + t.Logf("ignoring unexported %s", mType.Name) + continue + } + m := rVal.MethodByName(mType.Name) + if !m.IsValid() { + t.Errorf("unknown method for %s: %s", rVal.Type().Name(), mType.Name) + } + + numIn := mType.Type.NumIn() + if mType.Type.IsVariadic() { + numIn-- + } + args := make([]reflect.Value, numIn) + ctx := context.Background() + for i := range args { + aType := mType.Type.In(i) + if aType.Name() == "Context" { + // Do not panic on a nil context. + args[i] = reflect.ValueOf(ctx) + } else { + args[i] = reflect.New(aType).Elem() + } + } + + assert.NotPanicsf(t, func() { + _ = m.Call(args) + }, "%s.%s", rVal.Type().Name(), mType.Name) + } + } +} + +func TestNewTracerProvider(t *testing.T) { + tp := NewTracerProvider() + assert.Equal(t, tp, TracerProvider{}) + tracer := tp.Tracer("") + assert.Equal(t, tracer, Tracer{}) +} + +func TestTracerStartPropagatesSpanContext(t *testing.T) { + tracer := NewTracerProvider().Tracer("") + spanCtx := trace.SpanContext{} + + ctx := trace.ContextWithSpanContext(context.Background(), spanCtx) + ctx, span := tracer.Start(ctx, "test_span") + assert.Equal(t, spanCtx, trace.SpanContextFromContext(ctx), "empty span context not set in context") + assert.IsType(t, Span{}, span, "non-noop span returned") + assert.Equal(t, spanCtx, span.SpanContext(), "empty span context not returned from span") + assert.False(t, span.IsRecording(), "empty span context returned recording span") + + spanCtx = spanCtx.WithTraceID(trace.TraceID([16]byte{1})) + spanCtx = spanCtx.WithSpanID(trace.SpanID([8]byte{1})) + ctx = trace.ContextWithSpanContext(context.Background(), spanCtx) + ctx, span = tracer.Start(ctx, "test_span") + assert.Equal(t, spanCtx, trace.SpanContextFromContext(ctx), "non-empty span context not set in context") + assert.Equal(t, spanCtx, span.SpanContext(), "non-empty span context not returned from span") + assert.False(t, span.IsRecording(), "non-empty span context returned recording span") + + rSpan := recordingSpan{Span: Span{sc: spanCtx}} + ctx = trace.ContextWithSpan(context.Background(), rSpan) + ctx, span = tracer.Start(ctx, "test_span") + assert.Equal(t, spanCtx, trace.SpanContextFromContext(ctx), "recording span's span context not set in context") + assert.IsType(t, Span{}, span, "non-noop span returned") + assert.Equal(t, spanCtx, span.SpanContext(), "recording span's span context not returned from span") + assert.False(t, span.IsRecording(), "recording span returned") +} + +type recordingSpan struct{ Span } + +func (recordingSpan) IsRecording() bool { return true } diff --git a/trace/trace.go b/trace/trace.go index d3195558761..26a4b2260ec 100644 --- a/trace/trace.go +++ b/trace/trace.go @@ -22,6 +22,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace/embedded" ) const ( @@ -342,8 +343,15 @@ func (sc SpanContext) MarshalJSON() ([]byte, error) { // create a Span and it is then up to the operation the Span represents to // properly end the Span when the operation itself ends. // -// Warning: methods may be added to this interface in minor releases. +// Warning: Methods may be added to this interface in minor releases. See +// package documentation on API implementation for information on how to set +// default behavior for unimplemented methods. type Span interface { + // Users of the interface can ignore this. This embedded type is only used + // by implementations of this interface. See the "API Implementations" + // section of the package documentation for more information. + embedded.Span + // End completes the Span. The Span is considered complete and ready to be // delivered through the rest of the telemetry pipeline after this method // is called. Therefore, updates to the Span are not allowed after this @@ -490,8 +498,15 @@ func (sk SpanKind) String() string { // Tracer is the creator of Spans. // -// Warning: methods may be added to this interface in minor releases. +// Warning: Methods may be added to this interface in minor releases. See +// package documentation on API implementation for information on how to set +// default behavior for unimplemented methods. type Tracer interface { + // Users of the interface can ignore this. This embedded type is only used + // by implementations of this interface. See the "API Implementations" + // section of the package documentation for more information. + embedded.Tracer + // Start creates a span and a context.Context containing the newly-created span. // // If the context.Context provided in `ctx` contains a Span then the newly-created @@ -522,8 +537,15 @@ type Tracer interface { // at runtime from its users or it can simply use the globally registered one // (see https://pkg.go.dev/go.opentelemetry.io/otel#GetTracerProvider). // -// Warning: methods may be added to this interface in minor releases. +// Warning: Methods may be added to this interface in minor releases. See +// package documentation on API implementation for information on how to set +// default behavior for unimplemented methods. type TracerProvider interface { + // Users of the interface can ignore this. This embedded type is only used + // by implementations of this interface. See the "API Implementations" + // section of the package documentation for more information. + embedded.TracerProvider + // Tracer returns a unique Tracer scoped to be used by instrumentation code // to trace computational workflows. The scope and identity of that // instrumentation code is uniquely defined by the name and options passed. diff --git a/trace_test.go b/trace_test.go index 48d245b4116..21829767fd0 100644 --- a/trace_test.go +++ b/trace_test.go @@ -20,19 +20,21 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" + "go.opentelemetry.io/otel/trace/noop" ) -type testTracerProvider struct{} +type testTracerProvider struct{ embedded.TracerProvider } var _ trace.TracerProvider = &testTracerProvider{} func (*testTracerProvider) Tracer(_ string, _ ...trace.TracerOption) trace.Tracer { - return trace.NewNoopTracerProvider().Tracer("") + return noop.NewTracerProvider().Tracer("") } func TestMultipleGlobalTracerProvider(t *testing.T) { p1 := testTracerProvider{} - p2 := trace.NewNoopTracerProvider() + p2 := noop.NewTracerProvider() SetTracerProvider(&p1) SetTracerProvider(p2)