1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-08-10 22:31:50 +02:00

sdk/trace: self-observability: span metrics (#7027)

Fixes https://github.com/open-telemetry/opentelemetry-go/issues/7003

Implement following self-observability metrics from
https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/otel/sdk-metrics.md
for https://pkg.go.dev/go.opentelemetry.io/otel/sdk/trace:
- `otel.sdk.span.live`
- `otel.sdk.span.started`
This commit is contained in:
Robert Pająk
2025-07-22 11:36:06 +02:00
committed by GitHub
parent 3e440ba5b2
commit 186def7c56
24 changed files with 773 additions and 11 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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.

63
sdk/trace/internal/x/x.go Normal file
View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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{})

View File

@@ -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
}