Skip to content

Commit 901d028

Browse files
committed
[Metrics SDK] Implement configurable cardinality limit
Fixes #3292 This PR implements configurable aggregation cardinality limits at three priority levels as specified by the OpenTelemetry specification: 1. View-level: cardinality limit defined in a View's AggregationConfig 2. MetricReader-level: default limits per instrument type in the reader 3. SDK default: fallback to 2000 if neither above is defined Changes: - Add CardinalityLimitOptions to MetricReader with per-instrument-type limits - Implement GetCardinalityLimit() in MetricReader and MetricCollector - Add GetReaderCardinalityLimit() to MeterContext (returns max across readers) - Update storage constructors to accept owned AggregationConfig for lifetime management - Wire view-level cardinality from YAML config in SdkBuilder - Wire reader-level cardinality from YAML config in SdkBuilder - Apply reader-level fallback in Meter when view config is absent The implementation follows the spec priority order: View config > Reader config > SDK default (2000). Files modified: - sdk/include/opentelemetry/sdk/metrics/metric_reader.h - sdk/src/metrics/metric_reader.cc - sdk/include/opentelemetry/sdk/metrics/state/metric_collector.h - sdk/src/metrics/state/metric_collector.cc - sdk/include/opentelemetry/sdk/metrics/meter_context.h - sdk/src/metrics/meter_context.cc - sdk/include/opentelemetry/sdk/metrics/state/sync_metric_storage.h - sdk/include/opentelemetry/sdk/metrics/state/async_metric_storage.h - sdk/src/metrics/meter.cc - sdk/src/configuration/sdk_builder.cc
1 parent 82a2ada commit 901d028

10 files changed

Lines changed: 205 additions & 11 deletions

File tree

sdk/include/opentelemetry/sdk/metrics/meter_context.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ class MeterContext : public std::enable_shared_from_this<MeterContext>
108108
*/
109109
nostd::span<std::shared_ptr<CollectorHandle>> GetCollectors() noexcept;
110110

111+
/**
112+
* Get the cardinality limit for a given instrument type from configured readers.
113+
* Returns the maximum limit across all readers, or 0 if no reader specifies a limit.
114+
*
115+
* @param instrument_type The instrument type to get the limit for
116+
* @return The maximum cardinality limit, or 0 if not configured
117+
*/
118+
size_t GetReaderCardinalityLimit(InstrumentType instrument_type) noexcept;
119+
111120
/**
112121
* GET SDK Start time
113122
*

sdk/include/opentelemetry/sdk/metrics/metric_reader.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@ namespace sdk
1616
{
1717
namespace metrics
1818
{
19+
/**
20+
* Configuration options for cardinality limits per instrument type.
21+
* A value of 0 means use the SDK default (2000).
22+
*/
23+
struct CardinalityLimitOptions
24+
{
25+
size_t default_limit = 0; // 0 means use SDK default (2000)
26+
size_t counter = 0;
27+
size_t gauge = 0;
28+
size_t histogram = 0;
29+
size_t observable_counter = 0;
30+
size_t observable_gauge = 0;
31+
size_t observable_up_down_counter = 0;
32+
size_t up_down_counter = 0;
33+
};
34+
1935
/**
2036
* MetricReader defines the interface to collect metrics from SDK
2137
*/
@@ -45,6 +61,22 @@ class MetricReader
4561
virtual AggregationTemporality GetAggregationTemporality(
4662
InstrumentType instrument_type) const noexcept = 0;
4763

