1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2026-06-19 21:45:50 +02:00

Add support for experimental options in the metrics API (#8111)

Part of https://github.com/open-telemetry/opentelemetry-go/issues/8110

Related to
https://github.com/open-telemetry/opentelemetry-go/issues/5882.

I'm hoping to find a better way to support experimental Options types in
our API packages. This is one approach to consider.

This contains no public API changes. It introduces a type:
`ExperimentalOption` in `/metric/internal/x`, which can be used by our
experimental options defined outside of the module. Options that embed
this interface are ignored by `New*Config` builder functions in the
metrics API to prevent them from panicing when used. Only SDKs that
explicitly support the experimental option in question will respect it.
Alternative SDKs will ignore the experimental options. We would still
need to treat ExperimentalOption as a stable artifact, since the SDK
will indirectly depend on it.

See
https://github.com/open-telemetry/opentelemetry-go/compare/main...dashpole:opentelemetry-go:attributes_advisory
for how this would be used to support the advisory attributes parameter.
This commit is contained in:
David Ashpole
2026-04-03 15:56:57 -04:00
committed by GitHub
parent 9dab9bb36f
commit 28df982c8b
18 changed files with 244 additions and 2 deletions
+27 -1
View File
@@ -1135,13 +1135,39 @@ New telemetry signals (e.g., Logs before stabilization) and components (e.g. bri
The package should have the final name it will use once stabilized (i.e. not `/x`), and is released at a v0.x.y version to indicate it is not stable.
Most new components are hosted in [opentelemetry-go-contrib](https://github.com/open-telemetry/opentelemetry-go-contrib).
#### Experimental options for API or SDK functions
Experimental Options functions are implemented in an experimental module (e.g., `go.opentelemetry.io/otel/sdk/x`).
The return type of the Option function must embed the option's type (e.g. `metric.InstrumentOption`), and have an `Experimental()` method to prevent the API from panicking when the option is used.
The SDK uses type assertions (without importing the unstable package) to check if passing types implement these experimental interfaces.
The SDK must not depend on the experimental module.
For example:
```go
type myOption struct {
// Embed the stable option type.
metric.InstrumentOption
value string
}
// Experimental prevents the API from panicking when the option is used.
func (o myOption) Experimental() {}
// The SDK can use type assertions to use this function.
func (o myOption) Value() string { return o.value }
func WithMyOption(value string) metric.InstrumentOption {
return myOption{value: value}
}
```
#### Not Supported
The following kinds of experimental features are **not currently supported** on stable interfaces:
- Experimental methods on API interfaces
- Experimental fields for API or SDK exported structs
- Experimental options for API or SDK functions
In some cases forks or long-lived branches may be used for prototyping these features.
+7
View File
@@ -74,10 +74,17 @@ type LoggerConfig struct {
attrs attribute.Set
}
type experimentalOption interface {
Experimental()
}
// NewLoggerConfig returns a new [LoggerConfig] with all the options applied.
func NewLoggerConfig(options ...LoggerOption) LoggerConfig {
var c LoggerConfig
for _, opt := range options {
if _, ok := opt.(experimentalOption); ok {
continue
}
c = opt.applyLogger(c)
}
return c
+12
View File
@@ -196,3 +196,15 @@ func BenchmarkNewLoggerConfig(b *testing.B) {
})
}
}
type testExperimentalOption struct {
log.LoggerOption
}
func (testExperimentalOption) Experimental() {}
func TestExperimentalOptionSafe(t *testing.T) {
var opt testExperimentalOption
assert.NotPanics(t, func() { _ = log.NewLoggerConfig(opt) })
}
+9
View File
@@ -51,6 +51,9 @@ type Float64ObservableCounterConfig struct {
func NewFloat64ObservableCounterConfig(opts ...Float64ObservableCounterOption) Float64ObservableCounterConfig {
var config Float64ObservableCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64ObservableCounter(config)
}
return config
@@ -111,6 +114,9 @@ func NewFloat64ObservableUpDownCounterConfig(
) Float64ObservableUpDownCounterConfig {
var config Float64ObservableUpDownCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64ObservableUpDownCounter(config)
}
return config
@@ -168,6 +174,9 @@ type Float64ObservableGaugeConfig struct {
func NewFloat64ObservableGaugeConfig(opts ...Float64ObservableGaugeOption) Float64ObservableGaugeConfig {
var config Float64ObservableGaugeConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64ObservableGauge(config)
}
return config
+9
View File
@@ -50,6 +50,9 @@ type Int64ObservableCounterConfig struct {
func NewInt64ObservableCounterConfig(opts ...Int64ObservableCounterOption) Int64ObservableCounterConfig {
var config Int64ObservableCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64ObservableCounter(config)
}
return config
@@ -110,6 +113,9 @@ func NewInt64ObservableUpDownCounterConfig(
) Int64ObservableUpDownCounterConfig {
var config Int64ObservableUpDownCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64ObservableUpDownCounter(config)
}
return config
@@ -167,6 +173,9 @@ type Int64ObservableGaugeConfig struct {
func NewInt64ObservableGaugeConfig(opts ...Int64ObservableGaugeOption) Int64ObservableGaugeConfig {
var config Int64ObservableGaugeConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64ObservableGauge(config)
}
return config
+7
View File
@@ -42,11 +42,18 @@ type MeterOption interface {
applyMeter(MeterConfig) MeterConfig
}
type experimentalOption interface {
Experimental()
}
// NewMeterConfig creates a new MeterConfig and applies
// all the given options.
func NewMeterConfig(opts ...MeterOption) MeterConfig {
var config MeterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyMeter(config)
}
return config
+46
View File
@@ -197,3 +197,49 @@ func BenchmarkNewMeterConfig(b *testing.B) {
})
}
}
type testExperimentalOption struct {
metric.MeterOption
metric.Int64CounterOption
metric.Int64UpDownCounterOption
metric.Int64HistogramOption
metric.Int64GaugeOption
metric.Float64CounterOption
metric.Float64UpDownCounterOption
metric.Float64HistogramOption
metric.Float64GaugeOption
metric.Int64ObservableCounterOption
metric.Int64ObservableUpDownCounterOption
metric.Int64ObservableGaugeOption
metric.Float64ObservableCounterOption
metric.Float64ObservableUpDownCounterOption
metric.Float64ObservableGaugeOption
metric.AddOption
metric.RecordOption
metric.ObserveOption
}
func (testExperimentalOption) Experimental() {}
func TestExperimentalOptionSafe(t *testing.T) {
var opt testExperimentalOption
assert.NotPanics(t, func() { _ = metric.NewMeterConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewInt64CounterConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewInt64UpDownCounterConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewInt64HistogramConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewInt64GaugeConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewFloat64CounterConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewFloat64UpDownCounterConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewFloat64HistogramConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewFloat64GaugeConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewInt64ObservableCounterConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewInt64ObservableUpDownCounterConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewInt64ObservableGaugeConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewFloat64ObservableCounterConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewFloat64ObservableUpDownCounterConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewFloat64ObservableGaugeConfig(opt) })
assert.NotPanics(t, func() { _ = metric.NewAddConfig([]metric.AddOption{opt}) })
assert.NotPanics(t, func() { _ = metric.NewRecordConfig([]metric.RecordOption{opt}) })
assert.NotPanics(t, func() { _ = metric.NewObserveConfig([]metric.ObserveOption{opt}) })
}
+12 -1
View File
@@ -3,7 +3,9 @@
package metric // import "go.opentelemetry.io/otel/metric"
import "go.opentelemetry.io/otel/attribute"
import (
"go.opentelemetry.io/otel/attribute"
)
// Observable is used as a grouping mechanism for all instruments that are
// updated within a Callback.
@@ -228,6 +230,9 @@ type AddConfig struct {
func NewAddConfig(opts []AddOption) AddConfig {
config := AddConfig{attrs: *attribute.EmptySet()}
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyAdd(config)
}
return config
@@ -253,6 +258,9 @@ type RecordConfig struct {
func NewRecordConfig(opts []RecordOption) RecordConfig {
config := RecordConfig{attrs: *attribute.EmptySet()}
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyRecord(config)
}
return config
@@ -278,6 +286,9 @@ type ObserveConfig struct {
func NewObserveConfig(opts []ObserveOption) ObserveConfig {
config := ObserveConfig{attrs: *attribute.EmptySet()}
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyObserve(config)
}
return config
+12
View File
@@ -51,6 +51,9 @@ type Float64CounterConfig struct {
func NewFloat64CounterConfig(opts ...Float64CounterOption) Float64CounterConfig {
var config Float64CounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64Counter(config)
}
return config
@@ -116,6 +119,9 @@ type Float64UpDownCounterConfig struct {
func NewFloat64UpDownCounterConfig(opts ...Float64UpDownCounterOption) Float64UpDownCounterConfig {
var config Float64UpDownCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64UpDownCounter(config)
}
return config
@@ -182,6 +188,9 @@ type Float64HistogramConfig struct {
func NewFloat64HistogramConfig(opts ...Float64HistogramOption) Float64HistogramConfig {
var config Float64HistogramConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64Histogram(config)
}
return config
@@ -251,6 +260,9 @@ type Float64GaugeConfig struct {
func NewFloat64GaugeConfig(opts ...Float64GaugeOption) Float64GaugeConfig {
var config Float64GaugeConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64Gauge(config)
}
return config
+12
View File
@@ -51,6 +51,9 @@ type Int64CounterConfig struct {
func NewInt64CounterConfig(opts ...Int64CounterOption) Int64CounterConfig {
var config Int64CounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64Counter(config)
}
return config
@@ -116,6 +119,9 @@ type Int64UpDownCounterConfig struct {
func NewInt64UpDownCounterConfig(opts ...Int64UpDownCounterOption) Int64UpDownCounterConfig {
var config Int64UpDownCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64UpDownCounter(config)
}
return config
@@ -182,6 +188,9 @@ type Int64HistogramConfig struct {
func NewInt64HistogramConfig(opts ...Int64HistogramOption) Int64HistogramConfig {
var config Int64HistogramConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64Histogram(config)
}
return config
@@ -251,6 +260,9 @@ type Int64GaugeConfig struct {
func NewInt64GaugeConfig(opts ...Int64GaugeOption) Int64GaugeConfig {
var config Int64GaugeConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64Gauge(config)
}
return config
+7
View File
@@ -34,9 +34,16 @@ type providerConfig struct {
allowDupKeys setting[bool]
}
type experimentalOption interface {
Experimental()
}
func newProviderConfig(opts []LoggerProviderOption) providerConfig {
var c providerConfig
for _, opt := range opts {
if _, ok := opt.(experimentalOption); ok {
continue
}
c = opt.apply(c)
}
+12
View File
@@ -419,3 +419,15 @@ func BenchmarkLoggerProviderLogger(b *testing.B) {
b.StopTimer()
loggers[0].Enabled(b.Context(), log.EnabledParameters{})
}
type testExperimentalOption struct {
LoggerProviderOption
}
func (testExperimentalOption) Experimental() {}
func TestExperimentalOptionSafe(t *testing.T) {
var opt testExperimentalOption
assert.NotPanics(t, func() { _ = NewLoggerProvider(opt) })
}
+7
View File
@@ -70,6 +70,10 @@ func unifyShutdown(funcs []func(context.Context) error) func(context.Context) er
}
}
type experimentalOption interface {
Experimental()
}
// newConfig returns a config configured with options.
func newConfig(options []Option) config {
conf := config{
@@ -81,6 +85,9 @@ func newConfig(options []Option) config {
conf = o.apply(conf)
}
for _, o := range options {
if _, ok := o.(experimentalOption); ok {
continue
}
conf = o.apply(conf)
}
return conf
+12
View File
@@ -369,3 +369,15 @@ func sample(parent context.Context) context.Context {
})
return trace.ContextWithSpanContext(parent, sc)
}
type testExperimentalOption struct {
Option
}
func (testExperimentalOption) Experimental() {}
func TestExperimentalOptionSafe(t *testing.T) {
var opt testExperimentalOption
assert.NotPanics(t, func() { _ = newConfig([]Option{opt}) })
}
+7
View File
@@ -82,6 +82,10 @@ type TracerProvider struct {
var _ trace.TracerProvider = &TracerProvider{}
type experimentalOption interface {
Experimental()
}
// NewTracerProvider returns a new and configured TracerProvider.
//
// By default the returned TracerProvider is configured with:
@@ -99,6 +103,9 @@ func NewTracerProvider(opts ...TracerProviderOption) *TracerProvider {
o = applyTracerProviderEnvConfigs(o)
for _, opt := range opts {
if _, ok := opt.(experimentalOption); ok {
continue
}
o = opt.apply(o)
}
+12
View File
@@ -518,3 +518,15 @@ func (m *errMeter) Int64Counter(string, ...metric.Int64CounterOption) (metric.In
func (m *errMeter) Int64UpDownCounter(string, ...metric.Int64UpDownCounterOption) (metric.Int64UpDownCounter, error) {
return nil, m.err
}
type testExperimentalOption struct {
TracerProviderOption
}
func (testExperimentalOption) Experimental() {}
func TestExperimentalOptionSafe(t *testing.T) {
var opt testExperimentalOption
assert.NotPanics(t, func() { _ = NewTracerProvider(opt) })
}
+16
View File
@@ -34,10 +34,17 @@ func (t *TracerConfig) SchemaURL() string {
return t.schemaURL
}
type experimentalOption interface {
Experimental()
}
// NewTracerConfig applies all the options to a returned TracerConfig.
func NewTracerConfig(options ...TracerOption) TracerConfig {
var config TracerConfig
for _, option := range options {
if _, ok := option.(experimentalOption); ok {
continue
}
config = option.apply(config)
}
return config
@@ -103,6 +110,9 @@ func (cfg *SpanConfig) SpanKind() SpanKind {
func NewSpanStartConfig(options ...SpanStartOption) SpanConfig {
var c SpanConfig
for _, option := range options {
if _, ok := option.(experimentalOption); ok {
continue
}
c = option.applySpanStart(c)
}
return c
@@ -115,6 +125,9 @@ func NewSpanStartConfig(options ...SpanStartOption) SpanConfig {
func NewSpanEndConfig(options ...SpanEndOption) SpanConfig {
var c SpanConfig
for _, option := range options {
if _, ok := option.(experimentalOption); ok {
continue
}
c = option.applySpanEnd(c)
}
return c
@@ -167,6 +180,9 @@ func (cfg *EventConfig) StackTrace() bool {
func NewEventConfig(options ...EventOption) EventConfig {
var c EventConfig
for _, option := range options {
if _, ok := option.(experimentalOption); ok {
continue
}
c = option.applyEvent(c)
}
if c.timestamp.IsZero() {
+18
View File
@@ -544,3 +544,21 @@ func TestWithInstrumentationAttributesMerge(t *testing.T) {
"Attributes and attribute sets should be merged together.")
})
}
type testExperimentalOption struct {
TracerOption
SpanStartOption
SpanEndOption
EventOption
}
func (testExperimentalOption) Experimental() {}
func TestExperimentalOptionSafe(t *testing.T) {
var opt testExperimentalOption
assert.NotPanics(t, func() { _ = NewTracerConfig(opt) })
assert.NotPanics(t, func() { _ = NewSpanStartConfig(opt) })
assert.NotPanics(t, func() { _ = NewSpanEndConfig(opt) })
assert.NotPanics(t, func() { _ = NewEventConfig(opt) })
}