Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
48e5871
Implement per instrument cardinality limits
petern48 Jan 31, 2026
1a99dbb
gofmt
petern48 Jan 31, 2026
e9ee1e2
Remove original cardinalityLimit field outside of the struct
petern48 Jan 31, 2026
e2132b9
clean up and add comments
petern48 Jan 31, 2026
6e14a8c
Add changelog
petern48 Jan 31, 2026
3de1a84
gofmt
petern48 Jan 31, 2026
857afca
Drop 'CardinalityLimit' from each field name to make the name shorter
petern48 Jan 31, 2026
38505bc
Rearrange order
petern48 Jan 31, 2026
56c399f
Collapse to multiple lines to make linter happy
petern48 Jan 31, 2026
e760138
Revert most changes
petern48 Feb 10, 2026
2441176
Implment fixes for the metric reader instead of provider
petern48 Feb 16, 2026
3ffbbf1
Change defaultCardinality limit to 2000 instead of 0, in accordance w…
petern48 Feb 16, 2026
c107413
Fix condition for getCardinalityLimit to be '>'
petern48 Feb 16, 2026
199d82e
gofmt
petern48 Feb 16, 2026
05876f2
Update changelog
petern48 Feb 16, 2026
722b87a
Merge branch 'main' into per-instrument-cardinality-limits
petern48 Feb 16, 2026
3abb16b
Move changelog entry back to new unreleased section
petern48 Feb 16, 2026
a556d92
Fix golangci-lint
petern48 Feb 16, 2026
b3c3f1c
Fix lint
petern48 Feb 16, 2026
18d7e96
Revert defaultCardinalityLimit back to 0, and update changelog
petern48 Feb 19, 2026
b71a446
Add godoc link from WithCardinalityLimit to WithKindCardinalityLimit
petern48 Feb 19, 2026
ed78ed0
Allow user to input a CardinalityLimitSelector (func) instead of indi…
petern48 Feb 19, 2026
ea5169a
Update name of api in changelog
petern48 Feb 19, 2026
668c3dc
Add extra note about behavior of returning 0 in the WithCardinalityLi…
petern48 Feb 19, 2026
2c06d2a
Support 'fallback' return value in order to distinguish unlimited fro…
petern48 Mar 11, 2026
d3216fc
Add test with a instrument kind set to unlimited without falling back…
petern48 Mar 11, 2026
090d9b1
Merge branch 'main' into per-instrument-cardinality-limits
petern48 Mar 11, 2026
6ef906a
Move changelog to new unreleased section
petern48 Mar 11, 2026
6dc505e
Update sdk/metric/pipeline.go
petern48 Mar 11, 2026
4b819e7
Use table-based testing pattern instead
petern48 Mar 11, 2026
654119a
Condense the table tests to improve readability
petern48 Mar 11, 2026
2f5ea7a
Make 'defaultCardinalityLimitSelector()' private instead of public
petern48 Mar 11, 2026
3e84fe2
Update sdk/metric/config_test.go
petern48 Mar 12, 2026
39131c1
fix after feedback
petern48 Mar 12, 2026
68c821f
feedback: Refactor tests to use func(Meter) and only use single-metri…
petern48 Mar 12, 2026
c5471d9
Merge branch 'main' into per-instrument-cardinality-limits + fix merg…
petern48 Mar 12, 2026
1c80474
Merge branch 'main' into per-instrument-cardinality-limits + fix merg…
petern48 Mar 18, 2026
313de28
Merge branch 'main' into per-instrument-cardinality-limits
dmathieu Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest`. (#8038)
- Add support for per-series start time tracking for cumulative metrics in `go.opentelemetry.io/otel/sdk/metric`.
Set `OTEL_GO_X_PER_SERIES_START_TIMESTAMPS=true` to enable. (#8060)
- Add `WithCardinalityLimitSelector` for metric reader for configuring cardinality limits specific to the instrument kind. (#7855)

### Changed

Expand Down
4 changes: 3 additions & 1 deletion sdk/metric/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,14 @@ func WithExemplarFilter(filter exemplar.Filter) Option {
})
}

// WithCardinalityLimit sets the cardinality limit for the MeterProvider.
// WithCardinalityLimit sets the global cardinality limit for the MeterProvider.
//
// 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.
// 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 {
// For backward compatibility, the environment variable `OTEL_GO_X_CARDINALITY_LIMIT`
// can also be used to set this value.
Expand Down
22 changes: 15 additions & 7 deletions sdk/metric/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ import (
)

type reader struct {
producer sdkProducer
externalProducers []Producer
temporalityFunc TemporalitySelector
aggregationFunc AggregationSelector
collectFunc func(context.Context, *metricdata.ResourceMetrics) error
forceFlushFunc func(context.Context) error
shutdownFunc func(context.Context) error
producer sdkProducer
externalProducers []Producer
temporalityFunc TemporalitySelector
aggregationFunc AggregationSelector
cardinalityLimitSelector CardinalityLimitSelector
collectFunc func(context.Context, *metricdata.ResourceMetrics) error
forceFlushFunc func(context.Context) error
shutdownFunc func(context.Context) error
}

const envVarResourceAttributes = "OTEL_RESOURCE_ATTRIBUTES"
Expand All @@ -45,6 +46,13 @@ func (r *reader) temporality(kind InstrumentKind) metricdata.Temporality {
return r.temporalityFunc(kind)
}

func (r *reader) cardinalityLimit(kind InstrumentKind) (int, bool) {
Comment thread
dashpole marked this conversation as resolved.
if r.cardinalityLimitSelector != nil {
return r.cardinalityLimitSelector(kind)
}
return 0, true
}

func (r *reader) Collect(ctx context.Context, rm *metricdata.ResourceMetrics) error {
return r.collectFunc(ctx, rm)
}
Expand Down
27 changes: 18 additions & 9 deletions sdk/metric/manual_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ type ManualReader struct {
isShutdown bool
externalProducers atomic.Value

temporalitySelector TemporalitySelector
aggregationSelector AggregationSelector
temporalitySelector TemporalitySelector
aggregationSelector AggregationSelector
cardinalityLimitSelector CardinalityLimitSelector

inst *observ.Instrumentation
}
Expand All @@ -45,8 +46,9 @@ var _ = map[Reader]struct{}{&ManualReader{}: {}}
func NewManualReader(opts ...ManualReaderOption) *ManualReader {
cfg := newManualReaderConfig(opts)
r := &ManualReader{
temporalitySelector: cfg.temporalitySelector,
aggregationSelector: cfg.aggregationSelector,
temporalitySelector: cfg.temporalitySelector,
aggregationSelector: cfg.aggregationSelector,
cardinalityLimitSelector: cfg.cardinalityLimitSelector,
}
r.externalProducers.Store(cfg.producers)

Expand Down Expand Up @@ -89,6 +91,11 @@ func (mr *ManualReader) aggregation(
return mr.aggregationSelector(kind)
}

// cardinalityLimit returns the cardinality limit for kind.
func (mr *ManualReader) cardinalityLimit(kind InstrumentKind) (int, bool) {
Comment thread
dashpole marked this conversation as resolved.
return mr.cardinalityLimitSelector(kind)
}

// Shutdown closes any connections and frees any resources used by the reader.
//
// This method is safe to call concurrently.
Expand Down Expand Up @@ -179,16 +186,18 @@ func (r *ManualReader) MarshalLog() any {

// manualReaderConfig contains configuration options for a ManualReader.
type manualReaderConfig struct {
temporalitySelector TemporalitySelector
aggregationSelector AggregationSelector
producers []Producer
temporalitySelector TemporalitySelector
aggregationSelector AggregationSelector
cardinalityLimitSelector CardinalityLimitSelector
producers []Producer
}

// newManualReaderConfig returns a manualReaderConfig configured with options.
func newManualReaderConfig(opts []ManualReaderOption) manualReaderConfig {
cfg := manualReaderConfig{
temporalitySelector: DefaultTemporalitySelector,
aggregationSelector: DefaultAggregationSelector,
temporalitySelector: DefaultTemporalitySelector,
aggregationSelector: DefaultAggregationSelector,
cardinalityLimitSelector: defaultCardinalityLimitSelector,
}
for _, opt := range opts {
cfg = opt.applyManual(cfg)
Expand Down
32 changes: 21 additions & 11 deletions sdk/metric/periodic_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,19 @@ const (

// periodicReaderConfig contains configuration options for a PeriodicReader.
type periodicReaderConfig struct {
interval time.Duration
timeout time.Duration
producers []Producer
interval time.Duration
timeout time.Duration
producers []Producer
cardinalityLimitSelector CardinalityLimitSelector
}

// newPeriodicReaderConfig returns a periodicReaderConfig configured with
// options.
func newPeriodicReaderConfig(options []PeriodicReaderOption) periodicReaderConfig {
c := periodicReaderConfig{
interval: envDuration(envInterval, defaultInterval),
timeout: envDuration(envTimeout, defaultTimeout),
interval: envDuration(envInterval, defaultInterval),
timeout: envDuration(envTimeout, defaultTimeout),
cardinalityLimitSelector: defaultCardinalityLimitSelector,
}
for _, o := range options {
c = o.applyPeriodic(c)
Expand Down Expand Up @@ -111,12 +113,13 @@ func NewPeriodicReader(exporter Exporter, options ...PeriodicReaderOption) *Peri
context.Background(),
)
r := &PeriodicReader{
interval: conf.interval,
timeout: conf.timeout,
exporter: exporter,
flushCh: make(chan chan error),
cancel: cancel,
done: make(chan struct{}),
interval: conf.interval,
timeout: conf.timeout,
exporter: exporter,
flushCh: make(chan chan error),
cancel: cancel,
done: make(chan struct{}),
cardinalityLimitSelector: conf.cardinalityLimitSelector,
rmPool: sync.Pool{
New: func() any {
return &metricdata.ResourceMetrics{}
Expand Down Expand Up @@ -170,6 +173,8 @@ type PeriodicReader struct {

rmPool sync.Pool

cardinalityLimitSelector CardinalityLimitSelector

inst *observ.Instrumentation
}

Expand Down Expand Up @@ -222,6 +227,11 @@ func (r *PeriodicReader) aggregation(
return r.exporter.Aggregation(kind)
}

// cardinalityLimit returns the cardinality limit for kind.
func (r *PeriodicReader) cardinalityLimit(kind InstrumentKind) (int, bool) {
Comment thread
dashpole marked this conversation as resolved.
return r.cardinalityLimitSelector(kind)
}

// collectAndExport gather all metric data related to the periodicReader r from
// the SDK and exports it with r's exporter.
func (r *PeriodicReader) collectAndExport(ctx context.Context) error {
Expand Down
16 changes: 13 additions & 3 deletions sdk/metric/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,7 @@ func (i *inserter[N]) cachedAggregator(
b.Filter = stream.AttributeFilter
// A value less than or equal to zero will disable the aggregation
// limits for the builder (an all the created aggregates).
// cardinalityLimit will be 0 by default if unset (or
// unrecognized input). Use that value directly.
b.AggregationLimit = i.pipeline.cardinalityLimit
b.AggregationLimit = i.getCardinalityLimit(kind)
in, out, err := i.aggregateFunc(b, stream.Aggregation, kind)
if err != nil {
return aggVal[N]{0, nil, err}
Expand All @@ -419,6 +417,18 @@ func (i *inserter[N]) cachedAggregator(
return cv.Measure, cv.ID, cv.Err
}

// getCardinalityLimit returns the cardinality limit for the given instrument kind.
// When the reader's selector returns fallback = true, the pipeline's global
// limit is used, then the default if global is unset. When fallback is false,
// the selector's limit is used (0 or less means unlimited).
func (i *inserter[N]) getCardinalityLimit(kind InstrumentKind) int {
limit, fallback := i.pipeline.reader.cardinalityLimit(kind)
if fallback {
return i.pipeline.cardinalityLimit
}
return limit
}

// logConflict validates if an instrument with the same case-insensitive name
// as id has already been created. If that instrument conflicts with id, a
// warning is logged.
Expand Down
Loading
Loading