64+
/**
65+
* Get the cardinality limit for a given instrument type.
66+
* Returns 0 if no limit is configured for this instrument type.
67+
*
68+
* @param instrument_type The instrument type to get the limit for
69+
* @return The cardinality limit, or 0 if not configured
70+
*/
71+
size_t GetCardinalityLimit(InstrumentType instrument_type) const noexcept;
72+
73+
/**
74+
* Set cardinality limit options for this reader.
75+
*
76+
* @param options The cardinality limit options
77+
*/
78+
void SetCardinalityLimitOptions(const CardinalityLimitOptions &options) noexcept;
79+
4880
/**
4981
* Shutdown the metric reader.
5082
*/
@@ -73,6 +105,7 @@ class MetricReader
73105
private:
74106
MetricProducer *metric_producer_{nullptr};
75107
std::atomic<bool> shutdown_{false};
108+
CardinalityLimitOptions cardinality_limit_options_;
76109
};
77110
} // namespace metrics
78111
} // namespace sdk

sdk/include/opentelemetry/sdk/metrics/state/async_metric_storage.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ class AsyncMetricStorage : public MetricStorage, public AsyncWritableMetricStora
4040
ExemplarFilterType exempler_filter_type,
4141
nostd::shared_ptr<ExemplarReservoir> &&exemplar_reservoir,
4242
#endif
43-
const AggregationConfig *aggregation_config)
44-
: instrument_descriptor_(instrument_descriptor),
43+
const AggregationConfig *aggregation_config,
44+
std::shared_ptr<AggregationConfig> owned_aggregation_config = nullptr)
45+
: owned_aggregation_config_(std::move(owned_aggregation_config)),
46+
instrument_descriptor_(instrument_descriptor),
4547
aggregation_type_{aggregation_type},
4648
aggregation_config_{AggregationConfig::GetOrDefault(aggregation_config)},
4749
cumulative_hash_map_(
@@ -139,6 +141,8 @@ class AsyncMetricStorage : public MetricStorage, public AsyncWritableMetricStora
139141
}
140142

141143
private:
144+
// Keep owned config alive if created from reader-level defaults
145+
std::shared_ptr<AggregationConfig> owned_aggregation_config_;
142146
InstrumentDescriptor instrument_descriptor_;
143147
AggregationType aggregation_type_;
144148
const AggregationConfig *aggregation_config_;

sdk/include/opentelemetry/sdk/metrics/state/metric_collector.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ class CollectorHandle
3636

3737
virtual AggregationTemporality GetAggregationTemporality(
3838
InstrumentType instrument_type) noexcept = 0;
39+
40+
/**
41+
* Get the cardinality limit for a given instrument type.
42+
* Returns 0 if no limit is configured.
43+
*
44+
* @param instrument_type The instrument type to get the limit for
45+
* @return The cardinality limit, or 0 if not configured
46+
*/
47+
virtual size_t GetCardinalityLimit(InstrumentType /* instrument_type */) noexcept
48+
{
49+
return 0;
50+
}
3951
};
4052

