From 0209a3d6554d1cc190b36fe5363e69b46a1dc06f Mon Sep 17 00:00:00 2001 From: Aditya Bavadekar Date: Mon, 27 Jul 2026 09:17:35 +0000 Subject: [PATCH 1/2] [SDK] Support MeterConfigurator updates and example Adds MeterProvider::UpdateMeterConfigurator. It sets a new MeterConfig on all meters that already exist, and new meters get the new configurator too. The update takes the same lock as GetMeter, so GetMeter can never hand back a meter with an old config. The meter enabled flag is now an atomic, so the update does not block instrument creation or Collect. --- CHANGELOG.md | 3 + examples/CMakeLists.txt | 1 + examples/meter_configurator/CMakeLists.txt | 13 + examples/meter_configurator/README.md | 249 ++++++++++++++++++ examples/meter_configurator/main.cc | 231 ++++++++++++++++ sdk/include/opentelemetry/sdk/metrics/meter.h | 19 +- .../opentelemetry/sdk/metrics/meter_context.h | 9 + .../sdk/metrics/meter_provider.h | 13 + sdk/src/metrics/meter.cc | 42 +-- sdk/src/metrics/meter_context.cc | 13 + sdk/src/metrics/meter_provider.cc | 25 ++ sdk/test/metrics/meter_provider_sdk_test.cc | 234 +++++++++++++++- 12 files changed, 831 insertions(+), 21 deletions(-) create mode 100644 examples/meter_configurator/CMakeLists.txt create mode 100644 examples/meter_configurator/README.md create mode 100644 examples/meter_configurator/main.cc diff --git a/CHANGELOG.md b/CHANGELOG.md index d331510af8..756e3ae98e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ Increment the: ## [Unreleased] +* [SDK] Add `MeterProvider::UpdateMeterConfigurator()` and example + [#4256](https://github.com/open-telemetry/opentelemetry-cpp/issues/4256) + * [CODE HEALTH] Move SDK trace and metrics test helpers into anonymous namespaces [#4303](https://github.com/open-telemetry/opentelemetry-cpp/pull/4303) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e1237e5308..8f4c12bbc6 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -30,6 +30,7 @@ add_subdirectory(multi_processor) add_subdirectory(environment_carrier) add_subdirectory(tracer_configurator) add_subdirectory(logger_configurator) +add_subdirectory(meter_configurator) add_subdirectory(explicit_parent) if(WITH_EXAMPLES_HTTP) diff --git a/examples/meter_configurator/CMakeLists.txt b/examples/meter_configurator/CMakeLists.txt new file mode 100644 index 0000000000..1a63dbbe70 --- /dev/null +++ b/examples/meter_configurator/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +add_executable(example_meter_configurator main.cc) +target_link_libraries( + example_meter_configurator + PRIVATE opentelemetry-cpp::metrics + opentelemetry-cpp::ostream_metrics_exporter) + +if(BUILD_TESTING) + add_test(NAME examples.meter_configurator + COMMAND "$") +endif() diff --git a/examples/meter_configurator/README.md b/examples/meter_configurator/README.md new file mode 100644 index 0000000000..cddac6d85c --- /dev/null +++ b/examples/meter_configurator/README.md @@ -0,0 +1,249 @@ +# Meter Configurator Example + +This example demonstrates how to set a `MeterConfigurator` on construction +of the `MeterProvider` and to update it at runtime using +`MeterProvider::UpdateMeterConfigurator` to enable or disable specific +meters without restarting the application or recreating the meters. + +`MeterProvider::UpdateMeterConfigurator` recomputes and applies a new +`MeterConfig` to all existing meters, and the updated configurator is also +used for meters created afterwards. It is safe to call concurrently with +`MeterProvider::GetMeter` and with instrument creation and recording on +existing meters. + +Three meters with unique instrumentation scope names are used to simulate +a user application: + +- `my_application`: simulated user application +- `my_library`: simulated user library +- `external_library`: simulated external third-party library + +The example walks through a simulated cost management and debugging +workflow in four stages: + +- Stage 1: Startup. All meters are enabled and all three scopes report + metrics. +- Stage 2: Steady state. The noisy `external_library` metrics are not + needed, so its meter is disabled. +- Stage 3: A user reports unexpected behavior. Re-enable the + `external_library` meter to investigate. +- Stage 4: The investigation completes and the `external_library` meter is + disabled again. + +A `Meter` that is disabled when an instrument is created returns a no-op +instrument, and that instrument stays no-op even if the meter is enabled +later. Create instruments while the meter is enabled, as the classes in +this example do at startup, and then toggle the meters to start and stop +collection. + +Disabling a meter stops collection and export for that scope. Measurements +recorded through already-created instruments while the meter is disabled +are still accumulated by the aggregation, so with cumulative temporality +they are included in the total once the meter is enabled again. This is +why `external_library.requests` reports `3` in stage 3 rather than `2`. + +## Build and run + +```sh +~/build/examples/meter_configurator/example_meter_configurator +``` + +**Expected output:** + +Metrics are exported to stdout via the `OStreamMetricExporter`. The example +uses a long export interval and calls `ForceFlush` at the end of each +stage, so each stage exports exactly once. + +```sh +Stage 1: startup, all meters enabled + my_application, my_library and external_library report metrics +{ + scope name : my_application + schema url : + version : + start time : Mon Jul 27 09:32:35 2026 + end time : Mon Jul 27 09:32:35 2026 + instrument name : my_application.work_items + description : Work items processed by the application + unit : {item} + type : SumPointData + value : 1 + attributes : + resources : + service.name: meter_configurator_example + telemetry.sdk.language: cpp + telemetry.sdk.name: opentelemetry + telemetry.sdk.version: 1.29.0-dev +} +{ + scope name : my_library + schema url : + version : + start time : Mon Jul 27 09:32:35 2026 + end time : Mon Jul 27 09:32:35 2026 + instrument name : my_library.calls + description : Calls into the library + unit : {call} + type : SumPointData + value : 1 + attributes : + resources : + service.name: meter_configurator_example + telemetry.sdk.language: cpp + telemetry.sdk.name: opentelemetry + telemetry.sdk.version: 1.29.0-dev +} +{ + scope name : external_library + schema url : + version : + start time : Mon Jul 27 09:32:35 2026 + end time : Mon Jul 27 09:32:35 2026 + instrument name : external_library.requests + description : Requests handled by the external library + unit : {request} + type : SumPointData + value : 1 + attributes : + resources : + service.name: meter_configurator_example + telemetry.sdk.language: cpp + telemetry.sdk.name: opentelemetry + telemetry.sdk.version: 1.29.0-dev +} + +Stage 2: steady state, external_library meter disabled + only my_application and my_library report metrics +{ + scope name : my_application + schema url : + version : + start time : Mon Jul 27 09:32:35 2026 + end time : Mon Jul 27 09:32:35 2026 + instrument name : my_application.work_items + description : Work items processed by the application + unit : {item} + type : SumPointData + value : 2 + attributes : + resources : + service.name: meter_configurator_example + telemetry.sdk.language: cpp + telemetry.sdk.name: opentelemetry + telemetry.sdk.version: 1.29.0-dev +} +{ + scope name : my_library + schema url : + version : + start time : Mon Jul 27 09:32:35 2026 + end time : Mon Jul 27 09:32:35 2026 + instrument name : my_library.calls + description : Calls into the library + unit : {call} + type : SumPointData + value : 2 + attributes : + resources : + service.name: meter_configurator_example + telemetry.sdk.language: cpp + telemetry.sdk.name: opentelemetry + telemetry.sdk.version: 1.29.0-dev +} + +Stage 3: investigating, external_library meter re-enabled + all three scopes report metrics again +{ + scope name : my_application + schema url : + version : + start time : Mon Jul 27 09:32:35 2026 + end time : Mon Jul 27 09:32:35 2026 + instrument name : my_application.work_items + description : Work items processed by the application + unit : {item} + type : SumPointData + value : 3 + attributes : + resources : + service.name: meter_configurator_example + telemetry.sdk.language: cpp + telemetry.sdk.name: opentelemetry + telemetry.sdk.version: 1.29.0-dev +} +{ + scope name : my_library + schema url : + version : + start time : Mon Jul 27 09:32:35 2026 + end time : Mon Jul 27 09:32:35 2026 + instrument name : my_library.calls + description : Calls into the library + unit : {call} + type : SumPointData + value : 3 + attributes : + resources : + service.name: meter_configurator_example + telemetry.sdk.language: cpp + telemetry.sdk.name: opentelemetry + telemetry.sdk.version: 1.29.0-dev +} +{ + scope name : external_library + schema url : + version : + start time : Mon Jul 27 09:32:35 2026 + end time : Mon Jul 27 09:32:35 2026 + instrument name : external_library.requests + description : Requests handled by the external library + unit : {request} + type : SumPointData + value : 3 + attributes : + resources : + service.name: meter_configurator_example + telemetry.sdk.language: cpp + telemetry.sdk.name: opentelemetry + telemetry.sdk.version: 1.29.0-dev +} + +Stage 4: investigation complete, external_library meter disabled again + only my_application and my_library report metrics +{ + scope name : my_application + schema url : + version : + start time : Mon Jul 27 09:32:35 2026 + end time : Mon Jul 27 09:32:35 2026 + instrument name : my_application.work_items + description : Work items processed by the application + unit : {item} + type : SumPointData + value : 4 + attributes : + resources : + service.name: meter_configurator_example + telemetry.sdk.language: cpp + telemetry.sdk.name: opentelemetry + telemetry.sdk.version: 1.29.0-dev +} +{ + scope name : my_library + schema url : + version : + start time : Mon Jul 27 09:32:35 2026 + end time : Mon Jul 27 09:32:35 2026 + instrument name : my_library.calls + description : Calls into the library + unit : {call} + type : SumPointData + value : 4 + attributes : + resources : + service.name: meter_configurator_example + telemetry.sdk.language: cpp + telemetry.sdk.name: opentelemetry + telemetry.sdk.version: 1.29.0-dev +} +``` diff --git a/examples/meter_configurator/main.cc b/examples/meter_configurator/main.cc new file mode 100644 index 0000000000..1929344efd --- /dev/null +++ b/examples/meter_configurator/main.cc @@ -0,0 +1,231 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// This example shows how to use MeterProvider::UpdateMeterConfigurator to enable and disable +// meters by instrumentation scope at runtime. Updating the MeterConfigurator affects all existing +// and future meters provided by the MeterProvider. It is safe to call concurrently with +// MeterProvider::GetMeter and with instrument creation and recording on existing meters. +// +// Three instrumentation scopes are shown: +// 1. "my_application" (example instrumented user application code), +// 2. "my_library" (example instrumented user library code), +// 3. "external_library" (example instrumented third-party dependency). +// +// The example simulates a typical cost-management and debugging workflow: +// +// Stage 1: Startup. All meters are enabled and all three scopes report metrics. +// Stage 2: The noisy external_library metrics are not needed in steady state, so its meter is +// disabled. Its measurements are no longer collected or exported. +// Stage 3: A user reports unexpected behavior, so the external_library meter is re-enabled to +// investigate. Its metrics are collected and exported again. +// Stage 4: The investigation completes and the external_library meter is disabled again. +// +// IMPORTANT: a Meter that is disabled when an instrument is created returns a no-op instrument, +// and that instrument stays no-op even if the Meter is enabled later. Create instruments while +// the Meter is enabled (as the classes below do at startup) and then toggle the meters to start +// and stop collection. Code that must tolerate being started with a disabled Meter has to create +// its instruments again after the Meter is enabled. + +#include +#include +#include +#include +#include +#include + +#include "opentelemetry/exporters/ostream/metric_exporter_factory.h" +#include "opentelemetry/metrics/meter.h" +#include "opentelemetry/metrics/meter_provider.h" +#include "opentelemetry/metrics/provider.h" +#include "opentelemetry/metrics/sync_instruments.h" +#include "opentelemetry/nostd/shared_ptr.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/nostd/unique_ptr.h" +#include "opentelemetry/sdk/instrumentationscope/scope_configurator.h" +#include "opentelemetry/sdk/metrics/export/periodic_exporting_metric_reader_factory.h" +#include "opentelemetry/sdk/metrics/export/periodic_exporting_metric_reader_options.h" +#include "opentelemetry/sdk/metrics/meter_config.h" +#include "opentelemetry/sdk/metrics/meter_provider.h" +#include "opentelemetry/sdk/metrics/metric_reader.h" +#include "opentelemetry/sdk/metrics/push_metric_exporter.h" +#include "opentelemetry/sdk/metrics/view/view_registry.h" +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/semconv/service_attributes.h" + +namespace metrics_api = opentelemetry::metrics; +namespace metrics_sdk = opentelemetry::sdk::metrics; +namespace metrics_exporters = opentelemetry::exporter::metrics; +namespace scope_sdk = opentelemetry::sdk::instrumentationscope; +namespace nostd = opentelemetry::nostd; + +namespace +{ + +// Simulated third-party dependency instrumented under its own scope. +namespace external_library +{ +class ExternalModule +{ +public: + ExternalModule() + : meter_(metrics_api::Provider::GetMeterProvider()->GetMeter("external_library")), + request_count_(meter_->CreateUInt64Counter("external_library.requests", + "Requests handled by the external library", + "{request}")) + {} + + bool Execute() + { + request_count_->Add(1); + return true; + } + +private: + nostd::shared_ptr meter_; + nostd::unique_ptr> request_count_; +}; +} // namespace external_library + +// Simulated user library instrumented under its own scope. +namespace my_library +{ +class MyModule +{ +public: + MyModule() + : meter_(metrics_api::Provider::GetMeterProvider()->GetMeter("my_library")), + call_count_( + meter_->CreateUInt64Counter("my_library.calls", "Calls into the library", "{call}")) + {} + + bool Execute() + { + call_count_->Add(1); + return external_module_.Execute(); + } + +private: + nostd::shared_ptr meter_; + nostd::unique_ptr> call_count_; + external_library::ExternalModule external_module_; +}; +} // namespace my_library + +// Simulated user application instrumented under its own scope. +class MyApplication +{ +public: + MyApplication() + : meter_(metrics_api::Provider::GetMeterProvider()->GetMeter("my_application")), + work_count_(meter_->CreateUInt64Counter("my_application.work_items", + "Work items processed by the application", + "{item}")) + {} + + void Execute() + { + work_count_->Add(1); + my_module_.Execute(); + } + +private: + nostd::shared_ptr meter_; + nostd::unique_ptr> work_count_; + my_library::MyModule my_module_; +}; + +// Builds a configurator that applies default_config to all scopes, with optional per-scope +// overrides. +std::unique_ptr> MakeMeterConfigurator( + metrics_sdk::MeterConfig default_config, + std::initializer_list> overrides = {}) +{ + scope_sdk::ScopeConfigurator::Builder builder(default_config); + for (const auto &kv : overrides) + { + const auto &name = kv.first; + const auto &config = kv.second; + builder.AddConditionNameEquals(name, config); + } + return std::make_unique>(builder.Build()); +} + +// Creates a MeterProvider with an OStreamMetricExporter and an initial ScopeConfigurator. +std::shared_ptr CreateMeterProvider( + std::unique_ptr> configurator) +{ + auto exporter = metrics_exporters::OStreamMetricExporterFactory::Create(); + + // A long export interval is used so that this example only exports when it calls ForceFlush. + metrics_sdk::PeriodicExportingMetricReaderOptions options; + options.export_interval_millis = std::chrono::milliseconds(60000); + options.export_timeout_millis = std::chrono::milliseconds(500); + auto reader = + metrics_sdk::PeriodicExportingMetricReaderFactory::Create(std::move(exporter), options); + + auto provider = std::make_shared( + std::unique_ptr(new metrics_sdk::ViewRegistry()), + opentelemetry::sdk::resource::Resource::Create( + {{opentelemetry::semconv::service::kServiceName, "meter_configurator_example"}}), + std::move(configurator)); + provider->AddMetricReader(std::move(reader)); + return provider; +} + +} // namespace + +int main() +{ + const metrics_sdk::MeterConfig enabled_config = metrics_sdk::MeterConfig::Enabled(); + const metrics_sdk::MeterConfig disabled_config = metrics_sdk::MeterConfig::Disabled(); + + // Stage 1: Startup, all meters enabled. + auto sdk_meter_provider = CreateMeterProvider(MakeMeterConfigurator(enabled_config)); + + metrics_api::Provider::SetMeterProvider( + nostd::shared_ptr(sdk_meter_provider)); + + // Instantiate the application. This creates the meters and instruments for all three scopes. + // All meters are enabled, so all three scopes get working instruments. + MyApplication my_app; + + std::cout << "Stage 1: startup, all meters enabled\n"; + std::cout << " my_application, my_library and external_library report metrics\n"; + my_app.Execute(); + sdk_meter_provider->ForceFlush(); + + // Stage 2: Steady state. Disable the noisy external_library meter. + std::cout << "\nStage 2: steady state, external_library meter disabled\n"; + std::cout << " only my_application and my_library report metrics\n"; + + sdk_meter_provider->UpdateMeterConfigurator( + MakeMeterConfigurator(enabled_config, {{"external_library", disabled_config}})); + + my_app.Execute(); + sdk_meter_provider->ForceFlush(); + + // Stage 3: A user reports unexpected behavior. Re-enable the external_library meter. + std::cout << "\nStage 3: investigating, external_library meter re-enabled\n"; + std::cout << " all three scopes report metrics again\n"; + + sdk_meter_provider->UpdateMeterConfigurator(MakeMeterConfigurator(enabled_config)); + + my_app.Execute(); + sdk_meter_provider->ForceFlush(); + + // Stage 4: Investigation complete. Disable the external_library meter again. + std::cout << "\nStage 4: investigation complete, external_library meter disabled again\n"; + std::cout << " only my_application and my_library report metrics\n"; + + sdk_meter_provider->UpdateMeterConfigurator( + MakeMeterConfigurator(enabled_config, {{"external_library", disabled_config}})); + + my_app.Execute(); + sdk_meter_provider->ForceFlush(); + + sdk_meter_provider->Shutdown(); + + const nostd::shared_ptr none; + metrics_api::Provider::SetMeterProvider(none); + return 0; +} diff --git a/sdk/include/opentelemetry/sdk/metrics/meter.h b/sdk/include/opentelemetry/sdk/metrics/meter.h index 2fdc6c121c..0ed0e141d6 100644 --- a/sdk/include/opentelemetry/sdk/metrics/meter.h +++ b/sdk/include/opentelemetry/sdk/metrics/meter.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -140,6 +141,20 @@ class Meter final : public opentelemetry::metrics::Meter void DeregisterCallback(uintptr_t callback_id) noexcept override; #endif private: + // MeterProvider needs access to UpdateMeterConfig to propagate configuration updates to + // existing meters. + friend class MeterProvider; + + /** + * Update this meter's MeterConfig. Called only by + * MeterProvider::UpdateMeterConfigurator when the provider-level MeterConfigurator is + * replaced at runtime. + */ + void UpdateMeterConfig(MeterConfig config) noexcept; + + /** Returns whether this meter is enabled by its current MeterConfig. */ + bool IsMeterEnabled() const noexcept { return meter_enabled_.load(std::memory_order_relaxed); } + // order of declaration is important here - instrumentation scope should destroy after // meter-context. std::unique_ptr scope_; @@ -151,7 +166,9 @@ class Meter final : public opentelemetry::metrics::Meter InstrumentEqualNameCaseInsensitive>; MetricStorageMap storage_registry_; std::shared_ptr observable_registry_; - MeterConfig meter_config_; + // MeterConfig state is stored in an atomic variable so that instrument creation and Collect() + // never block on a concurrent MeterProvider::UpdateMeterConfigurator. + std::atomic meter_enabled_{true}; std::unique_ptr RegisterSyncMetricStorage( InstrumentDescriptor &instrument_descriptor); std::unique_ptr RegisterAsyncMetricStorage( diff --git a/sdk/include/opentelemetry/sdk/metrics/meter_context.h b/sdk/include/opentelemetry/sdk/metrics/meter_context.h index 9ccd766565..b1be77359e 100644 --- a/sdk/include/opentelemetry/sdk/metrics/meter_context.h +++ b/sdk/include/opentelemetry/sdk/metrics/meter_context.h @@ -88,6 +88,15 @@ class MeterContext : public std::enable_shared_from_this */ const instrumentationscope::ScopeConfigurator &GetMeterConfigurator() const noexcept; + /** + * Replace the ScopeConfigurator for this meter context. + * + * Note: This method is not thread safe. + * @param meter_configurator The new configurator. + */ + void SetMeterConfigurator(std::unique_ptr> + meter_configurator) noexcept; + /** * NOTE - INTERNAL method, can change in the future. * Process callback for each meter in thread-safe manner diff --git a/sdk/include/opentelemetry/sdk/metrics/meter_provider.h b/sdk/include/opentelemetry/sdk/metrics/meter_provider.h index fccaae690b..59e51980c2 100644 --- a/sdk/include/opentelemetry/sdk/metrics/meter_provider.h +++ b/sdk/include/opentelemetry/sdk/metrics/meter_provider.h @@ -122,6 +122,19 @@ class OPENTELEMETRY_EXPORT MeterProvider final : public opentelemetry::metrics:: std::unique_ptr meter_selector, std::unique_ptr view) noexcept; + /** + * Update the MeterConfigurator for this provider, recreate and propagate the resulting + * MeterConfig to all existing Meters while new Meters will use the updated configuration. + * + * @param meter_configurator The new configurator. + * + * @note Calling MeterProvider::GetMeter from within the + * ScopeConfigurator::ComputeConfig function (as a scope_matcher callback set with + * ScopeConfigurator::AddCondition) is not supported and will result in a deadlock. + */ + void UpdateMeterConfigurator(std::unique_ptr> + meter_configurator) noexcept; + #ifdef ENABLE_METRICS_EXEMPLAR_PREVIEW void SetExemplarFilter(metrics::ExemplarFilterType exemplar_filter_type = diff --git a/sdk/src/metrics/meter.cc b/sdk/src/metrics/meter.cc index 4a4c2f6b68..d8b25b5d06 100644 --- a/sdk/src/metrics/meter.cc +++ b/sdk/src/metrics/meter.cc @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#include #include #include #include @@ -101,26 +102,31 @@ Meter::Meter( std::unique_ptr instrumentation_scope) noexcept : scope_{std::move(instrumentation_scope)}, meter_context_{std::move(meter_context)}, - observable_registry_(new ObservableRegistry()), - meter_config_(MeterConfig::Default()) + observable_registry_(new ObservableRegistry()) { if (auto meter_context_locked_ptr = meter_context_.lock()) { - meter_config_ = meter_context_locked_ptr->GetMeterConfigurator().ComputeConfig(*scope_); + UpdateMeterConfig(meter_context_locked_ptr->GetMeterConfigurator().ComputeConfig(*scope_)); } else { + UpdateMeterConfig(MeterConfig::Default()); OTEL_INTERNAL_LOG_ERROR("[Meter::Meter()] - Error during initialization." << "The metric context is invalid") } } +void Meter::UpdateMeterConfig(MeterConfig config) noexcept +{ + meter_enabled_.store(config.IsEnabled(), std::memory_order_relaxed); +} + opentelemetry::nostd::unique_ptr> Meter::CreateUInt64Counter( opentelemetry::nostd::string_view name, opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateUInt64Counter(name, description, unit); } @@ -146,7 +152,7 @@ opentelemetry::nostd::unique_ptr> Meter::CreateDoubleCo opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateDoubleCounter(name, description, unit); } @@ -173,7 +179,7 @@ Meter::CreateInt64ObservableCounter(opentelemetry::nostd::string_view name, opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateInt64ObservableCounter(name, description, unit); } @@ -199,7 +205,7 @@ Meter::CreateDoubleObservableCounter(opentelemetry::nostd::string_view name, opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateDoubleObservableCounter(name, description, unit); } @@ -225,7 +231,7 @@ opentelemetry::nostd::unique_ptr> Meter::CreateUInt opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateUInt64Histogram(name, description, unit); } @@ -252,7 +258,7 @@ opentelemetry::nostd::unique_ptr> Meter::CreateDouble opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateDoubleHistogram(name, description, unit); } @@ -280,7 +286,7 @@ opentelemetry::nostd::unique_ptr> Meter::CreateInt64Gaug opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateInt64Gauge(name, description, unit); } @@ -306,7 +312,7 @@ opentelemetry::nostd::unique_ptr> Meter::CreateDoubleGaug opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateDoubleGauge(name, description, unit); } @@ -333,7 +339,7 @@ Meter::CreateInt64ObservableGauge(opentelemetry::nostd::string_view name, opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateInt64ObservableGauge(name, description, unit); } @@ -359,7 +365,7 @@ Meter::CreateDoubleObservableGauge(opentelemetry::nostd::string_view name, opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateDoubleObservableGauge(name, description, unit); } @@ -385,7 +391,7 @@ opentelemetry::nostd::unique_ptr> Meter::CreateI opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateInt64UpDownCounter(name, description, unit); } @@ -412,7 +418,7 @@ opentelemetry::nostd::unique_ptr> Meter::CreateDo opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateDoubleUpDownCounter(name, description, unit); } @@ -439,7 +445,7 @@ Meter::CreateInt64ObservableUpDownCounter(opentelemetry::nostd::string_view name opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateInt64ObservableUpDownCounter(name, description, unit); } @@ -465,7 +471,7 @@ Meter::CreateDoubleObservableUpDownCounter(opentelemetry::nostd::string_view nam opentelemetry::nostd::string_view description, opentelemetry::nostd::string_view unit) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return kNoopMeter.CreateDoubleObservableUpDownCounter(name, description, unit); } @@ -642,7 +648,7 @@ std::unique_ptr Meter::RegisterAsyncMetricStorage( std::vector Meter::Collect(CollectorHandle *collector, opentelemetry::common::SystemTimestamp collect_ts) noexcept { - if (!meter_config_.IsEnabled()) + if (!IsMeterEnabled()) { return std::vector(); } diff --git a/sdk/src/metrics/meter_context.cc b/sdk/src/metrics/meter_context.cc index 863ef21839..b76222714f 100644 --- a/sdk/src/metrics/meter_context.cc +++ b/sdk/src/metrics/meter_context.cc @@ -66,6 +66,19 @@ const instrumentationscope::ScopeConfigurator &MeterContext::GetMet return *meter_configurator_; } +void MeterContext::SetMeterConfigurator( + std::unique_ptr> + meter_configurator) noexcept +{ + if (!meter_configurator) + { + OTEL_INTERNAL_LOG_ERROR( + "[MeterContext::SetMeterConfigurator] meter_configurator must not be null, ignoring."); + return; + } + meter_configurator_ = std::move(meter_configurator); +} + bool MeterContext::ForEachMeter( nostd::function_ref &meter)> callback) noexcept { diff --git a/sdk/src/metrics/meter_provider.cc b/sdk/src/metrics/meter_provider.cc index f550d9bafa..ec55d495a0 100644 --- a/sdk/src/metrics/meter_provider.cc +++ b/sdk/src/metrics/meter_provider.cc @@ -130,6 +130,31 @@ void MeterProvider::AddView(std::unique_ptr instrument_selec context_->AddView(std::move(instrument_selector), std::move(meter_selector), std::move(view)); } +void MeterProvider::UpdateMeterConfigurator( + std::unique_ptr> + meter_configurator) noexcept +{ + if (!meter_configurator) + { + OTEL_INTERNAL_LOG_ERROR( + "[MeterProvider::UpdateMeterConfigurator] meter_configurator must not be null, " + "ignoring."); + return; + } + + // Shares the lock with GetMeter so a Meter is never returned while its MeterConfig is out of + // date with the provider configurator. + const std::lock_guard guard(lock_); + context_->SetMeterConfigurator(std::move(meter_configurator)); + + for (auto &meter : context_->GetMeters()) + { + MeterConfig new_config = + context_->GetMeterConfigurator().ComputeConfig(*meter->GetInstrumentationScope()); + meter->UpdateMeterConfig(new_config); + } +} + #ifdef ENABLE_METRICS_EXEMPLAR_PREVIEW void MeterProvider::SetExemplarFilter(metrics::ExemplarFilterType exemplar_filter_type) noexcept diff --git a/sdk/test/metrics/meter_provider_sdk_test.cc b/sdk/test/metrics/meter_provider_sdk_test.cc index dd0e02920c..21bd92a2a5 100644 --- a/sdk/test/metrics/meter_provider_sdk_test.cc +++ b/sdk/test/metrics/meter_provider_sdk_test.cc @@ -2,18 +2,30 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include +#include +#include #include +#include #include +#include #include #include #include "common.h" #include "opentelemetry/common/macros.h" #include "opentelemetry/metrics/meter.h" +#include "opentelemetry/metrics/sync_instruments.h" +#include "opentelemetry/nostd/shared_ptr.h" #include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/nostd/unique_ptr.h" #include "opentelemetry/sdk/common/global_log_handler.h" +#include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h" +#include "opentelemetry/sdk/instrumentationscope/scope_configurator.h" +#include "opentelemetry/sdk/metrics/export/metric_producer.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_provider.h" #include "opentelemetry/sdk/metrics/meter_provider_factory.h" #include "opentelemetry/sdk/metrics/metric_reader.h" @@ -21,10 +33,11 @@ #include "opentelemetry/sdk/metrics/view/instrument_selector.h" #include "opentelemetry/sdk/metrics/view/meter_selector.h" #include "opentelemetry/sdk/metrics/view/view.h" +#include "opentelemetry/sdk/metrics/view/view_registry.h" +#include "opentelemetry/sdk/resource/resource.h" #include "opentelemetry/test_common/sdk/common/scoped_test_log_handler.h" #if OPENTELEMETRY_ABI_VERSION_NO >= 2 -# include # include # include # include @@ -32,7 +45,6 @@ # include "opentelemetry/common/attribute_value.h" # include "opentelemetry/nostd/utility.h" # include "opentelemetry/nostd/variant.h" -# include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h" #endif /* OPENTELEMETRY_ABI_VERSION_NO >= 2 */ using namespace opentelemetry::sdk::metrics; @@ -343,3 +355,221 @@ TEST(MeterProvider, ExplicitShutdownNotWarnOnDestructionCheck) logs = log_handler.Drain(); EXPECT_TRUE(logs.empty()); } + +namespace +{ + +namespace scope_sdk = opentelemetry::sdk::instrumentationscope; + +// Returns a ScopeConfigurator that enables all meters (default config). +std::unique_ptr> EnableAll() +{ + return std::make_unique>( + scope_sdk::ScopeConfigurator::Builder(MeterConfig::Default()).Build()); +} + +// Returns a ScopeConfigurator that disables all meters. +std::unique_ptr> DisableAll() +{ + return std::make_unique>( + scope_sdk::ScopeConfigurator::Builder(MeterConfig::Disabled()).Build()); +} + +// Returns a ScopeConfigurator where the named scope is disabled and all other scopes are enabled. +std::unique_ptr> DisableByName( + opentelemetry::nostd::string_view name) +{ + return std::make_unique>( + scope_sdk::ScopeConfigurator::Builder(MeterConfig::Default()) + .AddConditionNameEquals(name, MeterConfig::Disabled()) + .Build()); +} + +// Builds a MeterProvider whose MetricReader pointer is returned via the out parameter. +std::shared_ptr MakeProvider( + MetricReader *&reader_out, + std::unique_ptr> configurator = EnableAll()) +{ + std::unique_ptr reader{new MockMetricReader()}; + reader_out = reader.get(); + auto provider = std::make_shared( + std::unique_ptr(new ViewRegistry()), + opentelemetry::sdk::resource::Resource::Create({}), std::move(configurator)); + provider->AddMetricReader(std::move(reader)); + return provider; +} + +// Returns the set of instrumentation scope names present in a collection. +std::set CollectScopeNames(MetricReader *reader) +{ + std::set scope_names; + reader->Collect([&scope_names](ResourceMetrics &metric_data) { + for (const auto &scope_metrics : metric_data.scope_metric_data_) + { + scope_names.insert(scope_metrics.scope_->GetName()); + } + return true; + }); + return scope_names; +} + +} // namespace + +TEST(MeterProvider, UpdateMeterConfiguratorDisableByName) +{ + MetricReader *reader{}; + auto provider = MakeProvider(reader); + ASSERT_NE(nullptr, reader); + + auto meter_disabled_by_update = provider->GetMeter("scope.disabled"); + auto meter_unaffected = provider->GetMeter("scope.unaffected"); + + // Both meters are initially enabled, so both produce working instruments. + auto counter_disabled_by_update = meter_disabled_by_update->CreateUInt64Counter("counter.a"); + auto counter_unaffected = meter_unaffected->CreateUInt64Counter("counter.b"); + + counter_disabled_by_update->Add(1); + counter_unaffected->Add(1); + EXPECT_EQ(CollectScopeNames(reader), + (std::set{"scope.disabled", "scope.unaffected"})); + + provider->UpdateMeterConfigurator(DisableByName("scope.disabled")); + + // The disabled meter is no longer collected, the other scope is untouched. + counter_disabled_by_update->Add(1); + counter_unaffected->Add(1); + EXPECT_EQ(CollectScopeNames(reader), (std::set{"scope.unaffected"})); +} + +TEST(MeterProvider, UpdateMeterConfiguratorReEnable) +{ + MetricReader *reader{}; + auto provider = MakeProvider(reader); + ASSERT_NE(nullptr, reader); + + auto meter = provider->GetMeter("scope.toggle"); + auto counter = meter->CreateUInt64Counter("counter.toggle"); + + provider->UpdateMeterConfigurator(DisableAll()); + counter->Add(1); + EXPECT_TRUE(CollectScopeNames(reader).empty()); + + provider->UpdateMeterConfigurator(EnableAll()); + counter->Add(1); + EXPECT_EQ(CollectScopeNames(reader), (std::set{"scope.toggle"})); +} + +TEST(MeterProvider, UpdateMeterConfiguratorAppliesToAllExistingMeters) +{ + MetricReader *reader{}; + auto provider = MakeProvider(reader); + ASSERT_NE(nullptr, reader); + + std::vector> meters; + std::vector>> counters; + for (const auto &name : {"scope.one", "scope.two", "scope.three"}) + { + meters.push_back(provider->GetMeter(name)); + counters.push_back(meters.back()->CreateUInt64Counter("counter")); + } + + provider->UpdateMeterConfigurator(DisableAll()); + for (auto &counter : counters) + { + counter->Add(1); + } + EXPECT_TRUE(CollectScopeNames(reader).empty()); + + provider->UpdateMeterConfigurator(EnableAll()); + for (auto &counter : counters) + { + counter->Add(1); + } + EXPECT_EQ(CollectScopeNames(reader), + (std::set{"scope.one", "scope.two", "scope.three"})); +} + +TEST(MeterProvider, UpdateMeterConfiguratorNewMeterUsesUpdatedConfig) +{ + MetricReader *reader{}; + auto provider = MakeProvider(reader); + ASSERT_NE(nullptr, reader); + + provider->UpdateMeterConfigurator(DisableByName("scope.disabled")); + + // Meters created after the update use the updated configurator. A meter that is disabled at + // instrument creation time returns no-op instruments. + auto meter_disabled = provider->GetMeter("scope.disabled"); + auto meter_enabled = provider->GetMeter("scope.enabled"); + + auto counter_disabled = meter_disabled->CreateUInt64Counter("counter.a"); + auto counter_enabled = meter_enabled->CreateUInt64Counter("counter.b"); + + counter_disabled->Add(1); + counter_enabled->Add(1); + + EXPECT_EQ(CollectScopeNames(reader), (std::set{"scope.enabled"})); +} + +TEST(MeterProvider, UpdateMeterConfiguratorNullIgnored) +{ + MetricReader *reader{}; + auto provider = MakeProvider(reader, DisableByName("scope.disabled")); + ASSERT_NE(nullptr, reader); + + auto meter = provider->GetMeter("scope.enabled"); + auto counter = meter->CreateUInt64Counter("counter"); + + provider->UpdateMeterConfigurator(nullptr); + + // The existing configurator is retained. + counter->Add(1); + EXPECT_EQ(CollectScopeNames(reader), (std::set{"scope.enabled"})); + + auto meter_disabled = provider->GetMeter("scope.disabled"); + auto counter_disabled = meter_disabled->CreateUInt64Counter("counter.disabled"); + counter_disabled->Add(1); + EXPECT_EQ(CollectScopeNames(reader), (std::set{"scope.enabled"})); +} + +TEST(MeterProvider, UpdateMeterConfiguratorConcurrentGetMeter) +{ + MetricReader *reader{}; + auto provider = MakeProvider(reader); + ASSERT_NE(nullptr, reader); + + constexpr int kUpdateCount = 200; + + std::atomic stop{false}; + std::promise worker_ready; + std::future worker_ready_future = worker_ready.get_future(); + + // Worker: create meters and record measurements while the configurator is being replaced. + std::thread worker([&] { + worker_ready.set_value(); + int i = 0; + while (!stop.load(std::memory_order_relaxed)) + { + auto meter = provider->GetMeter("scope." + std::to_string(i++ % 8)); + auto counter = meter->CreateUInt64Counter("counter"); + counter->Add(1); + } + }); + + worker_ready_future.wait(); + + for (int i = 0; i < kUpdateCount; ++i) + { + provider->UpdateMeterConfigurator(i % 2 == 0 ? DisableAll() : EnableAll()); + } + + stop.store(true, std::memory_order_relaxed); + worker.join(); + + // With the final configurator enabling all scopes, every existing meter must be collectable. + provider->UpdateMeterConfigurator(EnableAll()); + auto meter = provider->GetMeter("scope.final"); + auto counter = meter->CreateUInt64Counter("counter.final"); + counter->Add(1); + EXPECT_FALSE(CollectScopeNames(reader).empty()); +} From ab71694ad0fa35db9a7f1f8ad354262deb7ec127 Mon Sep 17 00:00:00 2001 From: Aditya Bavadekar Date: Tue, 28 Jul 2026 10:29:12 +0000 Subject: [PATCH 2/2] Fixing ci build errors Fixes include lists reported by include-what-you-use. Adds a bazel target for the example, and tests for the null configurator and invalid context branches. --- examples/meter_configurator/BUILD | 20 ++++++++ examples/meter_configurator/main.cc | 1 - .../instrumentationscope/scope_configurator.h | 2 + sdk/src/metrics/meter_context_factory.cc | 1 - sdk/src/metrics/meter_provider_factory.cc | 1 - sdk/test/metrics/meter_provider_sdk_test.cc | 49 ++++++++++++++++++- 6 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 examples/meter_configurator/BUILD diff --git a/examples/meter_configurator/BUILD b/examples/meter_configurator/BUILD new file mode 100644 index 0000000000..1f9d8dc5e0 --- /dev/null +++ b/examples/meter_configurator/BUILD @@ -0,0 +1,20 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_cc//cc:cc_test.bzl", "cc_test") + +cc_test( + name = "example_meter_configurator", + srcs = [ + "main.cc", + ], + tags = [ + "ostream", + "tested_example", + ], + deps = [ + "//api", + "//exporters/ostream:ostream_metric_exporter", + "//sdk/src/metrics", + ], +) diff --git a/examples/meter_configurator/main.cc b/examples/meter_configurator/main.cc index 1929344efd..68dd6c32bc 100644 --- a/examples/meter_configurator/main.cc +++ b/examples/meter_configurator/main.cc @@ -30,7 +30,6 @@ #include #include #include -#include #include #include "opentelemetry/exporters/ostream/metric_exporter_factory.h" diff --git a/sdk/include/opentelemetry/sdk/instrumentationscope/scope_configurator.h b/sdk/include/opentelemetry/sdk/instrumentationscope/scope_configurator.h index 7b07321741..170924b57e 100644 --- a/sdk/include/opentelemetry/sdk/instrumentationscope/scope_configurator.h +++ b/sdk/include/opentelemetry/sdk/instrumentationscope/scope_configurator.h @@ -3,7 +3,9 @@ #pragma once #include +#include #include +#include #include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h" #include "opentelemetry/version.h" diff --git a/sdk/src/metrics/meter_context_factory.cc b/sdk/src/metrics/meter_context_factory.cc index 27efbc965f..bb63f045cb 100644 --- a/sdk/src/metrics/meter_context_factory.cc +++ b/sdk/src/metrics/meter_context_factory.cc @@ -4,7 +4,6 @@ #include #include -#include #include "opentelemetry/sdk/instrumentationscope/scope_configurator.h" #include "opentelemetry/sdk/metrics/meter_config.h" #include "opentelemetry/sdk/metrics/meter_context.h" diff --git a/sdk/src/metrics/meter_provider_factory.cc b/sdk/src/metrics/meter_provider_factory.cc index 052639e341..2cc7d0b95e 100644 --- a/sdk/src/metrics/meter_provider_factory.cc +++ b/sdk/src/metrics/meter_provider_factory.cc @@ -4,7 +4,6 @@ #include #include -#include #include "opentelemetry/sdk/instrumentationscope/scope_configurator.h" #include "opentelemetry/sdk/metrics/meter_config.h" #include "opentelemetry/sdk/metrics/meter_context.h" diff --git a/sdk/test/metrics/meter_provider_sdk_test.cc b/sdk/test/metrics/meter_provider_sdk_test.cc index 21bd92a2a5..17e0fb9fdc 100644 --- a/sdk/test/metrics/meter_provider_sdk_test.cc +++ b/sdk/test/metrics/meter_provider_sdk_test.cc @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include #include @@ -16,6 +16,7 @@ #include "opentelemetry/common/macros.h" #include "opentelemetry/metrics/meter.h" #include "opentelemetry/metrics/sync_instruments.h" +#include "opentelemetry/nostd/function_ref.h" #include "opentelemetry/nostd/shared_ptr.h" #include "opentelemetry/nostd/string_view.h" #include "opentelemetry/nostd/unique_ptr.h" @@ -26,6 +27,7 @@ #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" #include "opentelemetry/sdk/metrics/meter_provider.h" #include "opentelemetry/sdk/metrics/meter_provider_factory.h" #include "opentelemetry/sdk/metrics/metric_reader.h" @@ -38,7 +40,6 @@ #include "opentelemetry/test_common/sdk/common/scoped_test_log_handler.h" #if OPENTELEMETRY_ABI_VERSION_NO >= 2 -# include # include # include @@ -573,3 +574,47 @@ TEST(MeterProvider, UpdateMeterConfiguratorConcurrentGetMeter) counter->Add(1); EXPECT_FALSE(CollectScopeNames(reader).empty()); } + +TEST(MeterProvider, SetMeterConfiguratorNullIgnoredOnContext) +{ + ScopedTestLogHandler log_handler{LogLevel::Error}; + + auto context = std::make_shared(std::unique_ptr(new ViewRegistry()), + opentelemetry::sdk::resource::Resource::Create({}), + DisableAll()); + + context->SetMeterConfigurator(nullptr); + + auto logs = log_handler.Drain(); + ASSERT_EQ(logs.size(), 1); + EXPECT_NE(logs[0].msg.find("must not be null"), std::string::npos); + + // The configurator passed at construction is retained. + auto scope = opentelemetry::sdk::instrumentationscope::InstrumentationScope::Create("scope"); + EXPECT_FALSE(context->GetMeterConfigurator().ComputeConfig(*scope).IsEnabled()); +} + +TEST(MeterProvider, MeterWithExpiredContextIsEnabledByDefault) +{ + ScopedTestLogHandler log_handler{LogLevel::Error}; + + std::weak_ptr expired_context; + { + auto context = std::make_shared( + std::unique_ptr(new ViewRegistry()), + opentelemetry::sdk::resource::Resource::Create({}), DisableAll()); + expired_context = context; + } + ASSERT_TRUE(expired_context.expired()); + + // A Meter cannot compute its config without a context, so it falls back to the default config. + Meter meter{expired_context, + opentelemetry::sdk::instrumentationscope::InstrumentationScope::Create("scope")}; + + auto logs = log_handler.Drain(); + ASSERT_EQ(logs.size(), 1); + EXPECT_NE(logs[0].msg.find("The metric context is invalid"), std::string::npos); + + // MeterConfig::Default() is enabled, so instrument creation is not short circuited. + EXPECT_NE(nullptr, meter.CreateUInt64Counter("counter")); +}