diff --git a/CHANGELOG.md b/CHANGELOG.md index 1906e3fc345..bcd4acc905f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - The `go.opentelemetry.io/otel/semconv/v1.36.0` package. The package contains semantic conventions from the `v1.36.0` version of the OpenTelemetry Semantic Conventions. See the [migration documentation](./semconv/v1.36.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.34.0.`(#7032) +- Add experimental self-observability span metrics in `go.opentelemetry.io/otel/sdk/trace`. + Check the `go.opentelemetry.io/otel/sdk/trace/internal/x` package documentation for more information. (#7027) ### Changed diff --git a/exporters/otlp/otlplog/otlploggrpc/go.mod b/exporters/otlp/otlplog/otlploggrpc/go.mod index b4d6329ff33..f647b113e69 100644 --- a/exporters/otlp/otlplog/otlploggrpc/go.mod +++ b/exporters/otlp/otlplog/otlploggrpc/go.mod @@ -50,3 +50,5 @@ replace go.opentelemetry.io/otel/trace => ../../../../trace replace go.opentelemetry.io/otel/metric => ../../../../metric replace go.opentelemetry.io/otel/sdk/log/logtest => ../../../../sdk/log/logtest + +replace go.opentelemetry.io/otel/sdk/metric => ../../../../sdk/metric diff --git a/exporters/otlp/otlplog/otlploggrpc/go.sum b/exporters/otlp/otlplog/otlploggrpc/go.sum index b3c3f5c4e57..3ca08e585b3 100644 --- a/exporters/otlp/otlplog/otlploggrpc/go.sum +++ b/exporters/otlp/otlplog/otlploggrpc/go.sum @@ -27,8 +27,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= diff --git a/exporters/otlp/otlplog/otlploghttp/go.mod b/exporters/otlp/otlplog/otlploghttp/go.mod index f7400f7c65c..5e533e08ad6 100644 --- a/exporters/otlp/otlplog/otlploghttp/go.mod +++ b/exporters/otlp/otlplog/otlploghttp/go.mod @@ -50,3 +50,5 @@ replace go.opentelemetry.io/otel/sdk => ../../../../sdk replace go.opentelemetry.io/otel/metric => ../../../../metric replace go.opentelemetry.io/otel/log => ../../../../log + +replace go.opentelemetry.io/otel/sdk/metric => ../../../../sdk/metric diff --git a/exporters/otlp/otlplog/otlploghttp/go.sum b/exporters/otlp/otlplog/otlploghttp/go.sum index b3c3f5c4e57..3ca08e585b3 100644 --- a/exporters/otlp/otlplog/otlploghttp/go.sum +++ b/exporters/otlp/otlplog/otlploghttp/go.sum @@ -27,8 +27,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= diff --git a/exporters/otlp/otlptrace/go.mod b/exporters/otlp/otlptrace/go.mod index f2f9ab1b0c0..75d1ee22a0d 100644 --- a/exporters/otlp/otlptrace/go.mod +++ b/exporters/otlp/otlptrace/go.mod @@ -31,3 +31,5 @@ replace go.opentelemetry.io/otel/sdk => ../../../sdk replace go.opentelemetry.io/otel/trace => ../../../trace replace go.opentelemetry.io/otel/metric => ../../../metric + +replace go.opentelemetry.io/otel/sdk/metric => ../../../sdk/metric diff --git a/exporters/otlp/otlptrace/otlptracegrpc/go.mod b/exporters/otlp/otlptrace/otlptracegrpc/go.mod index 62989443d7c..90aca9f4951 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/go.mod +++ b/exporters/otlp/otlptrace/otlptracegrpc/go.mod @@ -41,3 +41,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../ replace go.opentelemetry.io/otel/trace => ../../../../trace replace go.opentelemetry.io/otel/metric => ../../../../metric + +replace go.opentelemetry.io/otel/sdk/metric => ../../../../sdk/metric diff --git a/exporters/otlp/otlptrace/otlptracegrpc/go.sum b/exporters/otlp/otlptrace/otlptracegrpc/go.sum index cb9d2bd60f2..2bf964364a4 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/go.sum +++ b/exporters/otlp/otlptrace/otlptracegrpc/go.sum @@ -27,8 +27,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/exporters/otlp/otlptrace/otlptracehttp/go.mod b/exporters/otlp/otlptrace/otlptracehttp/go.mod index 3385126d1c9..67998500faf 100644 --- a/exporters/otlp/otlptrace/otlptracehttp/go.mod +++ b/exporters/otlp/otlptrace/otlptracehttp/go.mod @@ -40,3 +40,5 @@ replace go.opentelemetry.io/otel/sdk => ../../../../sdk replace go.opentelemetry.io/otel/trace => ../../../../trace replace go.opentelemetry.io/otel/metric => ../../../../metric + +replace go.opentelemetry.io/otel/sdk/metric => ../../../../sdk/metric diff --git a/exporters/otlp/otlptrace/otlptracehttp/go.sum b/exporters/otlp/otlptrace/otlptracehttp/go.sum index cb9d2bd60f2..2bf964364a4 100644 --- a/exporters/otlp/otlptrace/otlptracehttp/go.sum +++ b/exporters/otlp/otlptrace/otlptracehttp/go.sum @@ -27,8 +27,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/exporters/stdout/stdoutlog/go.mod b/exporters/stdout/stdoutlog/go.mod index 87865f54eeb..bf8039ce4a8 100644 --- a/exporters/stdout/stdoutlog/go.mod +++ b/exporters/stdout/stdoutlog/go.mod @@ -40,3 +40,5 @@ replace go.opentelemetry.io/otel/trace => ../../../trace replace go.opentelemetry.io/otel/sdk => ../../../sdk replace go.opentelemetry.io/otel/metric => ../../../metric + +replace go.opentelemetry.io/otel/sdk/metric => ../../../sdk/metric diff --git a/exporters/stdout/stdouttrace/go.mod b/exporters/stdout/stdouttrace/go.mod index 4a834d55ba9..45025073bcd 100644 --- a/exporters/stdout/stdouttrace/go.mod +++ b/exporters/stdout/stdouttrace/go.mod @@ -29,3 +29,5 @@ require ( replace go.opentelemetry.io/otel/trace => ../../../trace replace go.opentelemetry.io/otel/metric => ../../../metric + +replace go.opentelemetry.io/otel/sdk/metric => ../../../sdk/metric diff --git a/exporters/zipkin/go.mod b/exporters/zipkin/go.mod index 5f639c4b03a..44c50e0daff 100644 --- a/exporters/zipkin/go.mod +++ b/exporters/zipkin/go.mod @@ -30,3 +30,5 @@ replace go.opentelemetry.io/otel => ../.. replace go.opentelemetry.io/otel/sdk => ../../sdk replace go.opentelemetry.io/otel/metric => ../../metric + +replace go.opentelemetry.io/otel/sdk/metric => ../../sdk/metric diff --git a/sdk/go.mod b/sdk/go.mod index e41b8980107..c04cdaef225 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -10,6 +10,8 @@ require ( github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/metric v1.37.0 + go.opentelemetry.io/otel/sdk/metric v1.37.0 go.opentelemetry.io/otel/trace v1.37.0 go.uber.org/goleak v1.3.0 golang.org/x/sys v0.34.0 @@ -20,10 +22,11 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace go.opentelemetry.io/otel/trace => ../trace replace go.opentelemetry.io/otel/metric => ../metric + +replace go.opentelemetry.io/otel/sdk/metric => ./metric diff --git a/sdk/log/go.mod b/sdk/log/go.mod index e3c79d23474..a74e2ee0c32 100644 --- a/sdk/log/go.mod +++ b/sdk/log/go.mod @@ -32,3 +32,5 @@ replace go.opentelemetry.io/otel/sdk => ../ replace go.opentelemetry.io/otel/log => ../../log replace go.opentelemetry.io/otel => ../.. + +replace go.opentelemetry.io/otel/sdk/metric => ../metric diff --git a/sdk/log/logtest/go.mod b/sdk/log/logtest/go.mod index 382867a309b..506ba97a2fc 100644 --- a/sdk/log/logtest/go.mod +++ b/sdk/log/logtest/go.mod @@ -34,3 +34,5 @@ replace go.opentelemetry.io/otel/sdk/log => ../ replace go.opentelemetry.io/otel/log => ../../../log replace go.opentelemetry.io/otel => ../../.. + +replace go.opentelemetry.io/otel/sdk/metric => ../../metric diff --git a/sdk/trace/doc.go b/sdk/trace/doc.go index 1f60524e3ee..e58e7f6ed78 100644 --- a/sdk/trace/doc.go +++ b/sdk/trace/doc.go @@ -6,5 +6,8 @@ Package trace contains support for OpenTelemetry distributed tracing. The following assumes a basic familiarity with OpenTelemetry concepts. See https://opentelemetry.io. + +See [go.opentelemetry.io/otel/sdk/trace/internal/x] for information about +the experimental features. */ package trace // import "go.opentelemetry.io/otel/sdk/trace" diff --git a/sdk/trace/internal/x/README.md b/sdk/trace/internal/x/README.md new file mode 100644 index 00000000000..3afc23c3c38 --- /dev/null +++ b/sdk/trace/internal/x/README.md @@ -0,0 +1,35 @@ +# Experimental Features + +The metric SDK contains features that have not yet stabilized in the OpenTelemetry specification. +These features are added to the OpenTelemetry Go metric SDK prior to stabilization in the specification so that users can start experimenting with them and provide feedback. + +These feature may change in backwards incompatible ways as feedback is applied. +See the [Compatibility and Stability](#compatibility-and-stability) section for more information. + +## Features + +- [Self-Observability](#self-observability) + +### Self-Observability + +The SDK provides a self-observability feature that allows you to monitor the SDK itself. + +To opt-in, set the environment variable `OTEL_GO_X_SELF_OBSERVABILITY` to `true`. + +When enabled, the SDK will create the following metrics using the global `MeterProvider`: + +- `otel.sdk.span.live` +- `otel.sdk.span.started` + +Please see the [Semantic conventions for OpenTelemetry SDK metrics] documentation for more details on these metrics. + +[Semantic conventions for OpenTelemetry SDK metrics]: https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/otel/sdk-metrics.md + +## Compatibility and Stability + +Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../../VERSIONING.md). +These features may be removed or modified in successive version releases, including patch versions. + +When an experimental feature is promoted to a stable feature, a migration path will be included in the changelog entry of the release. +There is no guarantee that any environment variable feature flags that enabled the experimental feature will be supported by the stable version. +If they are supported, they may be accompanied with a deprecation notice stating a timeline for the removal of that support. diff --git a/sdk/trace/internal/x/x.go b/sdk/trace/internal/x/x.go new file mode 100644 index 00000000000..302294bc2c1 --- /dev/null +++ b/sdk/trace/internal/x/x.go @@ -0,0 +1,63 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package x documents experimental features for [go.opentelemetry.io/otel/sdk/trace]. +package x // import "go.opentelemetry.io/otel/sdk/trace/internal/x" + +import ( + "os" + "strings" +) + +// SelfObservability is an experimental feature flag that determines if SDK +// self-observability metrics are enabled. +// +// To enable this feature set the OTEL_GO_X_SELF_OBSERVABILITY environment variable +// to the case-insensitive string value of "true" (i.e. "True" and "TRUE" +// will also enable this). +var SelfObservability = newFeature("SELF_OBSERVABILITY", func(v string) (string, bool) { + if strings.ToLower(v) == "true" { + return v, true + } + return "", false +}) + +// Feature is an experimental feature control flag. It provides a uniform way +// to interact with these feature flags and parse their values. +type Feature[T any] struct { + key string + parse func(v string) (T, bool) +} + +func newFeature[T any](suffix string, parse func(string) (T, bool)) Feature[T] { + const envKeyRoot = "OTEL_GO_X_" + return Feature[T]{ + key: envKeyRoot + suffix, + parse: parse, + } +} + +// Key returns the environment variable key that needs to be set to enable the +// feature. +func (f Feature[T]) Key() string { return f.key } + +// Lookup returns the user configured value for the feature and true if the +// user has enabled the feature. Otherwise, if the feature is not enabled, a +// zero-value and false are returned. +func (f Feature[T]) Lookup() (v T, ok bool) { + // https://github.com/open-telemetry/opentelemetry-specification/blob/62effed618589a0bec416a87e559c0a9d96289bb/specification/configuration/sdk-environment-variables.md#parsing-empty-value + // + // > The SDK MUST interpret an empty value of an environment variable the + // > same way as when the variable is unset. + vRaw := os.Getenv(f.key) + if vRaw == "" { + return v, ok + } + return f.parse(vRaw) +} + +// Enabled returns if the feature is enabled. +func (f Feature[T]) Enabled() bool { + _, ok := f.Lookup() + return ok +} diff --git a/sdk/trace/internal/x/x_test.go b/sdk/trace/internal/x/x_test.go new file mode 100644 index 00000000000..15124ca91d1 --- /dev/null +++ b/sdk/trace/internal/x/x_test.go @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSelfObservability(t *testing.T) { + const key = "OTEL_GO_X_SELF_OBSERVABILITY" + require.Equal(t, key, SelfObservability.Key()) + + t.Run("100", run(setenv(key, "100"), assertDisabled(SelfObservability))) + t.Run("true", run(setenv(key, "true"), assertEnabled(SelfObservability, "true"))) + t.Run("True", run(setenv(key, "True"), assertEnabled(SelfObservability, "True"))) + t.Run("false", run(setenv(key, "false"), assertDisabled(SelfObservability))) + t.Run("empty", run(assertDisabled(SelfObservability))) +} + +func run(steps ...func(*testing.T)) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + for _, step := range steps { + step(t) + } + } +} + +func setenv(k, v string) func(t *testing.T) { //nolint:unparam // This is a reusable test utility function. + return func(t *testing.T) { t.Setenv(k, v) } +} + +func assertEnabled[T any](f Feature[T], want T) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + assert.True(t, f.Enabled(), "not enabled") + + v, ok := f.Lookup() + assert.True(t, ok, "Lookup state") + assert.Equal(t, want, v, "Lookup value") + } +} + +func assertDisabled[T any](f Feature[T]) func(*testing.T) { + var zero T + return func(t *testing.T) { + t.Helper() + + assert.False(t, f.Enabled(), "enabled") + + v, ok := f.Lookup() + assert.False(t, ok, "Lookup state") + assert.Equal(t, zero, v, "Lookup value") + } +} diff --git a/sdk/trace/provider.go b/sdk/trace/provider.go index 0e2a2e7c60d..950e1259f7c 100644 --- a/sdk/trace/provider.go +++ b/sdk/trace/provider.go @@ -159,6 +159,7 @@ func (p *TracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.T provider: p, instrumentationScope: is, } + t.initSelfObservability() p.namedTracer[is] = t } return t, ok diff --git a/sdk/trace/span.go b/sdk/trace/span.go index 549533ff9c2..84dad8049cd 100644 --- a/sdk/trace/span.go +++ b/sdk/trace/span.go @@ -20,7 +20,8 @@ import ( "go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/resource" - semconv "go.opentelemetry.io/otel/semconv/v1.34.0" + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/embedded" ) @@ -496,6 +497,22 @@ func (s *recordingSpan) End(options ...trace.SpanEndOption) { } s.mu.Unlock() + defer func() { + if s.tracer.selfObservabilityEnabled { + // Determine the sampling result and create the corresponding attribute. + var attrSamplingResult attribute.KeyValue + if s.spanContext.IsSampled() { + attrSamplingResult = s.tracer.spanLiveMetric.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ) + } else { + attrSamplingResult = s.tracer.spanLiveMetric.AttrSpanSamplingResult(otelconv.SpanSamplingResultRecordOnly) + } + + s.tracer.spanLiveMetric.Add(context.Background(), -1, attrSamplingResult) + } + }() + sps := s.tracer.provider.getSpanProcessors() if len(sps) == 0 { return diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go index 77e9f5c8630..a397488f9fa 100644 --- a/sdk/trace/trace_test.go +++ b/sdk/trace/trace_test.go @@ -22,9 +22,14 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/sdk" "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" "go.opentelemetry.io/otel/sdk/resource" - semconv "go.opentelemetry.io/otel/semconv/v1.34.0" + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" "go.opentelemetry.io/otel/trace" ) @@ -2177,6 +2182,495 @@ func TestAddLinkToNonRecordingSpan(t *testing.T) { } } +func TestSelfObservability(t *testing.T) { + testCases := []struct { + name string + test func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) + }{ + { + name: "SampledSpan", + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + tp := NewTracerProvider() + _, span := tp.Tracer("").Start(context.Background(), "StartSpan") + + want := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/otel/sdk/trace", + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, + }, + Metrics: []metricdata.Metrics{ + { + Name: otelconv.SDKSpanLive{}.Name(), + Description: otelconv.SDKSpanLive{}.Description(), + Unit: otelconv.SDKSpanLive{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanLive{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 1, + }, + }, + }, + }, + { + Name: otelconv.SDKSpanStarted{}.Name(), + Description: otelconv.SDKSpanStarted{}.Description(), + Unit: otelconv.SDKSpanStarted{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanStarted{}.AttrSpanParentOrigin( + otelconv.SpanParentOriginNone, + ), + otelconv.SDKSpanStarted{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 1, + }, + }, + }, + }, + }, + } + got := scopeMetrics() + metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp()) + + span.End() + + want = metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/otel/sdk/trace", + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, + }, + Metrics: []metricdata.Metrics{ + { + Name: otelconv.SDKSpanLive{}.Name(), + Description: otelconv.SDKSpanLive{}.Description(), + Unit: otelconv.SDKSpanLive{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanLive{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 0, // No live spans at this point. + }, + }, + }, + }, + { + Name: otelconv.SDKSpanStarted{}.Name(), + Description: otelconv.SDKSpanStarted{}.Description(), + Unit: otelconv.SDKSpanStarted{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanStarted{}.AttrSpanParentOrigin( + otelconv.SpanParentOriginNone, + ), + otelconv.SDKSpanStarted{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 1, + }, + }, + }, + }, + }, + } + got = scopeMetrics() + metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp()) + }, + }, + { + name: "NonRecordingSpan", + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + // Create a tracer provider with NeverSample sampler to get non-recording spans. + tp := NewTracerProvider(WithSampler(NeverSample())) + tp.Tracer("").Start(context.Background(), "NonRecordingSpan") + + want := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/otel/sdk/trace", + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, + }, + Metrics: []metricdata.Metrics{ + { + Name: otelconv.SDKSpanStarted{}.Name(), + Description: otelconv.SDKSpanStarted{}.Description(), + Unit: otelconv.SDKSpanStarted{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanStarted{}.AttrSpanParentOrigin( + otelconv.SpanParentOriginNone, + ), + otelconv.SDKSpanStarted{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultDrop, + ), + ), + Value: 1, + }, + }, + }, + }, + }, + } + + got := scopeMetrics() + metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp()) + }, + }, + { + name: "OnlyRecordingSpan", + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + // Create a tracer provider with NeverSample sampler to get non-recording spans. + tp := NewTracerProvider(WithSampler(RecordingOnly())) + tp.Tracer("").Start(context.Background(), "OnlyRecordingSpan") + + want := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/otel/sdk/trace", + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, + }, + Metrics: []metricdata.Metrics{ + { + Name: otelconv.SDKSpanLive{}.Name(), + Description: otelconv.SDKSpanLive{}.Description(), + Unit: otelconv.SDKSpanLive{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanLive{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordOnly, + ), + ), + Value: 1, + }, + }, + }, + }, + { + Name: otelconv.SDKSpanStarted{}.Name(), + Description: otelconv.SDKSpanStarted{}.Description(), + Unit: otelconv.SDKSpanStarted{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanStarted{}.AttrSpanParentOrigin( + otelconv.SpanParentOriginNone, + ), + otelconv.SDKSpanStarted{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordOnly, + ), + ), + Value: 1, + }, + }, + }, + }, + }, + } + + got := scopeMetrics() + metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp()) + }, + }, + { + name: "RemoteParentSpan", + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + // Create a remote parent context + tid, _ := trace.TraceIDFromHex("01020304050607080102040810203040") + sid, _ := trace.SpanIDFromHex("0102040810203040") + remoteCtx := trace.ContextWithRemoteSpanContext(context.Background(), + trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: tid, + SpanID: sid, + TraceFlags: 0x1, + Remote: true, + })) + + tp := NewTracerProvider() + tp.Tracer("").Start(remoteCtx, "ChildSpan") + + want := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/otel/sdk/trace", + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, + }, + Metrics: []metricdata.Metrics{ + { + Name: otelconv.SDKSpanLive{}.Name(), + Description: otelconv.SDKSpanLive{}.Description(), + Unit: otelconv.SDKSpanLive{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanLive{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 1, + }, + }, + }, + }, + { + Name: otelconv.SDKSpanStarted{}.Name(), + Description: otelconv.SDKSpanStarted{}.Description(), + Unit: otelconv.SDKSpanStarted{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanStarted{}.AttrSpanParentOrigin( + otelconv.SpanParentOriginRemote, + ), + otelconv.SDKSpanStarted{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 1, + }, + }, + }, + }, + }, + } + got := scopeMetrics() + metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp()) + }, + }, + { + name: "LocalParentSpan", + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + tp := NewTracerProvider() + ctx, parentSpan := tp.Tracer("").Start(context.Background(), "ParentSpan") + _, childSpan := tp.Tracer("").Start(ctx, "ChildSpan") + + want := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/otel/sdk/trace", + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, + }, + Metrics: []metricdata.Metrics{ + { + Name: otelconv.SDKSpanLive{}.Name(), + Description: otelconv.SDKSpanLive{}.Description(), + Unit: otelconv.SDKSpanLive{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanLive{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 2, // Both parent and child spans are active. + }, + }, + }, + }, + { + Name: otelconv.SDKSpanStarted{}.Name(), + Description: otelconv.SDKSpanStarted{}.Description(), + Unit: otelconv.SDKSpanStarted{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanStarted{}.AttrSpanParentOrigin( + otelconv.SpanParentOriginNone, + ), + otelconv.SDKSpanStarted{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 1, // Parent span with no parent of its own. + }, + { + Attributes: attribute.NewSet( + otelconv.SDKSpanStarted{}.AttrSpanParentOrigin( + otelconv.SpanParentOriginLocal, + ), + otelconv.SDKSpanStarted{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 1, // Child span with local parent. + }, + }, + }, + }, + }, + } + + got := scopeMetrics() + metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp()) + + childSpan.End() + parentSpan.End() + + want = metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/otel/sdk/trace", + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, + }, + Metrics: []metricdata.Metrics{ + { + Name: otelconv.SDKSpanLive{}.Name(), + Description: otelconv.SDKSpanLive{}.Description(), + Unit: otelconv.SDKSpanLive{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanLive{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 0, // No live spans after ending both. + }, + }, + }, + }, + { + Name: otelconv.SDKSpanStarted{}.Name(), + Description: otelconv.SDKSpanStarted{}.Description(), + Unit: otelconv.SDKSpanStarted{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKSpanStarted{}.AttrSpanParentOrigin( + otelconv.SpanParentOriginNone, + ), + otelconv.SDKSpanStarted{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + otelconv.SDKSpanStarted{}.AttrSpanParentOrigin( + otelconv.SpanParentOriginLocal, + ), + otelconv.SDKSpanStarted{}.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ), + ), + Value: 1, + }, + }, + }, + }, + }, + } + + got = scopeMetrics() + metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp()) + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "True") + prev := otel.GetMeterProvider() + defer otel.SetMeterProvider(prev) + r := metric.NewManualReader() + mp := metric.NewMeterProvider(metric.WithReader(r)) + otel.SetMeterProvider(mp) + + scopeMetrics := func() metricdata.ScopeMetrics { + var got metricdata.ResourceMetrics + err := r.Collect(context.Background(), &got) + require.NoError(t, err) + require.Len(t, got.ScopeMetrics, 1) + return got.ScopeMetrics[0] + } + tc.test(t, scopeMetrics) + }) + } +} + +// RecordingOnly creates a Sampler that samples no traces, but enables recording. +// The created sampler maintains any tracestate from the parent span context. +func RecordingOnly() Sampler { + return recordOnlySampler{} +} + +type recordOnlySampler struct{} + +// ShouldSample implements Sampler interface. It always returns Record but not Sample. +func (s recordOnlySampler) ShouldSample(p SamplingParameters) SamplingResult { + psc := trace.SpanContextFromContext(p.ParentContext) + return SamplingResult{ + Decision: RecordOnly, + Tracestate: psc.TraceState(), + } +} + +// Description returns description of the sampler. +func (recordOnlySampler) Description() string { + return "RecordingOnly" +} + +func TestRecordOnlySampler(t *testing.T) { + te := NewTestExporter() + tp := NewTracerProvider(WithSyncer(te), WithSampler(RecordingOnly())) + + _, span := tp.Tracer("RecordOnly").Start(context.Background(), "test-span") + + assert.True(t, span.IsRecording(), "span should be recording") + assert.False(t, span.SpanContext().IsSampled(), "span should not be sampled") + + span.End() + + assert.Zero(t, te.Len(), "no spans should be exported") +} + func BenchmarkTraceStart(b *testing.B) { tracer := NewTracerProvider().Tracer("") ctx := trace.ContextWithSpanContext(context.Background(), trace.SpanContext{}) diff --git a/sdk/trace/tracer.go b/sdk/trace/tracer.go index 0b65ae9ab70..07683efa518 100644 --- a/sdk/trace/tracer.go +++ b/sdk/trace/tracer.go @@ -7,7 +7,14 @@ import ( "context" "time" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk" "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/trace/internal/x" + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/embedded" ) @@ -17,10 +24,34 @@ type tracer struct { provider *TracerProvider instrumentationScope instrumentation.Scope + + selfObservabilityEnabled bool + spanLiveMetric otelconv.SDKSpanLive + spanStartedMetric otelconv.SDKSpanStarted } var _ trace.Tracer = &tracer{} +func (tr *tracer) initSelfObservability() { + if !x.SelfObservability.Enabled() { + return + } + + tr.selfObservabilityEnabled = true + mp := otel.GetMeterProvider() + m := mp.Meter("go.opentelemetry.io/otel/sdk/trace", + metric.WithInstrumentationVersion(sdk.Version()), + metric.WithSchemaURL(semconv.SchemaURL)) + + var err error + if tr.spanLiveMetric, err = otelconv.NewSDKSpanLive(m); err != nil { + otel.Handle(err) + } + if tr.spanStartedMetric, err = otelconv.NewSDKSpanStarted(m); err != nil { + otel.Handle(err) + } +} + // Start starts a Span and returns it along with a context containing it. // // The Span is created with the provided name and as a child of any existing @@ -46,6 +77,34 @@ func (tr *tracer) Start( } s := tr.newSpan(ctx, name, &config) + if tr.selfObservabilityEnabled { + // Check if the span has a parent span and set the origin attribute accordingly. + var attrParentOrigin attribute.KeyValue + if psc := trace.SpanContextFromContext(ctx); psc.IsValid() { + if psc.IsRemote() { + attrParentOrigin = tr.spanStartedMetric.AttrSpanParentOrigin(otelconv.SpanParentOriginRemote) + } else { + attrParentOrigin = tr.spanStartedMetric.AttrSpanParentOrigin(otelconv.SpanParentOriginLocal) + } + } else { + attrParentOrigin = tr.spanStartedMetric.AttrSpanParentOrigin(otelconv.SpanParentOriginNone) + } + + // Determine the sampling result and create the corresponding attribute. + var attrSamplingResult attribute.KeyValue + if s.SpanContext().IsSampled() && s.IsRecording() { + attrSamplingResult = tr.spanStartedMetric.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ) + } else if s.IsRecording() { + attrSamplingResult = tr.spanStartedMetric.AttrSpanSamplingResult(otelconv.SpanSamplingResultRecordOnly) + } else { + attrSamplingResult = tr.spanStartedMetric.AttrSpanSamplingResult(otelconv.SpanSamplingResultDrop) + } + + tr.spanStartedMetric.Add(context.Background(), 1, attrParentOrigin, attrSamplingResult) + } + if rw, ok := s.(ReadWriteSpan); ok && s.IsRecording() { sps := tr.provider.getSpanProcessors() for _, sp := range sps { @@ -153,6 +212,20 @@ func (tr *tracer) newRecordingSpan( s.SetAttributes(sr.Attributes...) s.SetAttributes(config.Attributes()...) + if tr.selfObservabilityEnabled { + // Determine the sampling result and create the corresponding attribute. + var attrSamplingResult attribute.KeyValue + if s.spanContext.IsSampled() { + attrSamplingResult = tr.spanLiveMetric.AttrSpanSamplingResult( + otelconv.SpanSamplingResultRecordAndSample, + ) + } else { + attrSamplingResult = tr.spanLiveMetric.AttrSpanSamplingResult(otelconv.SpanSamplingResultRecordOnly) + } + + tr.spanLiveMetric.Add(context.Background(), 1, attrSamplingResult) + } + return s }