From 5f80184b579ffff0c870d4795047bb96de6ee00d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 22 Apr 2026 14:40:51 +0200 Subject: [PATCH] sdk/metric: apply default cardinality limit of 2000 (#8247) Per https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#cardinality-limits: > If none of the previous values are defined, the default value of 2000 SHOULD be used. --- CHANGELOG.md | 7 ++++- sdk/metric/config.go | 7 +++-- sdk/metric/config_test.go | 6 +++++ sdk/metric/doc.go | 8 +++--- sdk/metric/example_test.go | 1 - sdk/metric/provider_test.go | 54 ++++++++++++++++++++++++------------- 6 files changed, 56 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2c4ca197..f6b5d1398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed +- ⚠️ **Breaking Change:** `go.opentelemetry.io/otel/sdk/metric` now applies a default cardinality limit of 2000 to comply with the Metrics SDK specification recommendation. + New attribute sets are dropped when the cardinality limit is reached. The measurement of these sets are aggregated into a special attribute set containing `attribute.Bool("otel.metric.overflow", true)`. + This can break users who relied on the previous unlimited default. + Set `WithCardinalityLimit(0)` or the deprecated `OTEL_GO_X_CARDINALITY_LIMIT=0` environment variable to preserve unlimited cardinality. + Note that support for `OTEL_GO_X_CARDINALITY_LIMIT` may be removed in a future release. (#8247) - `ErrorType` in `go.opentelemetry.io/otel/semconv` now unwraps errors created with `fmt.Errorf` when deriving the `error.type` attribute. (#8133) - `go.opentelemetry.io/otel/sdk/log` now unwraps error chains created with `fmt.Errorf` when deriving the `error.type` attribute from errors on log records. (#8133) - `Set.MarshalLog` method in `go.opentelemetry.io/otel/attribute` now uses `Value.String` formatting following the [OpenTelemetry AnyValue representation for non-OTLP protocols](https://opentelemetry.io/docs/specs/otel/common/#anyvalue). (#8169) @@ -41,7 +46,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Deprecated - Deprecate `Value.Emit` method in `go.opentelemetry.io/otel/attribute`. - Use `Value.String` instead. (#8176) + Use `Value.String` instead. (#8176) ### Fixed diff --git a/sdk/metric/config.go b/sdk/metric/config.go index bc74ab2e5..dda4e086d 100644 --- a/sdk/metric/config.go +++ b/sdk/metric/config.go @@ -25,7 +25,7 @@ type config struct { cardinalityLimit int } -const defaultCardinalityLimit = 0 +const defaultCardinalityLimit = 2000 // readerSignals returns a force-flush and shutdown function for a // MeterProvider to call in their respective options. All Readers c contains @@ -172,7 +172,10 @@ func WithExemplarFilter(filter exemplar.Filter) Option { // The cardinality limit is the hard limit on the number of metric datapoints // that can be collected for a single instrument in a single collect cycle. // -// Setting this to a zero or negative value means no limit is applied. +// By default, if this option is not used, a limit of +// 2000 is applied. +// +// Setting this to a zero or negative means no limit is applied. // This value applies to all instrument kinds, but can be overridden per kind by // the reader's cardinality limit selector (see [WithCardinalityLimitSelector]). func WithCardinalityLimit(limit int) Option { diff --git a/sdk/metric/config_test.go b/sdk/metric/config_test.go index 0da60d02a..64ae7dba3 100644 --- a/sdk/metric/config_test.go +++ b/sdk/metric/config_test.go @@ -338,6 +338,12 @@ func TestWithCardinalityLimit(t *testing.T) { options: []Option{}, expectedLimit: 1234, }, + { + name: "zero cardinality limit from env disables limit", + envValue: "0", + options: []Option{}, + expectedLimit: 0, + }, { name: "invalid env value uses default", envValue: "not-a-number", diff --git a/sdk/metric/doc.go b/sdk/metric/doc.go index dd75eefac..72392a2cb 100644 --- a/sdk/metric/doc.go +++ b/sdk/metric/doc.go @@ -44,9 +44,7 @@ // Cardinality refers to the number of unique attributes collected. High cardinality can lead to // excessive memory usage, increased storage costs, and backend performance issues. // -// Currently, the OpenTelemetry Go Metric SDK does not enforce a cardinality limit by default -// (note that this may change in a future release). Use [WithCardinalityLimit] to set the -// cardinality limit as desired. +// By default, the OpenTelemetry Go Metric SDK enforces a cardinality limit of 2000. // // New attribute sets are dropped when the cardinality limit is reached. The measurement of // these sets are aggregated into @@ -57,8 +55,8 @@ // // Recommendations: // -// - Set the limit based on the theoretical maximum combinations or expected -// active combinations. The OpenTelemetry Specification recommends a default of 2000. +// - Tune the limit based on the theoretical maximum combinations or expected +// active combinations. The SDK default is 2000. // - A too high of a limit increases worst-case memory overhead in the SDK and may cause downstream // issues for databases that cannot handle high cardinality. // - A too low of a limit causes loss of attribute detail as more data falls into overflow. diff --git a/sdk/metric/example_test.go b/sdk/metric/example_test.go index 2c8d9a5e5..2e74df6dc 100644 --- a/sdk/metric/example_test.go +++ b/sdk/metric/example_test.go @@ -45,7 +45,6 @@ func Example() { meterProvider := metric.NewMeterProvider( metric.WithResource(res), metric.WithReader(reader), - metric.WithCardinalityLimit(2000), ) // Handle shutdown properly so that nothing leaks. diff --git a/sdk/metric/provider_test.go b/sdk/metric/provider_test.go index 22d9dec48..7f64e1ad7 100644 --- a/sdk/metric/provider_test.go +++ b/sdk/metric/provider_test.go @@ -176,32 +176,40 @@ func TestMeterProviderMixingOnRegisterErrors(t *testing.T) { } func TestMeterProviderCardinalityLimit(t *testing.T) { - const uniqueAttributesCount = 10 - tests := []struct { - name string - options []Option - wantDataPoints int + name string + options []Option + uniqueAttributesCount int + wantDataPoints int + wantOverflowPoints int }{ { - name: "no limit (default)", - options: nil, - wantDataPoints: uniqueAttributesCount, + name: "default limit", + options: nil, + uniqueAttributesCount: defaultCardinalityLimit + 5, + wantDataPoints: defaultCardinalityLimit, + wantOverflowPoints: 1, }, { - name: "no limit (limit=0)", - options: []Option{WithCardinalityLimit(0)}, - wantDataPoints: uniqueAttributesCount, + name: "no limit (limit=0)", + options: []Option{WithCardinalityLimit(0)}, + uniqueAttributesCount: 10, + wantDataPoints: 10, + wantOverflowPoints: 0, }, { - name: "no limit (negative)", - options: []Option{WithCardinalityLimit(-5)}, - wantDataPoints: uniqueAttributesCount, + name: "no limit (negative)", + options: []Option{WithCardinalityLimit(-5)}, + uniqueAttributesCount: 10, + wantDataPoints: 10, + wantOverflowPoints: 0, }, { - name: "limit=5", - options: []Option{WithCardinalityLimit(5)}, - wantDataPoints: 5, + name: "limit=5", + options: []Option{WithCardinalityLimit(5)}, + uniqueAttributesCount: 10, + wantDataPoints: 5, + wantOverflowPoints: 1, }, } @@ -216,7 +224,7 @@ func TestMeterProviderCardinalityLimit(t *testing.T) { counter, err := meter.Int64Counter("metric") require.NoError(t, err, "failed to create counter") - for i := range uniqueAttributesCount { + for i := range tt.uniqueAttributesCount { counter.Add( t.Context(), 1, @@ -241,6 +249,16 @@ func TestMeterProviderCardinalityLimit(t *testing.T) { tt.wantDataPoints, "unexpected number of data points", ) + + overflow := attribute.NewSet(attribute.Bool("otel.metric.overflow", true)) + var overflowPoints int + for _, dp := range sumData.DataPoints { + attrs := dp.Attributes + if attrs.Equals(&overflow) { + overflowPoints++ + } + } + assert.Equal(t, tt.wantOverflowPoints, overflowPoints, "unexpected overflow data points") }) } }