diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c462112a7..88719eed4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ Increment the: ## [Unreleased] +* [METRICS SDK] Implement configurable cardinality limit at three priority levels + [#3292](https://github.com/open-telemetry/opentelemetry-cpp/issues/3292) + * [SDK] Add `TracerProvider::UpdateTracerConfigurator()` and example [#4065](https://github.com/open-telemetry/opentelemetry-cpp/issues/4065) diff --git a/sdk/include/opentelemetry/sdk/metrics/meter_context.h b/sdk/include/opentelemetry/sdk/metrics/meter_context.h index 9ccd766565..eb585cfbb4 100644 --- a/sdk/include/opentelemetry/sdk/metrics/meter_context.h +++ b/sdk/include/opentelemetry/sdk/metrics/meter_context.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -108,6 +109,15 @@ class MeterContext : public std::enable_shared_from_this */ nostd::span> GetCollectors() noexcept; + /** + * Get the cardinality limit for a given instrument type from configured readers. + * Returns the maximum limit across all readers, or 0 if no reader specifies a limit. + * + * @param instrument_type The instrument type to get the limit for + * @return The maximum cardinality limit, or 0 if not configured + */ + size_t GetReaderCardinalityLimit(InstrumentType instrument_type) noexcept; + /** * GET SDK Start time * diff --git a/sdk/include/opentelemetry/sdk/metrics/metric_reader.h b/sdk/include/opentelemetry/sdk/metrics/metric_reader.h index a9a60753c0..a17fbf8b83 100644 --- a/sdk/include/opentelemetry/sdk/metrics/metric_reader.h +++ b/sdk/include/opentelemetry/sdk/metrics/metric_reader.h @@ -5,6 +5,7 @@ #include #include +#include #include "opentelemetry/nostd/function_ref.h" #include "opentelemetry/sdk/metrics/export/metric_producer.h" @@ -16,6 +17,22 @@ namespace sdk { namespace metrics { +/** + * Configuration options for cardinality limits per instrument type. + * A value of 0 means use the SDK default (2000). + */ +struct CardinalityLimitOptions +{ + size_t default_limit = 0; // 0 means use SDK default (2000) + size_t counter = 0; + size_t gauge = 0; + size_t histogram = 0; + size_t observable_counter = 0; + size_t observable_gauge = 0; + size_t observable_up_down_counter = 0; + size_t up_down_counter = 0; +}; + /** * MetricReader defines the interface to collect metrics from SDK */ @@ -45,6 +62,22 @@ class MetricReader virtual AggregationTemporality GetAggregationTemporality( InstrumentType instrument_type) const noexcept = 0; + /** + * Get the cardinality limit for a given instrument type. + * Returns 0 if no limit is configured for this instrument type. + * + * @param instrument_type The instrument type to get the limit for + * @return The cardinality limit, or 0 if not configured + */ + size_t GetCardinalityLimit(InstrumentType instrument_type) const noexcept; + + /** + * Set cardinality limit options for this reader. + * + * @param options The cardinality limit options + */ + void SetCardinalityLimitOptions(const CardinalityLimitOptions &options) noexcept; + /** * Shutdown the metric reader. */ @@ -73,6 +106,7 @@ class MetricReader private: MetricProducer *metric_producer_{nullptr}; std::atomic shutdown_{false}; + CardinalityLimitOptions cardinality_limit_options_; }; } // namespace metrics } // namespace sdk diff --git a/sdk/include/opentelemetry/sdk/metrics/state/metric_collector.h b/sdk/include/opentelemetry/sdk/metrics/state/metric_collector.h index f01a36854b..9d6e79eb83 100644 --- a/sdk/include/opentelemetry/sdk/metrics/state/metric_collector.h +++ b/sdk/include/opentelemetry/sdk/metrics/state/metric_collector.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include "opentelemetry/nostd/function_ref.h" @@ -36,6 +37,19 @@ class CollectorHandle virtual AggregationTemporality GetAggregationTemporality( InstrumentType instrument_type) noexcept = 0; + + /** + * Get the cardinality limit for a given instrument type. + * Returns 0 if no limit is configured. + * + * @param instrument_type The instrument type to get the limit for + * @return The cardinality limit, or 0 if not configured + */ + virtual size_t GetCardinalityLimit(InstrumentType instrument_type) noexcept + { + (void)instrument_type; + return 0; + } }; /** @@ -61,6 +75,8 @@ class MetricCollector : public MetricProducer, public CollectorHandle AggregationTemporality GetAggregationTemporality( InstrumentType instrument_type) noexcept override; + size_t GetCardinalityLimit(InstrumentType instrument_type) noexcept override; + /** * The callback to be called for each metric exporter. This will only be those * metrics that have been produced since the last time this method was called. diff --git a/sdk/src/configuration/sdk_builder.cc b/sdk/src/configuration/sdk_builder.cc index f60362fa9e..5d875491a4 100644 --- a/sdk/src/configuration/sdk_builder.cc +++ b/sdk/src/configuration/sdk_builder.cc @@ -31,6 +31,7 @@ #include "opentelemetry/sdk/configuration/batch_span_processor_configuration.h" #include "opentelemetry/sdk/configuration/boolean_array_attribute_value_configuration.h" #include "opentelemetry/sdk/configuration/boolean_attribute_value_configuration.h" +#include "opentelemetry/sdk/configuration/cardinality_limits_configuration.h" #include "opentelemetry/sdk/configuration/composable_probability_sampler_configuration.h" #include "opentelemetry/sdk/configuration/configuration.h" #include "opentelemetry/sdk/configuration/configured_sdk.h" @@ -1471,14 +1472,24 @@ std::unique_ptr SdkBuilder::CreatePer OTEL_INTERNAL_LOG_WARN("metric producer not supported, ignoring"); } + sdk = opentelemetry::sdk::metrics::PeriodicExportingMetricReaderFactory::Create( + std::move(exporter_sdk), options); + if (model->cardinality_limits != nullptr) { - OTEL_INTERNAL_LOG_WARN("cardinality limits not supported, ignoring"); + opentelemetry::sdk::metrics::CardinalityLimitOptions cardinality_options; + cardinality_options.default_limit = model->cardinality_limits->default_limit; + cardinality_options.counter = model->cardinality_limits->counter; + cardinality_options.gauge = model->cardinality_limits->gauge; + cardinality_options.histogram = model->cardinality_limits->histogram; + cardinality_options.observable_counter = model->cardinality_limits->observable_counter; + cardinality_options.observable_gauge = model->cardinality_limits->observable_gauge; + cardinality_options.observable_up_down_counter = + model->cardinality_limits->observable_up_down_counter; + cardinality_options.up_down_counter = model->cardinality_limits->up_down_counter; + sdk->SetCardinalityLimitOptions(cardinality_options); } - sdk = opentelemetry::sdk::metrics::PeriodicExportingMetricReaderFactory::Create( - std::move(exporter_sdk), options); - return sdk; } @@ -1496,7 +1507,17 @@ std::unique_ptr SdkBuilder::CreatePul if (model->cardinality_limits != nullptr) { - OTEL_INTERNAL_LOG_WARN("cardinality limits not supported, ignoring"); + opentelemetry::sdk::metrics::CardinalityLimitOptions cardinality_options; + cardinality_options.default_limit = model->cardinality_limits->default_limit; + cardinality_options.counter = model->cardinality_limits->counter; + cardinality_options.gauge = model->cardinality_limits->gauge; + cardinality_options.histogram = model->cardinality_limits->histogram; + cardinality_options.observable_counter = model->cardinality_limits->observable_counter; + cardinality_options.observable_gauge = model->cardinality_limits->observable_gauge; + cardinality_options.observable_up_down_counter = + model->cardinality_limits->observable_up_down_counter; + cardinality_options.up_down_counter = model->cardinality_limits->up_down_counter; + sdk->SetCardinalityLimitOptions(cardinality_options); } return sdk; @@ -1600,6 +1621,20 @@ void SdkBuilder::AddView( sdk_aggregation_config = CreateAggregationConfig(stream->aggregation, sdk_aggregation_type); + // Apply aggregation_cardinality_limit from the view stream configuration + if (stream->aggregation_cardinality_limit != 0) + { + if (sdk_aggregation_config) + { + sdk_aggregation_config->cardinality_limit_ = stream->aggregation_cardinality_limit; + } + else + { + sdk_aggregation_config = std::make_shared( + stream->aggregation_cardinality_limit); + } + } + std::unique_ptr sdk_attribute_processor; if (stream->attribute_keys != nullptr) diff --git a/sdk/src/metrics/meter.cc b/sdk/src/metrics/meter.cc index 4a4c2f6b68..bd111cb63c 100644 --- a/sdk/src/metrics/meter.cc +++ b/sdk/src/metrics/meter.cc @@ -542,6 +542,7 @@ std::unique_ptr Meter::RegisterSyncMetricStorage( else { WarnOnDuplicateInstrument(GetInstrumentationScope(), storage_registry_, view_instr_desc); + sync_storage = std::shared_ptr(new SyncMetricStorage( view_instr_desc, view.GetAggregationType(), view.GetAttributesProcessor(), #ifdef ENABLE_METRICS_EXEMPLAR_PREVIEW @@ -615,6 +616,7 @@ std::unique_ptr Meter::RegisterAsyncMetricStorage( else { WarnOnDuplicateInstrument(GetInstrumentationScope(), storage_registry_, view_instr_desc); + async_storage = std::shared_ptr(new AsyncMetricStorage( view_instr_desc, view.GetAggregationType(), #ifdef ENABLE_METRICS_EXEMPLAR_PREVIEW diff --git a/sdk/src/metrics/meter_context.cc b/sdk/src/metrics/meter_context.cc index 863ef21839..69e43c3a02 100644 --- a/sdk/src/metrics/meter_context.cc +++ b/sdk/src/metrics/meter_context.cc @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -18,6 +19,7 @@ #include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h" #include "opentelemetry/sdk/instrumentationscope/scope_configurator.h" #include "opentelemetry/sdk/metrics/export/metric_filter.h" +#include "opentelemetry/sdk/metrics/instruments.h" #include "opentelemetry/sdk/metrics/meter.h" #include "opentelemetry/sdk/metrics/meter_config.h" #include "opentelemetry/sdk/metrics/meter_context.h" @@ -91,6 +93,20 @@ nostd::span> MeterContext::GetCollectors() noex return nostd::span>(collectors_.data(), collectors_.size()); } +size_t MeterContext::GetReaderCardinalityLimit(InstrumentType instrument_type) noexcept +{ + size_t max_limit = 0; + for (const auto &collector : collectors_) + { + size_t limit = collector->GetCardinalityLimit(instrument_type); + if (limit > max_limit) + { + max_limit = limit; + } + } + return max_limit; +} + opentelemetry::common::SystemTimestamp MeterContext::GetSDKStartTime() noexcept { return sdk_start_ts_; diff --git a/sdk/src/metrics/metric_reader.cc b/sdk/src/metrics/metric_reader.cc index 65cd6be338..004263d4bb 100644 --- a/sdk/src/metrics/metric_reader.cc +++ b/sdk/src/metrics/metric_reader.cc @@ -1,9 +1,15 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#include "opentelemetry/sdk/metrics/metric_reader.h" +#include +#include +#include + +#include "opentelemetry/nostd/function_ref.h" #include "opentelemetry/sdk/common/global_log_handler.h" #include "opentelemetry/sdk/metrics/export/metric_producer.h" +#include "opentelemetry/sdk/metrics/instruments.h" +#include "opentelemetry/sdk/metrics/metric_reader.h" #include "opentelemetry/version.h" OPENTELEMETRY_BEGIN_NAMESPACE @@ -84,6 +90,45 @@ bool MetricReader::IsShutdown() const noexcept return shutdown_.load(std::memory_order_acquire); } +size_t MetricReader::GetCardinalityLimit(InstrumentType instrument_type) const noexcept +{ + switch (instrument_type) + { + case InstrumentType::kCounter: + return cardinality_limit_options_.counter != 0 ? cardinality_limit_options_.counter + : cardinality_limit_options_.default_limit; + case InstrumentType::kHistogram: + return cardinality_limit_options_.histogram != 0 ? cardinality_limit_options_.histogram + : cardinality_limit_options_.default_limit; + case InstrumentType::kUpDownCounter: + return cardinality_limit_options_.up_down_counter != 0 + ? cardinality_limit_options_.up_down_counter + : cardinality_limit_options_.default_limit; + case InstrumentType::kObservableCounter: + return cardinality_limit_options_.observable_counter != 0 + ? cardinality_limit_options_.observable_counter + : cardinality_limit_options_.default_limit; + case InstrumentType::kObservableGauge: + return cardinality_limit_options_.observable_gauge != 0 + ? cardinality_limit_options_.observable_gauge + : cardinality_limit_options_.default_limit; + case InstrumentType::kObservableUpDownCounter: + return cardinality_limit_options_.observable_up_down_counter != 0 + ? cardinality_limit_options_.observable_up_down_counter + : cardinality_limit_options_.default_limit; + case InstrumentType::kGauge: + return cardinality_limit_options_.gauge != 0 ? cardinality_limit_options_.gauge + : cardinality_limit_options_.default_limit; + default: + return cardinality_limit_options_.default_limit; + } +} + +void MetricReader::SetCardinalityLimitOptions(const CardinalityLimitOptions &options) noexcept +{ + cardinality_limit_options_ = options; +} + } // namespace metrics } // namespace sdk OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/src/metrics/state/metric_collector.cc b/sdk/src/metrics/state/metric_collector.cc index d733218835..245fef98a7 100644 --- a/sdk/src/metrics/state/metric_collector.cc +++ b/sdk/src/metrics/state/metric_collector.cc @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include #include @@ -45,6 +46,11 @@ AggregationTemporality MetricCollector::GetAggregationTemporality( return aggregation_temporality; } +size_t MetricCollector::GetCardinalityLimit(InstrumentType instrument_type) noexcept +{ + return metric_reader_->GetCardinalityLimit(instrument_type); +} + MetricProducer::Result MetricCollector::Produce() noexcept { if (!meter_context_) diff --git a/sdk/test/metrics/metric_collector_test.cc b/sdk/test/metrics/metric_collector_test.cc index fb877f84f2..4ec0d3229d 100644 --- a/sdk/test/metrics/metric_collector_test.cc +++ b/sdk/test/metrics/metric_collector_test.cc @@ -402,6 +402,95 @@ TEST_F(MetricCollectorTest, CollectWithMetricFilterTestAttributesTest2) } } +TEST_F(MetricCollectorTest, CardinalityLimitDelegation) +{ + auto context = std::shared_ptr(new MeterContext(ViewRegistryFactory::Create())); + + // Create reader with cardinality limits + auto reader = std::shared_ptr(new MockMetricReader()); + CardinalityLimitOptions options; + options.default_limit = 1000; + options.counter = 100; + options.histogram = 200; + reader->SetCardinalityLimitOptions(options); + + auto collector = AddMetricReaderToMeterContext(context, reader).lock(); + + // Test that MetricCollector delegates to MetricReader + EXPECT_EQ(collector->GetCardinalityLimit(InstrumentType::kCounter), 100); + EXPECT_EQ(collector->GetCardinalityLimit(InstrumentType::kHistogram), 200); + EXPECT_EQ(collector->GetCardinalityLimit(InstrumentType::kUpDownCounter), 1000); + EXPECT_EQ(collector->GetCardinalityLimit(InstrumentType::kObservableCounter), 1000); +} + +TEST_F(MetricCollectorTest, MeterContextMaxCardinalityLimit) +{ + auto context = std::shared_ptr(new MeterContext(ViewRegistryFactory::Create())); + + // Test with no readers - should return 0 + EXPECT_EQ(context->GetReaderCardinalityLimit(InstrumentType::kCounter), 0); + + // Add first reader with limits + auto reader1 = std::shared_ptr(new MockMetricReader()); + CardinalityLimitOptions options1; + options1.counter = 100; + options1.histogram = 500; + reader1->SetCardinalityLimitOptions(options1); + AddMetricReaderToMeterContext(context, reader1); + + // Should return reader1's limits + EXPECT_EQ(context->GetReaderCardinalityLimit(InstrumentType::kCounter), 100); + EXPECT_EQ(context->GetReaderCardinalityLimit(InstrumentType::kHistogram), 500); + + // Add second reader with different limits + auto reader2 = std::shared_ptr(new MockMetricReader()); + CardinalityLimitOptions options2; + options2.counter = 1000; // Higher than reader1 + options2.histogram = 200; // Lower than reader1 + reader2->SetCardinalityLimitOptions(options2); + AddMetricReaderToMeterContext(context, reader2); + + // Should return the max across both readers + EXPECT_EQ(context->GetReaderCardinalityLimit(InstrumentType::kCounter), 1000); // max(100, 1000) + EXPECT_EQ(context->GetReaderCardinalityLimit(InstrumentType::kHistogram), 500); // max(500, 200) +} + +TEST_F(MetricCollectorTest, MeterContextCardinalityLimitWithMultipleReaders) +{ + auto context = std::shared_ptr(new MeterContext(ViewRegistryFactory::Create())); + + // Add three readers with different configurations + auto reader1 = std::shared_ptr(new MockMetricReader()); + CardinalityLimitOptions options1; + options1.default_limit = 500; + options1.counter = 100; + reader1->SetCardinalityLimitOptions(options1); + AddMetricReaderToMeterContext(context, reader1); + + auto reader2 = std::shared_ptr(new MockMetricReader()); + CardinalityLimitOptions options2; + options2.default_limit = 1000; + options2.histogram = 300; + reader2->SetCardinalityLimitOptions(options2); + AddMetricReaderToMeterContext(context, reader2); + + auto reader3 = std::shared_ptr(new MockMetricReader()); + CardinalityLimitOptions options3; + options3.counter = 200; + options3.histogram = 400; + reader3->SetCardinalityLimitOptions(options3); + AddMetricReaderToMeterContext(context, reader3); + + // Counter: max(100, 1000 (default), 200) = 1000 + EXPECT_EQ(context->GetReaderCardinalityLimit(InstrumentType::kCounter), 1000); + + // Histogram: max(500 (default), 300, 400) = 500 + EXPECT_EQ(context->GetReaderCardinalityLimit(InstrumentType::kHistogram), 500); + + // UpDownCounter: max(500 (default), 1000 (default), 0) = 1000 + EXPECT_EQ(context->GetReaderCardinalityLimit(InstrumentType::kUpDownCounter), 1000); +} + #if defined(__GNUC__) || defined(__clang__) || defined(__apple_build_version__) # pragma GCC diagnostic pop #endif diff --git a/sdk/test/metrics/metric_reader_test.cc b/sdk/test/metrics/metric_reader_test.cc index 3019a1225b..0fa91abc10 100644 --- a/sdk/test/metrics/metric_reader_test.cc +++ b/sdk/test/metrics/metric_reader_test.cc @@ -33,3 +33,92 @@ TEST(MetricReaderTest, BasicTests) new MetricCollector(meter_context2.get(), std::move(metric_reader2))}; metric_producer->Produce(); } + +TEST(MetricReaderTest, CardinalityLimitOptions) +{ + std::unique_ptr metric_reader(new MockMetricReader()); + + // Test default values (all 0) + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kCounter), 0); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kHistogram), 0); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kUpDownCounter), 0); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableCounter), 0); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableGauge), 0); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableUpDownCounter), 0); +#if OPENTELEMETRY_ABI_VERSION_NO >= 2 + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kGauge), 0); +#endif + + // Set cardinality limit options + CardinalityLimitOptions options; + options.default_limit = 1000; + options.counter = 100; + options.histogram = 200; + options.up_down_counter = 300; + options.observable_counter = 400; + options.observable_gauge = 500; + options.observable_up_down_counter = 600; + options.gauge = 700; + + metric_reader->SetCardinalityLimitOptions(options); + + // Test instrument-specific limits + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kCounter), 100); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kHistogram), 200); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kUpDownCounter), 300); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableCounter), 400); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableGauge), 500); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableUpDownCounter), 600); +#if OPENTELEMETRY_ABI_VERSION_NO >= 2 + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kGauge), 700); +#endif +} + +TEST(MetricReaderTest, CardinalityLimitOptionsDefaultFallback) +{ + std::unique_ptr metric_reader(new MockMetricReader()); + + // Set only default_limit, leave instrument-specific limits at 0 + CardinalityLimitOptions options; + options.default_limit = 1500; + + metric_reader->SetCardinalityLimitOptions(options); + + // All instrument types should fall back to default_limit + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kCounter), 1500); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kHistogram), 1500); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kUpDownCounter), 1500); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableCounter), 1500); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableGauge), 1500); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableUpDownCounter), 1500); +#if OPENTELEMETRY_ABI_VERSION_NO >= 2 + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kGauge), 1500); +#endif +} + +TEST(MetricReaderTest, CardinalityLimitOptionsMixedConfiguration) +{ + std::unique_ptr metric_reader(new MockMetricReader()); + + // Set default and only some instrument-specific limits + CardinalityLimitOptions options; + options.default_limit = 1000; + options.counter = 100; + options.histogram = 200; + // Leave other instrument types at 0 to test fallback + + metric_reader->SetCardinalityLimitOptions(options); + + // Configured instruments use their specific values + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kCounter), 100); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kHistogram), 200); + + // Unconfigured instruments fall back to default + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kUpDownCounter), 1000); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableCounter), 1000); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableGauge), 1000); + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kObservableUpDownCounter), 1000); +#if OPENTELEMETRY_ABI_VERSION_NO >= 2 + EXPECT_EQ(metric_reader->GetCardinalityLimit(InstrumentType::kGauge), 1000); +#endif +}