4153
/**
@@ -61,6 +73,8 @@ class MetricCollector : public MetricProducer, public CollectorHandle
6173
AggregationTemporality GetAggregationTemporality(
6274
InstrumentType instrument_type) noexcept override;
6375

76+
size_t GetCardinalityLimit(InstrumentType instrument_type) noexcept override;
77+
6478
/**
6579
* The callback to be called for each metric exporter. This will only be those
6680
* metrics that have been produced since the last time this method was called.

sdk/include/opentelemetry/sdk/metrics/state/sync_metric_storage.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ class SyncMetricStorage : public MetricStorage, public SyncWritableMetricStorage
6969
ExemplarFilterType exempler_filter_type,
7070
nostd::shared_ptr<ExemplarReservoir> &&exemplar_reservoir,
7171
#endif
72-
const AggregationConfig *aggregation_config)
73-
: instrument_descriptor_(instrument_descriptor),
72+
const AggregationConfig *aggregation_config,
73+
std::shared_ptr<AggregationConfig> owned_aggregation_config = nullptr)
74+
: owned_aggregation_config_(std::move(owned_aggregation_config)),
75+
instrument_descriptor_(instrument_descriptor),
7476
aggregation_config_(AggregationConfig::GetOrDefault(aggregation_config)),
7577
attributes_hashmap_(
7678
std::make_unique<AttributesHashMap>(aggregation_config_->cardinality_limit_)),
@@ -291,6 +293,8 @@ class SyncMetricStorage : public MetricStorage, public SyncWritableMetricStorage
291293
}
292294
#endif
293295

296+
// Keep owned config alive if created from reader-level defaults
297+
std::shared_ptr<AggregationConfig> owned_aggregation_config_;
294298
InstrumentDescriptor instrument_descriptor_;
295299
// hashmap to maintain the metrics for delta collection (i.e, collection since last Collect call)
296300
const AggregationConfig *aggregation_config_;

sdk/src/configuration/sdk_builder.cc

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,14 +1471,23 @@ std::unique_ptr<opentelemetry::sdk::metrics::MetricReader> SdkBuilder::CreatePer
14711471
OTEL_INTERNAL_LOG_WARN("metric producer not supported, ignoring");
14721472
}
14731473

1474+
sdk = opentelemetry::sdk::metrics::PeriodicExportingMetricReaderFactory::Create(
1475+
std::move(exporter_sdk), options);
1476+
14741477
if (model->cardinality_limits != nullptr)
14751478
{
1476-
OTEL_INTERNAL_LOG_WARN("cardinality limits not supported, ignoring");
1479+
opentelemetry::sdk::metrics::CardinalityLimitOptions cardinality_options;
1480+
cardinality_options.default_limit = model->cardinality_limits->default_limit;
1481+
cardinality_options.counter = model->cardinality_limits->counter;
1482+
cardinality_options.gauge = model->cardinality_limits->gauge;
1483+
cardinality_options.histogram = model->cardinality_limits->histogram;
1484+
cardinality_options.observable_counter = model->cardinality_limits->observable_counter;
1485+
cardinality_options.observable_gauge = model->cardinality_limits->observable_gauge;
1486+
cardinality_options.observable_up_down_counter = model->cardinality_limits->observable_up_down_counter;
1487+
cardinality_options.up_down_counter = model->cardinality_limits->up_down_counter;
1488+
sdk->SetCardinalityLimitOptions(cardinality_options);
14771489
}
14781490

1479-
sdk = opentelemetry::sdk::metrics::PeriodicExportingMetricReaderFactory::Create(
1480-
std::move(exporter_sdk), options);
1481-
14821491
return sdk;
14831492
}
14841493

@@ -1496,7 +1505,16 @@ std::unique_ptr<opentelemetry::sdk::metrics::MetricReader> SdkBuilder::CreatePul
14961505

14971506
if (model->cardinality_limits != nullptr)
14981507
{
1499-
OTEL_INTERNAL_LOG_WARN("cardinality limits not supported, ignoring");
1508+
opentelemetry::sdk::metrics::CardinalityLimitOptions cardinality_options;
1509+
cardinality_options.default_limit = model->cardinality_limits->default_limit;
1510+
cardinality_options.counter = model->cardinality_limits->counter;
1511+
cardinality_options.gauge = model->cardinality_limits->gauge;
1512+
cardinality_options.histogram = model->cardinality_limits->histogram;
1513+
cardinality_options.observable_counter = model->cardinality_limits->observable_counter;
1514+
cardinality_options.observable_gauge = model->cardinality_limits->observable_gauge;
1515+
cardinality_options.observable_up_down_counter = model->cardinality_limits->observable_up_down_counter;
1516+
cardinality_options.up_down_counter = model->cardinality_limits->up_down_counter;
1517+
sdk->SetCardinalityLimitOptions(cardinality_options);
15001518
}
15011519

15021520
return sdk;
@@ -1600,6 +1618,20 @@ void SdkBuilder::AddView(
16001618

16011619
sdk_aggregation_config = CreateAggregationConfig(stream->aggregation, sdk_aggregation_type);
16021620

1621+
// Apply aggregation_cardinality_limit from the view stream configuration
1622+
if (stream->aggregation_cardinality_limit != 0)
1623+
{
1624+
if (sdk_aggregation_config)
1625+
{
1626+
sdk_aggregation_config->cardinality_limit_ = stream->aggregation_cardinality_limit;
1627+
}
1628+
else
1629+
{
1630+
sdk_aggregation_config = std::make_shared<opentelemetry::sdk::metrics::AggregationConfig>(
1631+
stream->aggregation_cardinality_limit);
1632+
}
1633+
}
1634+
16031635
std::unique_ptr<opentelemetry::sdk::metrics::AttributesProcessor> sdk_attribute_processor;
16041636

16051637
if (stream->attribute_keys != nullptr)

sdk/src/metrics/meter.cc

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,14 +542,34 @@ std::unique_ptr<SyncWritableMetricStorage> Meter::RegisterSyncMetricStorage(
542542
else
543543
{
544544
WarnOnDuplicateInstrument(GetInstrumentationScope(), storage_registry_, view_instr_desc);
545+
546+
// Determine effective aggregation config with reader-level fallback
547+
const AggregationConfig *effective_config = view.GetAggregationConfig();
548+
std::shared_ptr<AggregationConfig> reader_config;
549+
550+
// If the view does not specify a cardinality limit, use the MetricReader-level default
551+
if (!effective_config)
552+
{
553+
auto ctx_ptr = meter_context_.lock();
554+
if (ctx_ptr)
555+
{
556+
size_t reader_limit = ctx_ptr->GetReaderCardinalityLimit(instrument_descriptor.type_);
557+
if (reader_limit != 0)
558+
{
559+
reader_config = std::make_shared<AggregationConfig>(reader_limit);
560+
effective_config = reader_config.get();
561+
}
562+
}
563+
}
564+
545565
sync_storage = std::shared_ptr<SyncMetricStorage>(new SyncMetricStorage(
546566
view_instr_desc, view.GetAggregationType(), view.GetAttributesProcessor(),
547567
#ifdef ENABLE_METRICS_EXEMPLAR_PREVIEW
548568
exemplar_filter_type,
549569
GetExemplarReservoir(view.GetAggregationType(), view.GetAggregationConfig(),
550570
view_instr_desc),
551571
#endif
552-
view.GetAggregationConfig()));
572+
effective_config, std::move(reader_config)));
553573
storage_registry_.insert({view_instr_desc, sync_storage});
554574
}
555575
auto sync_multi_storage = static_cast<SyncMultiMetricStorage *>(storages.get());
@@ -615,14 +635,34 @@ std::unique_ptr<AsyncWritableMetricStorage> Meter::RegisterAsyncMetricStorage(
615635
else
616636
{
617637
WarnOnDuplicateInstrument(GetInstrumentationScope(), storage_registry_, view_instr_desc);
638+
639+
// Determine effective aggregation config with reader-level fallback
640+
const AggregationConfig *effective_config = view.GetAggregationConfig();
641+
std::shared_ptr<AggregationConfig> reader_config;
642+
643+
// If the view does not specify a cardinality limit, use the MetricReader-level default
644+
if (!effective_config)
645+
{
646+
auto ctx_ptr = meter_context_.lock();
647+
if (ctx_ptr)
648+
{
649+
size_t reader_limit = ctx_ptr->GetReaderCardinalityLimit(instrument_descriptor.type_);
650+
if (reader_limit != 0)
651+
{
652+
reader_config = std::make_shared<AggregationConfig>(reader_limit);
653+
effective_config = reader_config.get();
654+
}
655+
}
656+
}
657+
618658
async_storage = std::shared_ptr<AsyncMetricStorage>(new AsyncMetricStorage(
619659
view_instr_desc, view.GetAggregationType(),
620660
#ifdef ENABLE_METRICS_EXEMPLAR_PREVIEW
621661
exemplar_filter_type,
622662
GetExemplarReservoir(view.GetAggregationType(), view.GetAggregationConfig(),
623663
view_instr_desc),
624664
#endif
625-
view.GetAggregationConfig()));
665+
effective_config, std::move(reader_config)));
626666
storage_registry_.insert({view_instr_desc, async_storage});
627667
}
628668
auto async_multi_storage = static_cast<AsyncMultiMetricStorage *>(storages.get());

sdk/src/metrics/meter_context.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,20 @@ nostd::span<std::shared_ptr<CollectorHandle>> MeterContext::GetCollectors() noex
9191
return nostd::span<std::shared_ptr<CollectorHandle>>(collectors_.data(), collectors_.size());
9292
}
9393

94+
size_t MeterContext::GetReaderCardinalityLimit(InstrumentType instrument_type) noexcept
95+
{
96+
size_t max_limit = 0;
97+
for (const auto &collector : collectors_)
98+
{
99+
size_t limit = collector->GetCardinalityLimit(instrument_type);
100+
if (limit > max_limit)
101+
{
102+
max_limit = limit;
103+
}
104+
}
105+
return max_limit;
106+
}
107+
94108
opentelemetry::common::SystemTimestamp MeterContext::GetSDKStartTime() noexcept
95109
{
96110
return sdk_start_ts_;

sdk/src/metrics/metric_reader.cc

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,45 @@ bool MetricReader::IsShutdown() const noexcept
8484
return shutdown_.load(std::memory_order_acquire);
8585
}
8686

87+
size_t MetricReader::GetCardinalityLimit(InstrumentType instrument_type) const noexcept
88+
{
89+
switch (instrument_type)
90+
{
91+
case InstrumentType::kCounter:
92+
return cardinality_limit_options_.counter != 0 ? cardinality_limit_options_.counter
93+
: cardinality_limit_options_.default_limit;
94+
case InstrumentType::kHistogram:
95+
return cardinality_limit_options_.histogram != 0 ? cardinality_limit_options_.histogram
96+
: cardinality_limit_options_.default_limit;
97+
case InstrumentType::kUpDownCounter:
98+
return cardinality_limit_options_.up_down_counter != 0
99+
? cardinality_limit_options_.up_down_counter
100+
: cardinality_limit_options_.default_limit;
101+
case InstrumentType::kObservableCounter:
102+
return cardinality_limit_options_.observable_counter != 0
103+
? cardinality_limit_options_.observable_counter
104+
: cardinality_limit_options_.default_limit;
105+
case InstrumentType::kObservableGauge:
106+
return cardinality_limit_options_.observable_gauge != 0
107+
? cardinality_limit_options_.observable_gauge
108+
: cardinality_limit_options_.default_limit;
109+
case InstrumentType::kObservableUpDownCounter:
110+
return cardinality_limit_options_.observable_up_down_counter != 0
111+
? cardinality_limit_options_.observable_up_down_counter
112+
: cardinality_limit_options_.default_limit;
113+
case InstrumentType::kGauge:
114+
return cardinality_limit_options_.gauge != 0 ? cardinality_limit_options_.gauge
115+
: cardinality_limit_options_.default_limit;
116+
default:
117+
return cardinality_limit_options_.default_limit;
118+
}
119+
}
120+
121+
void MetricReader::SetCardinalityLimitOptions(const CardinalityLimitOptions &options) noexcept
122+
{
123+
cardinality_limit_options_ = options;
124+
}
125+
87126
} // namespace metrics
88127
} // namespace sdk
89128
OPENTELEMETRY_END_NAMESPACE

sdk/src/metrics/state/metric_collector.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ AggregationTemporality MetricCollector::GetAggregationTemporality(
4545
return aggregation_temporality;
4646
}
4747

48+
size_t MetricCollector::GetCardinalityLimit(InstrumentType instrument_type) noexcept
49+
{
50+
return metric_reader_->GetCardinalityLimit(instrument_type);
51+
}
52+
4853
MetricProducer::Result MetricCollector::Produce() noexcept
4954
{
5055
if (!meter_context_)

0 commit comments

Comments
 (0)