From 85505eec82172bfe8a18d27bfbaec75e5eb10807 Mon Sep 17 00:00:00 2001 From: Andres Borja Date: Mon, 10 Nov 2025 17:42:01 +0000 Subject: [PATCH 1/7] feat: Add opentelemetry declarative configuration. --- Cargo.toml | 2 + opentelemetry-config-stdout/CHANGELOG.md | 9 + opentelemetry-config-stdout/CODEOWNERS | 5 + opentelemetry-config-stdout/Cargo.toml | 12 + opentelemetry-config-stdout/README.md | 118 ++++++ .../examples/console/Cargo.toml | 14 + .../examples/console/src/main.rs | 52 +++ .../examples/metrics_console.yaml | 9 + opentelemetry-config-stdout/src/lib.rs | 157 ++++++++ opentelemetry-config/CHANGELOG.md | 9 + opentelemetry-config/CODEOWNERS | 5 + opentelemetry-config/Cargo.toml | 25 ++ opentelemetry-config/README.md | 204 ++++++++++ .../examples/console/Cargo.toml | 14 + .../examples/console/src/main.rs | 160 ++++++++ .../examples/metrics_console.yaml | 9 + opentelemetry-config/src/configurators.rs | 211 ++++++++++ .../src/configurators/metrics_configurator.rs | 49 +++ .../reader_configurator.rs | 364 ++++++++++++++++++ opentelemetry-config/src/lib.rs | 246 ++++++++++++ opentelemetry-config/src/model.rs | 71 ++++ opentelemetry-config/src/model/metrics.rs | 18 + .../src/model/metrics/reader.rs | 109 ++++++ 23 files changed, 1872 insertions(+) create mode 100644 opentelemetry-config-stdout/CHANGELOG.md create mode 100644 opentelemetry-config-stdout/CODEOWNERS create mode 100644 opentelemetry-config-stdout/Cargo.toml create mode 100644 opentelemetry-config-stdout/README.md create mode 100644 opentelemetry-config-stdout/examples/console/Cargo.toml create mode 100644 opentelemetry-config-stdout/examples/console/src/main.rs create mode 100644 opentelemetry-config-stdout/examples/metrics_console.yaml create mode 100644 opentelemetry-config-stdout/src/lib.rs create mode 100644 opentelemetry-config/CHANGELOG.md create mode 100644 opentelemetry-config/CODEOWNERS create mode 100644 opentelemetry-config/Cargo.toml create mode 100644 opentelemetry-config/README.md create mode 100644 opentelemetry-config/examples/console/Cargo.toml create mode 100644 opentelemetry-config/examples/console/src/main.rs create mode 100644 opentelemetry-config/examples/metrics_console.yaml create mode 100644 opentelemetry-config/src/configurators.rs create mode 100644 opentelemetry-config/src/configurators/metrics_configurator.rs create mode 100644 opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs create mode 100644 opentelemetry-config/src/lib.rs create mode 100644 opentelemetry-config/src/model.rs create mode 100644 opentelemetry-config/src/model/metrics.rs create mode 100644 opentelemetry-config/src/model/metrics/reader.rs diff --git a/Cargo.toml b/Cargo.toml index de89943b..d1d951d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,8 @@ [workspace] members = [ "opentelemetry-aws", + "opentelemetry-config", + "opentelemetry-config-stdout", "opentelemetry-contrib", "opentelemetry-datadog", "opentelemetry-etw-logs", diff --git a/opentelemetry-config-stdout/CHANGELOG.md b/opentelemetry-config-stdout/CHANGELOG.md new file mode 100644 index 00000000..192f42e8 --- /dev/null +++ b/opentelemetry-config-stdout/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## vNext + +## v0.1.0 + +### Added + +- Initial declarative configuration for stdout (console) diff --git a/opentelemetry-config-stdout/CODEOWNERS b/opentelemetry-config-stdout/CODEOWNERS new file mode 100644 index 00000000..d6962a90 --- /dev/null +++ b/opentelemetry-config-stdout/CODEOWNERS @@ -0,0 +1,5 @@ +# Code owners file. +# This file controls who is tagged for review for any given pull request. + +# For anything not explicitly taken by someone else: +* @open-telemetry/rust-approvers diff --git a/opentelemetry-config-stdout/Cargo.toml b/opentelemetry-config-stdout/Cargo.toml new file mode 100644 index 00000000..97bf9d79 --- /dev/null +++ b/opentelemetry-config-stdout/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "opentelemetry-config-stdout" +version = "0.1.0" +description = "Declarative configuration for OpenTelemetry SDK using console (stdout) configuration" +license = "Apache-2.0" +edition = "2021" +rust-version = "1.75.0" + +[dependencies] +opentelemetry-config = { path = "../opentelemetry-config" } +opentelemetry_sdk = { version = "0.31.0" } +opentelemetry-stdout = { version = "0.31.0" } diff --git a/opentelemetry-config-stdout/README.md b/opentelemetry-config-stdout/README.md new file mode 100644 index 00000000..6a99ea74 --- /dev/null +++ b/opentelemetry-config-stdout/README.md @@ -0,0 +1,118 @@ +# OpenTelemetry Declarative Configuration for stdout (console) + +![OpenTelemetry — An observability framework for cloud-native software.][splash] + +[splash]: https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo-text.png + +Declarative configuration for applications instrumented with [`OpenTelemetry`]. + +[`OpenTelemetry`]: https://crates.io/crates/opentelemetry + +## Overview + +This crate provides a declarative configuration extension for OpenTelemetry that enables stdout (console) metric exports. It integrates with the `opentelemetry-config` crate to allow YAML-based configuration of the console exporter. + +### Features + +- Console/stdout metrics exporter configuration via YAML +- Support for both Delta and Cumulative temporality +- Integration with OpenTelemetry declarative configuration +- Simple registration API for the configurator + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +opentelemetry-config-stdout = "0.1.0" +``` + +## Quick Start + +### 1. Create a YAML Configuration File + +Create a file named `otel-config.yaml`: + +```yaml +metrics: + readers: + - periodic: + exporter: + console: + temporality: delta + +resource: + service.name: "my-service" + service.version: "1.0.0" +``` + +### 2. Load and Apply Configuration + +```rust +use opentelemetry_config::{ConfiguratorManager, TelemetryConfigurator}; +use opentelemetry_config_stdout::ConsolePeriodicExporterConfigurator; + +fn main() -> Result<(), Box> { + // Create configurator manager and register stdout exporter + let mut configurator_manager = ConfiguratorManager::new(); + ConsolePeriodicExporterConfigurator::register_into(&mut configurator_manager); + + // Load configuration from YAML file + let telemetry_configurator = TelemetryConfigurator::new(); + let providers = telemetry_configurator + .configure_from_yaml_file(&configurator_manager, "otel-config.yaml")?; + + // Use the configured providers + if let Some(meter_provider) = providers.meter_provider() { + // Your application code here + } + + // Shutdown all providers + providers.shutdown()?; + Ok(()) +} +``` + +## Examples + +### Console Exporter Example + +See the [examples/console](examples/console) directory for a complete working example that demonstrates: + +- Setting up a console exporter configurator +- Loading configuration from a YAML file +- Configuring a meter provider +- Proper shutdown handling + +To run the example: + +```bash +cd examples/console +cargo run -- --file ../metrics_console.yaml +``` + +## Configuration Schema + +### Metrics Configuration + +```yaml +metrics: + readers: + - periodic: + exporter: + console: + temporality: delta # or cumulative +``` + +## Contributing + +Contributions are welcome! Please feel free to submit issues or pull requests. + +## License + +This project is licensed under the Apache-2.0 license. + +## Release Notes + +You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust-contrib/tree/main/opentelemetry-config-stdout/CHANGELOG.md). diff --git a/opentelemetry-config-stdout/examples/console/Cargo.toml b/opentelemetry-config-stdout/examples/console/Cargo.toml new file mode 100644 index 00000000..0b642118 --- /dev/null +++ b/opentelemetry-config-stdout/examples/console/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "opentelemetry-config-console-example" +version = "0.1.0" +description = "Declarative configuration for OpenTelemetry SDK example using console configuration" +license = "Apache-2.0" +edition = "2021" +rust-version = "1.75.0" + +[workspace] + +[dependencies] +opentelemetry-config-stdout = { path = "../../" } +opentelemetry-config = { path = " ../../../../../opentelemetry-config" } +opentelemetry_sdk = { version = "0.31.0" } diff --git a/opentelemetry-config-stdout/examples/console/src/main.rs b/opentelemetry-config-stdout/examples/console/src/main.rs new file mode 100644 index 00000000..b67d2023 --- /dev/null +++ b/opentelemetry-config-stdout/examples/console/src/main.rs @@ -0,0 +1,52 @@ +//! # Example OpenTelemetry Config Console +//! +//! This example demonstrates how to configure OpenTelemetry Metrics +//! using the OpenTelemetry Config crate with a Console Exporter. + +use opentelemetry_config::{configurators::TelemetryConfigurator, ConfiguratorManager}; +use opentelemetry_config_stdout::ConsolePeriodicExporterConfigurator; + +use std::env; + +pub fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + + if args.len() == 1 || (args.len() > 1 && args[1] == "--help") { + println!("Usage: cargo run -- --file ../metrics_console.yaml"); + println!("This example demonstrates how to configure OpenTelemetry Metrics using the OpenTelemetry Config crate with a Console Exporter."); + return Ok(()); + } + if args.len() < 3 || args[1] != "--file" { + println!("Error: Configuration file path not provided."); + println!("Usage: cargo run -- --file ../metrics_console.yaml"); + return Ok(()); + } + let config_file = &args[2]; + + // Setup configurator manager with console exporter configurator + let mut configurator_manager = ConfiguratorManager::new(); + ConsolePeriodicExporterConfigurator::register_into(&mut configurator_manager); + + let telemetry_configurator = TelemetryConfigurator::new(); + let providers = telemetry_configurator + .configure_from_yaml_file(&configurator_manager, config_file)?; + + println!("Metrics configured with Console Exporter successfully."); + + println!( + "Meter provider configured: {}", + providers.meter_provider().is_some() + ); + println!( + "Logs provider configured: {}", + providers.logs_provider().is_some() + ); + println!( + "Traces provider configured: {}", + providers.traces_provider().is_some() + ); + + println!("Shutting down telemetry providers..."); + providers.shutdown()?; + Ok(()) +} diff --git a/opentelemetry-config-stdout/examples/metrics_console.yaml b/opentelemetry-config-stdout/examples/metrics_console.yaml new file mode 100644 index 00000000..7cb0ae3e --- /dev/null +++ b/opentelemetry-config-stdout/examples/metrics_console.yaml @@ -0,0 +1,9 @@ +metrics: + readers: + - periodic: + exporter: + console: + temporality: delta +resource: + service.name: "test-service" + service.version: "1.0.0" diff --git a/opentelemetry-config-stdout/src/lib.rs b/opentelemetry-config-stdout/src/lib.rs new file mode 100644 index 00000000..bb89f09a --- /dev/null +++ b/opentelemetry-config-stdout/src/lib.rs @@ -0,0 +1,157 @@ +//! # OpenTelemetry Dynamic Configurator module for Stdout (Console) exporter +//! +//! This module provides a configurator for OpenTelemetry Metrics +//! that enables exporting metrics to the console (stdout) using +//! the OpenTelemetry Config crate. + +use opentelemetry_config::{ + model::metrics::reader::PeriodicExporterConsole, MetricsReaderPeriodicExporterConfigurator, +}; +use opentelemetry_sdk::metrics::MeterProviderBuilder; + +#[derive(Clone)] +pub struct ConsolePeriodicExporterConfigurator {} + +impl ConsolePeriodicExporterConfigurator { + pub fn new() -> Self { + Self {} + } + + pub fn register_into(configurators_manager: &mut opentelemetry_config::ConfiguratorManager) { + let configurator = ConsolePeriodicExporterConfigurator::new(); + configurators_manager + .metrics_mut() + .register_periodic_exporter_configurator::(Box::new( + configurator.clone(), + )); + // TODO: Add logs and traces configurator registration. + } +} + +impl MetricsReaderPeriodicExporterConfigurator for ConsolePeriodicExporterConfigurator { + fn configure( + &self, + mut meter_provider_builder: MeterProviderBuilder, + config: &dyn std::any::Any, + ) -> MeterProviderBuilder { + let mut exporter_builder = opentelemetry_stdout::MetricExporter::builder(); + + let config = config + .downcast_ref::() + .expect("Invalid config type. Expected PeriodicExporterConsole."); + + if let Some(temporality) = &config.temporality { + match temporality { + opentelemetry_config::model::metrics::reader::Temporality::Delta => { + exporter_builder = exporter_builder + .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta); + } + opentelemetry_config::model::metrics::reader::Temporality::Cumulative => { + exporter_builder = exporter_builder + .with_temporality(opentelemetry_sdk::metrics::Temporality::Cumulative); + } + } + } + + let exporter = exporter_builder.build(); + meter_provider_builder = meter_provider_builder.with_periodic_exporter(exporter); + meter_provider_builder + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl Default for ConsolePeriodicExporterConfigurator { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_console_configurator_registration() { + // Arrange + let mut configurator_manager = opentelemetry_config::ConfiguratorManager::new(); + + // Act + ConsolePeriodicExporterConfigurator::register_into(&mut configurator_manager); + + let configurators_option = configurator_manager + .metrics() + .readers_periodic_exporter::(); + + // Assert + assert!(configurators_option.is_some()); + } + + #[test] + fn test_console_configurator_configure_temporality_minimal() { + // Arrange + let configurator = ConsolePeriodicExporterConfigurator::new(); + let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); + + let config = PeriodicExporterConsole { temporality: None }; + + // Act + let configured_builder = configurator.configure(meter_provider_builder, &config); + + // Assert + // Since the MeterProviderBuilder does not expose its internal state, + // we will just ensure that the returned builder is not the same as the original. + assert!(!std::ptr::eq( + &configured_builder, + &opentelemetry_sdk::metrics::SdkMeterProvider::builder() + )); + } + + #[test] + fn test_console_configurator_configure_temporality_delta() { + // Arrange + let configurator = ConsolePeriodicExporterConfigurator::new(); + let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); + + let config = PeriodicExporterConsole { + temporality: Some(opentelemetry_config::model::metrics::reader::Temporality::Delta), + }; + + // Act + let configured_builder = configurator.configure(meter_provider_builder, &config); + + // Assert + // Since the MeterProviderBuilder does not expose its internal state, + // we will just ensure that the returned builder is not the same as the original. + assert!(!std::ptr::eq( + &configured_builder, + &opentelemetry_sdk::metrics::SdkMeterProvider::builder() + )); + } + + #[test] + fn test_console_configurator_configure_temporality_cumulative() { + // Arrange + let configurator = ConsolePeriodicExporterConfigurator::new(); + let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); + + let config = PeriodicExporterConsole { + temporality: Some( + opentelemetry_config::model::metrics::reader::Temporality::Cumulative, + ), + }; + + // Act + let configured_builder = configurator.configure(meter_provider_builder, &config); + + // Assert + // Since the MeterProviderBuilder does not expose its internal state, + // we will just ensure that the returned builder is not the same as the original. + assert!(!std::ptr::eq( + &configured_builder, + &opentelemetry_sdk::metrics::SdkMeterProvider::builder() + )); + } +} diff --git a/opentelemetry-config/CHANGELOG.md b/opentelemetry-config/CHANGELOG.md new file mode 100644 index 00000000..0abd81ea --- /dev/null +++ b/opentelemetry-config/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## vNext + +## v0.1.0 + +### Added + +- Initial declarative configuration diff --git a/opentelemetry-config/CODEOWNERS b/opentelemetry-config/CODEOWNERS new file mode 100644 index 00000000..d6962a90 --- /dev/null +++ b/opentelemetry-config/CODEOWNERS @@ -0,0 +1,5 @@ +# Code owners file. +# This file controls who is tagged for review for any given pull request. + +# For anything not explicitly taken by someone else: +* @open-telemetry/rust-approvers diff --git a/opentelemetry-config/Cargo.toml b/opentelemetry-config/Cargo.toml new file mode 100644 index 00000000..9a80b63c --- /dev/null +++ b/opentelemetry-config/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "opentelemetry-config" +version = "0.1.0" +description = "Declarative configuration for OpenTelemetry SDK" +homepage = "https://github.com/open-telemetry/opentelemetry-rust-contrib/tree/main/opentelemetry-config" +repository = "https://github.com/open-telemetry/opentelemetry-rust-contrib/tree/main/opentelemetry-config" +readme = "README.md" +categories = [ + "development-tools::debugging", + "development-tools::profiling", +] +keywords = ["opentelemetry", "declarative", "metrics", "tracing", "logs", "configuration"] +license = "Apache-2.0" +edition = "2021" +rust-version = "1.75.0" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +opentelemetry = { version = "0.31.0" } +opentelemetry_sdk = { version = "0.31.0", features = ["experimental_metrics_custom_reader"] } +serde = { version = "1.0", features = ["derive"] } +serde_yaml = { version = "0.9.34" } diff --git a/opentelemetry-config/README.md b/opentelemetry-config/README.md new file mode 100644 index 00000000..97fd49c0 --- /dev/null +++ b/opentelemetry-config/README.md @@ -0,0 +1,204 @@ +# OpenTelemetry Declarative Configuration + +![OpenTelemetry — An observability framework for cloud-native software.][splash] + +[splash]: https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo-text.png + +Declarative configuration for applications instrumented with [`OpenTelemetry`]. + +[`OpenTelemetry`]: https://crates.io/crates/opentelemetry + +## Overview + +This crate provides a declarative, YAML-based configuration approach for the OpenTelemetry Rust SDK. Instead of programmatically building telemetry providers with code, you can define your OpenTelemetry configuration in YAML files and load them at runtime. + +### Features + +- **Declarative Configuration**: Define metrics, traces, and logs configuration in YAML +- **Extensible Architecture**: Register custom configurators for different exporters +- **Type-Safe**: Strongly typed configuration models with serde deserialization +- **Multiple Exporters**: Support for Console, OTLP, and custom exporters +- **Resource Attributes**: Configure resource attributes for all telemetry signals + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +opentelemetry-config = "0.1.0" +``` + +## Quick Start + +### 1. Create a YAML Configuration File + +Create a file named `otel-config.yaml`: + +```yaml +metrics: + readers: + - periodic: + exporter: + console: + temporality: delta + +resource: + service.name: "my-service" + service.version: "1.0.0" +``` + +### 2. Load and Apply Configuration + +```rust +fn main() -> Result<(), Box> { + // Create configurator manager and register exporters + let mut configurator_manager = ConfiguratorManager::new(); + configurator_manager + .metrics_mut() + .register_periodic_exporter_configurator::( + Box::new(ConsoleExporterConfigurator) + ); + + // Load configuration from YAML file + let telemetry_configurator = TelemetryConfigurator::new(); + let providers = telemetry_configurator + .configure_from_yaml_file(&configurator_manager, "otel-config.yaml")?; + + // Use the configured providers + if let Some(meter_provider) = providers.meter_provider() { + // Your application code here + } + + // Shutdown all providers + providers.shutdown()?; + Ok(()) +} +``` + +## Architecture + +### Core Components + +- **`ConfiguratorManager`**: Central registry for exporter configurators +- **`TelemetryConfigurator`**: Orchestrates the configuration process from YAML to providers +- **`TelemetryProviders`**: Holds configured meter, tracer, and logger providers +- **`MetricsReaderPeriodicExporterConfigurator`**: Trait for implementing custom metric exporters + +### Configuration Model + +The configuration is structured around the `Telemetry` model which includes: + +- **`metrics`**: Metrics configuration including readers and exporters +- **`traces`**: (Coming soon) Trace configuration +- **`logs`**: (Coming soon) Log configuration +- **`resource`**: Resource attributes (service name, version, etc.) + +## Examples + +### Console Exporter Example + +See the [examples/console](examples/console) directory for a complete working example that demonstrates: + +- Setting up a console exporter configurator +- Loading configuration from a YAML file +- Configuring a meter provider +- Proper shutdown handling + +To run the example: + +```bash +cd examples/console +cargo run -- --file ../metrics_console.yaml +``` + +## Configuration Schema + +### Metrics Configuration + +```yaml +metrics: + readers: + - periodic: + exporter: + console: + temporality: delta # or cumulative + # or + otlp: + endpoint: "http://localhost:4317" + protocol: grpc +``` + +### Resource Attributes + +```yaml +resource: + service.name: "my-service" + service.version: "1.0.0" + deployment.environment: "production" + # Add any custom attributes +``` + +## Extending with Custom Exporters + +To add support for a custom exporter: + +1. Define your exporter configuration model: + +```rust +#[derive(Debug, Deserialize)] +pub struct MyCustomExporter { + pub endpoint: String, + pub timeout: Option, +} +``` + +2. Implement the configurator trait: + +```rust +impl MetricsReaderPeriodicExporterConfigurator for MyCustomConfigurator { + fn configure( + &self, + meter_provider_builder: MeterProviderBuilder, + config: &dyn std::any::Any, + ) -> MeterProviderBuilder { + let config = config.downcast_ref::() + .expect("Invalid config type"); + + // Build your exporter with the config + let exporter = MyExporter::new(config); + meter_provider_builder.with_periodic_exporter(exporter) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} +``` + +3. Register it with the configurator manager: + +```rust +configurator_manager + .metrics_mut() + .register_periodic_exporter_configurator::( + Box::new(MyCustomConfigurator) + ); +``` + +## Current Limitations + +- Only metrics configuration is currently implemented +- Traces and logs configuration are planned for future releases + +## Contributing + +Contributions are welcome! Please feel free to submit issues or pull requests. + +## License + +This project is licensed under the Apache-2.0 license. + +## Release Notes + +You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust-contrib/tree/main/opentelemetry-config/CHANGELOG.md). diff --git a/opentelemetry-config/examples/console/Cargo.toml b/opentelemetry-config/examples/console/Cargo.toml new file mode 100644 index 00000000..bdb716e6 --- /dev/null +++ b/opentelemetry-config/examples/console/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "opentelemetry-config-mock-exporter-example" +version = "0.1.0" +description = "Declarative configuration for OpenTelemetry SDK example using a mock console exporter configuration" +license = "Apache-2.0" +edition = "2021" +rust-version = "1.75.0" + +[workspace] + +[dependencies] +opentelemetry-config = { path = "../../"} +opentelemetry_sdk = { version = "0.31.0", features = ["experimental_metrics_custom_reader"] } +opentelemetry-stdout = { version = "0.31.0" } diff --git a/opentelemetry-config/examples/console/src/main.rs b/opentelemetry-config/examples/console/src/main.rs new file mode 100644 index 00000000..b9e045ec --- /dev/null +++ b/opentelemetry-config/examples/console/src/main.rs @@ -0,0 +1,160 @@ +//! # Example OpenTelemetry Config Console +//! +//! This example demonstrates how to configure OpenTelemetry Metrics +//! using the OpenTelemetry Config crate with a Mock Console Exporter. +//! It is helpful to implement and test custom exporters. + +use opentelemetry_config::{ + configurators::TelemetryConfigurator, model::metrics::reader::PeriodicExporterConsole, + ConfiguratorManager, MetricsReaderPeriodicExporterConfigurator, +}; +use opentelemetry_sdk::metrics::Temporality; +use opentelemetry_sdk::{ + error::OTelSdkResult, + metrics::{data::ResourceMetrics, exporter::PushMetricExporter, MeterProviderBuilder}, +}; +use std::time::Duration; + +use std::env; + +pub fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + + if args.len() == 1 || (args.len() > 1 && args[1] == "--help") { + println!("Usage: cargo run -- --file ../metrics_console.yaml"); + println!("This example demonstrates how to configure OpenTelemetry Metrics using the OpenTelemetry Config crate with a Console Exporter."); + return Ok(()); + } + if args.len() < 3 || args[1] != "--file" { + println!("Error: Configuration file path not provided."); + println!("Usage: cargo run -- --file ../metrics_console.yaml"); + return Ok(()); + } + let config_file = &args[2]; + + // Setup configurator manager with console exporter configurator + let configurator = Box::new(MockPeriodicExporterConfigurator::new()); + let mut configurator_manager = ConfiguratorManager::new(); + + // Register the console exporter configurator for the specific exporter type. + configurator_manager + .metrics_mut() + .register_periodic_exporter_configurator::(configurator); + + let telemetry_configurator = TelemetryConfigurator::new(); + let providers = telemetry_configurator + .configure_from_yaml_file(&configurator_manager, config_file) + .unwrap(); + assert!(providers.meter_provider().is_some()); + println!("Metrics configured with Console Exporter successfully."); + + println!( + "Meter provider configured: {}", + providers.meter_provider().is_some() + ); + println!( + "Logs provider configured: {}", + providers.logs_provider().is_some() + ); + println!( + "Traces provider configured: {}", + providers.traces_provider().is_some() + ); + + println!("Shutting down telemetry providers..."); + providers.shutdown()?; + Ok(()) +} + +pub struct MockPeriodicExporterConfigurator {} + +impl MockPeriodicExporterConfigurator { + fn new() -> Self { + Self {} + } +} + +pub struct MockConsoleExporter { + temporality: Temporality, +} + +impl MockConsoleExporter { + fn new() -> Self { + let temporality = Temporality::Delta; + Self { temporality } + } + + pub fn set_temporality(&mut self, temporality: Temporality) { + self.temporality = temporality; + } +} + +impl PushMetricExporter for MockConsoleExporter { + async fn export(&self, metrics: &ResourceMetrics) -> OTelSdkResult { + println!( + "MockConsoleExporter exporting metrics {:?} with temporality: {:?}", + metrics, self.temporality + ); + Ok(()) + } + + /// Flushes any metric data held by an exporter. + fn force_flush(&self) -> OTelSdkResult { + println!("MockConsoleExporter force flushing metrics."); + Ok(()) + } + + /// Releases any held computational resources. + /// + /// After Shutdown is called, calls to Export will perform no operation and + /// instead will return an error indicating the shutdown state. + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + println!( + "MockConsoleExporter shutting down with timeout: {:?}", + timeout + ); + Ok(()) + } + + /// Shutdown with the default timeout of 5 seconds. + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } + + /// Access the [Temporality] of the MetricExporter. + fn temporality(&self) -> Temporality { + self.temporality + } +} + +impl MetricsReaderPeriodicExporterConfigurator for MockPeriodicExporterConfigurator { + fn configure( + &self, + mut meter_provider_builder: MeterProviderBuilder, + config: &dyn std::any::Any, + ) -> MeterProviderBuilder { + let mut exporter = MockConsoleExporter::new(); + + let config = config + .downcast_ref::() + .expect("Invalid config type. Expected PeriodicExporterConsole."); + + if let Some(temporality) = &config.temporality { + match temporality { + opentelemetry_config::model::metrics::reader::Temporality::Delta => { + exporter.set_temporality(Temporality::Delta); + } + opentelemetry_config::model::metrics::reader::Temporality::Cumulative => { + exporter.set_temporality(Temporality::Cumulative); + } + } + } + + meter_provider_builder = meter_provider_builder.with_periodic_exporter(exporter); + meter_provider_builder + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/opentelemetry-config/examples/metrics_console.yaml b/opentelemetry-config/examples/metrics_console.yaml new file mode 100644 index 00000000..7cb0ae3e --- /dev/null +++ b/opentelemetry-config/examples/metrics_console.yaml @@ -0,0 +1,9 @@ +metrics: + readers: + - periodic: + exporter: + console: + temporality: delta +resource: + service.name: "test-service" + service.version: "1.0.0" diff --git a/opentelemetry-config/src/configurators.rs b/opentelemetry-config/src/configurators.rs new file mode 100644 index 00000000..f821c867 --- /dev/null +++ b/opentelemetry-config/src/configurators.rs @@ -0,0 +1,211 @@ +//! # Configurator objects for OpenTelemetry SDKs +//! +//! This module provides the different element configurators to configure +//! OpenTelemetry SDKs using declarative YAML configurations. + +pub mod metrics_configurator; + +use std::collections::HashMap; + +use opentelemetry::KeyValue; +use opentelemetry_sdk::{metrics::SdkMeterProvider, Resource}; + +use crate::{ + configurators::metrics_configurator::MetricsConfigurator, model::Telemetry, ConfiguratorError, + ConfiguratorManager, TelemetryProviders, +}; + +/// Configurator for Telemetry object +pub struct TelemetryConfigurator { + metrics_configurator: MetricsConfigurator, +} + +impl TelemetryConfigurator { + /// Creates a new TelemetryConfigurator + pub fn new() -> Self { + Self { + metrics_configurator: MetricsConfigurator::new(), + } + } + + pub fn configure( + &self, + configurator_manager: &ConfiguratorManager, + config: &Telemetry, + ) -> Result { + let mut providers = TelemetryProviders::new(); + let resource: Resource = self.as_resource(&config.resource); + if let Some(metrics_config) = &config.metrics { + let mut meter_provider_builder = + SdkMeterProvider::builder().with_resource(resource.clone()); + meter_provider_builder = self.metrics_configurator.configure( + configurator_manager.metrics(), + meter_provider_builder, + metrics_config, + )?; + let meter_provider = meter_provider_builder.build(); + providers = providers.with_meter_provider(meter_provider); + } + + // TODO: Add traces and logs configuration + + Ok(providers) + } + + pub fn configure_from_yaml( + &self, + configurator_manager: &ConfiguratorManager, + yaml_str: &str, + ) -> Result { + let config: crate::model::Telemetry = serde_yaml::from_str(yaml_str).map_err(|e| { + ConfiguratorError::InvalidConfiguration(format!( + "Failed to parse YAML configuration: {}", + e + )) + })?; + self.configure(configurator_manager, &config) + } + + pub fn configure_from_yaml_file( + &self, + configurator_manager: &ConfiguratorManager, + file_path: &str, + ) -> Result { + let yaml_str = std::fs::read_to_string(file_path).map_err(|e| { + ConfiguratorError::InvalidConfiguration(format!( + "Failed to read YAML configuration file: {}", + e + )) + })?; + self.configure_from_yaml(configurator_manager, &yaml_str) + } + + /// Converts resource attributes from HashMap to Resource + fn as_resource(&self, attributes: &HashMap) -> Resource { + let mut builder = Resource::builder(); + + for (key, value) in attributes { + let resource_attribute = self.as_resource_attribute(key, value); + builder = builder.with_attribute(resource_attribute); + } + + builder.build() + } + + /// Converts a single resource attribute from serde_yaml::Value to KeyValue + fn as_resource_attribute(&self, key: &str, value: &serde_yaml::Value) -> KeyValue { + match value { + serde_yaml::Value::String(s) => KeyValue::new(key.to_string(), s.clone()), + serde_yaml::Value::Number(n) => { + if let Some(i) = n.as_i64() { + KeyValue::new(key.to_string(), i) + } else if let Some(f) = n.as_f64() { + KeyValue::new(key.to_string(), f) + } else { + KeyValue::new(key.to_string(), n.to_string()) + } + } + serde_yaml::Value::Bool(b) => KeyValue::new(key.to_string(), *b), + _ => KeyValue::new(key.to_string(), format!("{:?}", value)), + } + } +} + +impl Default for TelemetryConfigurator { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use std::{any::Any, sync::atomic::AtomicU16}; + + use opentelemetry_sdk::metrics::MeterProviderBuilder; + + use crate::{ + model::metrics::reader::PeriodicExporterConsole, MetricsReaderPeriodicExporterConfigurator, + }; + + use super::*; + + struct MockMetricsReadersPeriodicExporterConsoleConfigurator { + call_count: AtomicU16, + } + + impl MockMetricsReadersPeriodicExporterConsoleConfigurator { + fn new() -> Self { + Self { + call_count: AtomicU16::new(0), + } + } + + pub fn get_call_count(&self) -> u16 { + self.call_count.load(std::sync::atomic::Ordering::SeqCst) + } + } + + impl MetricsReaderPeriodicExporterConfigurator + for MockMetricsReadersPeriodicExporterConsoleConfigurator + { + fn configure( + &self, + meter_provider_builder: MeterProviderBuilder, + config: &(dyn Any + 'static), + ) -> MeterProviderBuilder { + // Mock implementation: In a real scenario, configure the console exporter here + self.call_count + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + let config = config + .downcast_ref::() + .expect("Invalid config type"); + println!("Mock configure called with config: {:?}", config); + meter_provider_builder + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + } + + #[test] + fn test_configure_telemetry_from_yaml() { + let yaml_str = r#" + metrics: + readers: + - periodic: + exporter: + console: + temporality: delta + resource: + service.name: "test-service" + service.version: "1.0.0" + replica.count: 3 + development: true + "#; + + let configurator = Box::new(MockMetricsReadersPeriodicExporterConsoleConfigurator::new()); + + let mut configurator_manager = ConfiguratorManager::new(); + let metrics_configurator_manager = configurator_manager.metrics_mut(); + //metrics_configurator_manager.register_periodic_exporter_console_configurator(configurator); + metrics_configurator_manager + .register_periodic_exporter_configurator::(configurator); + + let telemetry_configurator = TelemetryConfigurator::new(); + let providers = telemetry_configurator + .configure_from_yaml(&configurator_manager, yaml_str) + .unwrap(); + assert!(providers.meter_provider.is_some()); + + let configurator = configurator_manager + .metrics() + .readers_periodic_exporter::() + .unwrap(); + let configurator = configurator + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(configurator.get_call_count(), 1); + } +} diff --git a/opentelemetry-config/src/configurators/metrics_configurator.rs b/opentelemetry-config/src/configurators/metrics_configurator.rs new file mode 100644 index 00000000..9317b554 --- /dev/null +++ b/opentelemetry-config/src/configurators/metrics_configurator.rs @@ -0,0 +1,49 @@ +//! Configurator for Metrics telemetry +//! +//! This module provides functionality to configure Metrics telemetry +//! in OpenTelemetry SDKs using declarative YAML configurations. + +pub mod reader_configurator; + +use opentelemetry_sdk::metrics::MeterProviderBuilder; + +use crate::{ConfiguratorError, MetricsConfiguratorManager}; + +use crate::configurators::metrics_configurator::reader_configurator::ReaderConfigurator; + +/// Configurator for Metrics telemetry +pub struct MetricsConfigurator { + reader_configurator: ReaderConfigurator, +} + +impl MetricsConfigurator { + pub fn new() -> Self { + MetricsConfigurator { + reader_configurator: ReaderConfigurator::new(), + } + } + + /// Configures the Metrics provider based on the provided configuration + pub fn configure( + &self, + metrics_configurator_manager: &MetricsConfiguratorManager, + mut meter_provider_builder: MeterProviderBuilder, + config: &crate::model::metrics::Metrics, + ) -> Result { + for reader in &config.readers { + meter_provider_builder = self.reader_configurator.configure( + metrics_configurator_manager, + meter_provider_builder, + reader, + )?; + } + + Ok(meter_provider_builder) + } +} + +impl Default for MetricsConfigurator { + fn default() -> Self { + Self::new() + } +} diff --git a/opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs b/opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs new file mode 100644 index 00000000..42884b1b --- /dev/null +++ b/opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs @@ -0,0 +1,364 @@ +//! # Metrics reader configurator module. +//! +//! This module provides configurators for setting up metrics readers +//! in OpenTelemetry SDKs using declarative YAML configurations. + +use opentelemetry_sdk::metrics::MeterProviderBuilder; + +use crate::{ + model::metrics::reader::{PeriodicExporterConsole, PeriodicExporterOtlp, Reader}, + ConfiguratorError, MetricsConfiguratorManager, +}; + +/// Configurator for Metrics readers +pub struct ReaderConfigurator { + periodic_reader_configurator: PeriodicReaderConfigurator, +} + +impl ReaderConfigurator { + pub fn new() -> Self { + ReaderConfigurator { + periodic_reader_configurator: PeriodicReaderConfigurator::new(), + } + } + /// Configures a metrics reader based on the provided configuration + pub fn configure( + &self, + metrics_configurator_manager: &MetricsConfiguratorManager, + mut meter_provider_builder: MeterProviderBuilder, + config: &Reader, + ) -> Result { + match config { + crate::model::metrics::reader::Reader::Periodic(periodic_config) => { + meter_provider_builder = self.periodic_reader_configurator.configure( + metrics_configurator_manager, + meter_provider_builder, + periodic_config, + )?; + } + crate::model::metrics::reader::Reader::Pull(_pull_config) => { + // TODO: Implement pull reader configuration + } + } + Ok(meter_provider_builder) + } +} + +impl Default for ReaderConfigurator { + fn default() -> Self { + Self::new() + } +} + +/// Periodic reader configurator +pub struct PeriodicReaderConfigurator { + periodic_exporter_configurator: PeriodicExporterConfigurator, +} + +impl PeriodicReaderConfigurator { + /// Creates a new PeriodicReaderConfigurator + pub fn new() -> Self { + PeriodicReaderConfigurator { + periodic_exporter_configurator: PeriodicExporterConfigurator::new(), + } + } + + /// Configures a periodic metrics reader based on the provided configuration + pub fn configure( + &self, + metrics_configurator_manager: &MetricsConfiguratorManager, + mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, + config: &crate::model::metrics::reader::Periodic, + ) -> Result { + if let Some(exporter_config) = &config.exporter { + meter_provider_builder = self.periodic_exporter_configurator.configure( + metrics_configurator_manager, + meter_provider_builder, + exporter_config, + )?; + } + + Ok(meter_provider_builder) + } +} + +impl Default for PeriodicReaderConfigurator { + fn default() -> Self { + Self::new() + } +} + +/// Periodic exporter configurator +pub struct PeriodicExporterConfigurator {} + +impl PeriodicExporterConfigurator { + /// Creates a new PeriodicExporterConfigurator + pub fn new() -> Self { + PeriodicExporterConfigurator {} + } + + /// Configures a periodic metrics exporter based on the provided configuration + pub fn configure( + &self, + metrics_configurator_manager: &MetricsConfiguratorManager, + mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, + config: &crate::model::metrics::reader::PeriodicExporter, + ) -> Result { + if let Some(console_config) = &config.console { + let configurator_option = + metrics_configurator_manager.readers_periodic_exporter::(); + if let Some(configurator) = configurator_option { + meter_provider_builder = + configurator.configure(meter_provider_builder, console_config); + } else { + return Err(ConfiguratorError::NotRegisteredConfigurator("No configurator found for PeriodicExporterConsole. Make sure it is registered as configurator.".to_string())); + } + } + + if let Some(otlp_config) = &config.otlp { + let configurator_option = + metrics_configurator_manager.readers_periodic_exporter::(); + if let Some(configurator) = configurator_option { + meter_provider_builder = + configurator.configure(meter_provider_builder, otlp_config); + } else { + return Err(ConfiguratorError::NotRegisteredConfigurator("No configurator found for PeriodicExporterOtlp. Make sure it is registered as configurator.".to_string())); + } + } + + Ok(meter_provider_builder) + } +} + +impl Default for PeriodicExporterConfigurator { + fn default() -> Self { + Self::new() + } +} + +// Pull reader configurator +pub struct PullReaderConfigurator { + pull_exporter_configurator: PullExporterConfigurator, +} + +impl PullReaderConfigurator { + /// Creates a new PullReaderConfigurator + pub fn new() -> Self { + PullReaderConfigurator { + pull_exporter_configurator: PullExporterConfigurator::new(), + } + } + + /// Configures a pull metrics reader based on the provided configuration + pub fn configure( + &self, + metrics_configurator_manager: &MetricsConfiguratorManager, + mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, + config: &crate::model::metrics::reader::Pull, + ) -> Result { + if let Some(exporter_config) = &config.exporter { + meter_provider_builder = self.pull_exporter_configurator.configure( + metrics_configurator_manager, + meter_provider_builder, + exporter_config, + )?; + } + Ok(meter_provider_builder) + } +} + +impl Default for PullReaderConfigurator { + fn default() -> Self { + Self::new() + } +} + +/// Pull exporter configurator +pub struct PullExporterConfigurator {} + +impl PullExporterConfigurator { + /// Creates a new PullExporterConfigurator + pub fn new() -> Self { + PullExporterConfigurator {} + } + + pub fn configure( + &self, + _metrics_configurator_manager: &MetricsConfiguratorManager, + meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, + config: &crate::model::metrics::reader::PullExporter, + ) -> Result { + if let Some(_prometheus_config) = &config.prometheus { + // Explicitly Prometheus exporter is not supported in this configurator. + return Err(ConfiguratorError::UnsupportedExporter( + "Prometheus exporter is not supported.".to_string(), + )); + } + Ok(meter_provider_builder) + } +} + +impl Default for PullExporterConfigurator { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + + use opentelemetry_sdk::metrics::SdkMeterProvider; + + use crate::MetricsReaderPeriodicExporterConfigurator; + + use super::*; + + struct MockMetricsReadersPeriodicExporterConsoleConfigurator {} + + impl MockMetricsReadersPeriodicExporterConsoleConfigurator { + fn new() -> Self { + MockMetricsReadersPeriodicExporterConsoleConfigurator {} + } + + fn register_into(manager: &mut crate::ConfiguratorManager) { + manager + .metrics_mut() + .register_periodic_exporter_configurator::(Box::new( + Self::new(), + )); + } + } + + impl MetricsReaderPeriodicExporterConfigurator + for MockMetricsReadersPeriodicExporterConsoleConfigurator + { + fn configure( + &self, + meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, + _config: &dyn std::any::Any, + ) -> opentelemetry_sdk::metrics::MeterProviderBuilder { + // Mock implementation: just return the builder as is + meter_provider_builder + } + + fn as_any(&self) -> &dyn std::any::Any { + todo!() + } + } + + #[test] + fn test_reader_configurator_configure() { + let configurator = ReaderConfigurator::new(); + let mut configurator_manager = crate::ConfiguratorManager::new(); + MockMetricsReadersPeriodicExporterConsoleConfigurator::register_into( + &mut configurator_manager, + ); + let meter_provider_builder = SdkMeterProvider::builder(); + + let config = crate::model::metrics::reader::Reader::Periodic( + crate::model::metrics::reader::Periodic { + exporter: Some(crate::model::metrics::reader::PeriodicExporter { + console: Some(PeriodicExporterConsole { temporality: None }), + otlp: None, + }), + }, + ); + + let metrics_configurator_manager = configurator_manager.metrics(); + + _ = configurator + .configure( + &metrics_configurator_manager, + meter_provider_builder, + &config, + ) + .unwrap(); + } + + #[test] + fn test_reader_configurator_configure_console_configurator_not_registered() { + let configurator = ReaderConfigurator::new(); + let metrics_configurator_manager = MetricsConfiguratorManager::new(); + let meter_provider_builder = SdkMeterProvider::builder(); + + let config = crate::model::metrics::reader::Reader::Periodic( + crate::model::metrics::reader::Periodic { + exporter: Some(crate::model::metrics::reader::PeriodicExporter { + console: Some(PeriodicExporterConsole { temporality: None }), + otlp: None, + }), + }, + ); + + let result = configurator.configure( + &metrics_configurator_manager, + meter_provider_builder, + &config, + ); + if let Err(e) = result { + assert!(e + .to_string() + .contains("No configurator found for PeriodicExporterConsole")); + } else { + panic!("Expected error due to missing configurator, but got Ok"); + } + } + + #[test] + fn test_reader_configurator_configure_otlp_configurator_not_registered() { + let configurator = ReaderConfigurator::new(); + let metrics_configurator_manager = MetricsConfiguratorManager::new(); + let meter_provider_builder = SdkMeterProvider::builder(); + + let config = crate::model::metrics::reader::Reader::Periodic( + crate::model::metrics::reader::Periodic { + exporter: Some(crate::model::metrics::reader::PeriodicExporter { + console: None, + otlp: Some(PeriodicExporterOtlp { + endpoint: None, + protocol: None, + temporality: None, + }), + }), + }, + ); + + let result = configurator.configure( + &metrics_configurator_manager, + meter_provider_builder, + &config, + ); + if let Err(e) = result { + assert!(e + .to_string() + .contains("No configurator found for PeriodicExporterOtlp")); + } else { + panic!("Expected error due to missing configurator, but got Ok"); + } + } + + #[test] + fn test_periodic_exporter_configurator_configure_unsupported_exporter() { + let configurator = PullExporterConfigurator::new(); + let metrics_configurator_manager = MetricsConfiguratorManager::new(); + let meter_provider_builder = SdkMeterProvider::builder(); + let config = crate::model::metrics::reader::PullExporter { + prometheus: Some(crate::model::metrics::reader::PullExporterPrometheus { + host: "localhost".to_string(), + port: 9090, + }), + }; + let result = configurator.configure( + &metrics_configurator_manager, + meter_provider_builder, + &config, + ); + if let Err(e) = result { + assert!(e + .to_string() + .contains("Prometheus exporter is not supported.")); + } else { + panic!("Expected error due to unsupported exporter, but got Ok"); + } + } +} diff --git a/opentelemetry-config/src/lib.rs b/opentelemetry-config/src/lib.rs new file mode 100644 index 00000000..24f07c42 --- /dev/null +++ b/opentelemetry-config/src/lib.rs @@ -0,0 +1,246 @@ +//! # Library for declarative configuration of OpenTelemetry. +//! +//! This library provides a way to configure OpenTelemetry SDK components +//! using a declarative approach. It allows users to define configurations +//! for metrics, traces, and exporters in a structured manner. + +use std::{ + any::type_name, + collections::HashMap, + error, + fmt::{self, Display}, +}; + +use opentelemetry_sdk::{ + error::OTelSdkResult, + logs::SdkLoggerProvider, + metrics::{MeterProviderBuilder, SdkMeterProvider}, + trace::SdkTracerProvider, +}; + +use crate::model::metrics::reader::{PeriodicExporterConsole, PeriodicExporterOtlp}; + +pub mod configurators; +pub mod model; + +pub struct ConfiguratorManager { + metrics: MetricsConfiguratorManager, +} + +impl ConfiguratorManager { + pub fn new() -> Self { + Self { + metrics: MetricsConfiguratorManager::new(), + } + } + + pub fn metrics_mut(&mut self) -> &mut MetricsConfiguratorManager { + &mut self.metrics + } + + pub fn metrics(&self) -> &MetricsConfiguratorManager { + &self.metrics + } +} + +impl Default for ConfiguratorManager { + fn default() -> Self { + Self::new() + } +} + +pub struct MetricsConfiguratorManager { + readers_periodic_exporters: HashMap>, +} + +impl MetricsConfiguratorManager { + pub fn new() -> Self { + Self { + readers_periodic_exporters: HashMap::new(), + } + } + + pub fn register_periodic_exporter_configurator( + &mut self, + configurator: Box, + ) { + let name: String = type_name::().to_string(); + self.readers_periodic_exporters.insert( + name, + configurator as Box, + ); + } + + pub fn readers_periodic_exporter( + &self, + //type_name: &str, + ) -> Option<&dyn MetricsReaderPeriodicExporterConfigurator> { + let type_name = type_name::().to_string(); + self.readers_periodic_exporters + .get(&type_name) + .map(|b| b.as_ref()) + } +} + +impl Default for MetricsConfiguratorManager { + fn default() -> Self { + Self::new() + } +} + +/// Trait for configuring Console Exporter for Periodic Metrics Reader +pub trait MetricsReadersPeriodicExporterConsoleConfigurator { + fn configure( + &self, + meter_provider_builder: MeterProviderBuilder, + config: &PeriodicExporterConsole, + ) -> MeterProviderBuilder; + + fn as_any(&self) -> &dyn std::any::Any; +} + +/// Trait for configuring OTLP Exporter for Periodic Metrics Reader +pub trait MetricsReadersPeriodicExporterOtlpConfigurator { + fn configure( + &self, + meter_provider_builder: MeterProviderBuilder, + config: &PeriodicExporterOtlp, + ) -> MeterProviderBuilder; +} + +pub trait MetricsReaderPeriodicExporterConfigurator { + fn configure( + &self, + meter_provider_builder: MeterProviderBuilder, + config: &dyn std::any::Any, + ) -> MeterProviderBuilder; + + fn as_any(&self) -> &dyn std::any::Any; +} + +/// Holds the configured telemetry providers +pub struct TelemetryProviders { + meter_provider: Option, + traces_provider: Option, + logs_provider: Option, +} + +impl TelemetryProviders { + pub fn new() -> Self { + TelemetryProviders { + meter_provider: None, + traces_provider: None, + logs_provider: None, + } + } + + pub fn with_meter_provider(mut self, meter_provider: SdkMeterProvider) -> Self { + self.meter_provider = Some(meter_provider); + self + } + + pub fn with_traces_provider(mut self, traces_provider: SdkTracerProvider) -> Self { + self.traces_provider = Some(traces_provider); + self + } + pub fn with_logs_provider(mut self, logs_provider: SdkLoggerProvider) -> Self { + self.logs_provider = Some(logs_provider); + self + } + + pub fn meter_provider(&self) -> Option<&SdkMeterProvider> { + self.meter_provider.as_ref() + } + + pub fn traces_provider(&self) -> Option<&SdkTracerProvider> { + self.traces_provider.as_ref() + } + + pub fn logs_provider(&self) -> Option<&SdkLoggerProvider> { + self.logs_provider.as_ref() + } + + pub fn shutdown(self) -> OTelSdkResult { + if let Some(meter_provider) = self.meter_provider { + meter_provider.shutdown()?; + } + if let Some(traces_provider) = self.traces_provider { + traces_provider.shutdown()?; + } + if let Some(logs_provider) = self.logs_provider { + logs_provider.shutdown()?; + } + Ok(()) + } +} + +impl Default for TelemetryProviders { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug)] +pub enum ConfiguratorError { + InvalidConfiguration(String), + UnsupportedExporter(String), + NotRegisteredConfigurator(String), +} + +impl error::Error for ConfiguratorError {} + +impl Display for ConfiguratorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConfiguratorError::InvalidConfiguration(details) => { + write!(f, "Invalid configuration: {}", details) + } + ConfiguratorError::UnsupportedExporter(details) => { + write!(f, "Unsupported exporter: {}", details) + } + ConfiguratorError::NotRegisteredConfigurator(details) => { + write!(f, "Not registered configurator: {}", details) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_register_periodic_exporter_configurator() { + // Arrange + struct MockPeriodicExporterConfigurator {} + + impl MetricsReaderPeriodicExporterConfigurator for MockPeriodicExporterConfigurator { + fn configure( + &self, + meter_provider_builder: MeterProviderBuilder, + _config: &dyn std::any::Any, + ) -> MeterProviderBuilder { + meter_provider_builder + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + } + + let mock_configurator = Box::new(MockPeriodicExporterConfigurator {}); + let mut configurator_manager = ConfiguratorManager::new(); + + // Act + configurator_manager + .metrics_mut() + .register_periodic_exporter_configurator::(mock_configurator); + + // Assert + let type_name = type_name::().to_string(); + assert!(configurator_manager + .metrics() + .readers_periodic_exporters + .contains_key(&type_name)); + } +} diff --git a/opentelemetry-config/src/model.rs b/opentelemetry-config/src/model.rs new file mode 100644 index 00000000..a47e8bb3 --- /dev/null +++ b/opentelemetry-config/src/model.rs @@ -0,0 +1,71 @@ +//! # Telemetry Configuration models +//! +//! This module defines the configuration structures for telemetry +//! used in OpenTelemetry SDKs. + +pub mod metrics; + +use std::collections::HashMap; + +use serde::Deserialize; +use serde_yaml::Value; + +use crate::model::metrics::Metrics; + +/// Configuration for Telemetry +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Telemetry { + /// Metrics telemetry configuration + pub metrics: Option, + + /// Resource attributes to be associated with all telemetry data + #[serde(default)] + pub resource: HashMap, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_yaml; + + #[test] + fn test_deserialize_telemetry() { + let yaml_str = r#" +metrics: + readers: + - periodic: + exporter: + console: {} +resource: + service.name: "example-service" + service.version: "1.0.0" +"#; + let telemetry: Telemetry = serde_yaml::from_str(yaml_str).unwrap(); + assert!(telemetry.metrics.is_some()); + let resource = telemetry.resource; + assert_eq!(resource.get("service.name").unwrap(), "example-service"); + assert_eq!(resource.get("service.version").unwrap(), "1.0.0"); + } + + #[test] + fn test_deserialize_invalid_telemetry() { + let yaml_str = r#" +metrics: + readers: + - periodic: + exporter: + invalid_console: {} +resource: + service.name: "example-service" + service.version: "1.0.0" +"#; + let telemetry_result: Result = serde_yaml::from_str(yaml_str); + + if let Err(e) = telemetry_result { + assert!(e.to_string().contains("unknown field `invalid_console`")); + } else { + panic!("Expected error due to invalid field, but got Ok"); + } + } +} diff --git a/opentelemetry-config/src/model/metrics.rs b/opentelemetry-config/src/model/metrics.rs new file mode 100644 index 00000000..74cffe10 --- /dev/null +++ b/opentelemetry-config/src/model/metrics.rs @@ -0,0 +1,18 @@ +//! # Metrics Configuration module +//! +//! This module defines the configuration structures for Metrics telemetry +//! used in OpenTelemetry SDKs. + +pub mod reader; + +use serde::Deserialize; + +use crate::model::metrics::reader::Reader; + +/// Configuration for Metrics +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Metrics { + /// Readers configuration for Metrics telemetry + pub readers: Vec, +} diff --git a/opentelemetry-config/src/model/metrics/reader.rs b/opentelemetry-config/src/model/metrics/reader.rs new file mode 100644 index 00000000..f8fc05a2 --- /dev/null +++ b/opentelemetry-config/src/model/metrics/reader.rs @@ -0,0 +1,109 @@ +//! Metrics Reader Configuration models +//! +//! This module defines the configuration structures and factory traits +//! for Metrics readers used in OpenTelemetry SDKs. + +use std::collections::HashMap; + +use serde::Deserialize; + +/// Metrics reader configuration +#[derive(Debug)] +pub enum Reader { + Periodic(Periodic), + Pull(Pull), +} + +/// Custom deserialization for Reader enum to handle different reader types +impl<'de> Deserialize<'de> for Reader { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let map: HashMap = HashMap::deserialize(deserializer)?; + + if let Some((key, value)) = map.into_iter().next() { + match key.as_str() { + "periodic" => { + let variant: Periodic = + serde_yaml::from_value(value).map_err(serde::de::Error::custom)?; + Ok(Reader::Periodic(variant)) + } + "pull" => { + let variant: Pull = + serde_yaml::from_value(value).map_err(serde::de::Error::custom)?; + Ok(Reader::Pull(variant)) + } + _ => Err(serde::de::Error::unknown_variant( + &key, + &["periodic", "pull"], + )), + } + } else { + Err(serde::de::Error::custom("Empty map")) + } + } +} + +#[derive(serde::Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Periodic { + pub exporter: Option, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct PeriodicExporter { + pub console: Option, + pub otlp: Option, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct PeriodicExporterConsole { + pub temporality: Option, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct PeriodicExporterOtlp { + pub protocol: Option, + pub endpoint: Option, + pub temporality: Option, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Pull { + pub exporter: Option, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct PullExporter { + pub prometheus: Option, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct PullExporterPrometheus { + pub host: String, + pub port: u16, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(deny_unknown_fields, rename_all = "lowercase")] +pub enum Temporality { + Cumulative, + Delta, +} + +#[derive(serde::Deserialize, Debug)] +pub enum Protocol { + #[serde(rename = "grpc")] + Grpc, + #[serde(rename = "http/protobuf")] + HttpBinary, + #[serde(rename = "http/json")] + HttpJson, +} From 51765dcc27f573c971c5234bf46420ead6ef38a0 Mon Sep 17 00:00:00 2001 From: Andres Borja Date: Tue, 11 Nov 2025 02:47:55 +0000 Subject: [PATCH 2/7] Improve test coverage --- .../examples/console/Cargo.toml | 1 - .../examples/console/Cargo.toml | 2 - opentelemetry-config/src/configurators.rs | 15 ++++ .../reader_configurator.rs | 2 +- opentelemetry-config/src/lib.rs | 81 ++++++++++++++++++- .../src/model/metrics/reader.rs | 62 ++++++++++++++ 6 files changed, 157 insertions(+), 6 deletions(-) diff --git a/opentelemetry-config-stdout/examples/console/Cargo.toml b/opentelemetry-config-stdout/examples/console/Cargo.toml index 0b642118..3256ae56 100644 --- a/opentelemetry-config-stdout/examples/console/Cargo.toml +++ b/opentelemetry-config-stdout/examples/console/Cargo.toml @@ -11,4 +11,3 @@ rust-version = "1.75.0" [dependencies] opentelemetry-config-stdout = { path = "../../" } opentelemetry-config = { path = " ../../../../../opentelemetry-config" } -opentelemetry_sdk = { version = "0.31.0" } diff --git a/opentelemetry-config/examples/console/Cargo.toml b/opentelemetry-config/examples/console/Cargo.toml index bdb716e6..14f72d20 100644 --- a/opentelemetry-config/examples/console/Cargo.toml +++ b/opentelemetry-config/examples/console/Cargo.toml @@ -10,5 +10,3 @@ rust-version = "1.75.0" [dependencies] opentelemetry-config = { path = "../../"} -opentelemetry_sdk = { version = "0.31.0", features = ["experimental_metrics_custom_reader"] } -opentelemetry-stdout = { version = "0.31.0" } diff --git a/opentelemetry-config/src/configurators.rs b/opentelemetry-config/src/configurators.rs index f821c867..0102da75 100644 --- a/opentelemetry-config/src/configurators.rs +++ b/opentelemetry-config/src/configurators.rs @@ -181,6 +181,7 @@ mod tests { service.name: "test-service" service.version: "1.0.0" replica.count: 3 + cores: 4.5 development: true "#; @@ -208,4 +209,18 @@ mod tests { .unwrap(); assert_eq!(configurator.get_call_count(), 1); } + + #[test] + fn test_telemetry_configurator_default() { + let telemetry_configurator = TelemetryConfigurator::default(); + let configurator_manager = ConfiguratorManager::default(); + let telemetry = Telemetry { + resource: HashMap::new(), + metrics: None, + }; + let providers = telemetry_configurator + .configure(&configurator_manager, &telemetry) + .unwrap(); + assert!(providers.meter_provider.is_none()); + } } diff --git a/opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs b/opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs index 42884b1b..3f073527 100644 --- a/opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs +++ b/opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs @@ -268,7 +268,7 @@ mod tests { _ = configurator .configure( - &metrics_configurator_manager, + metrics_configurator_manager, meter_provider_builder, &config, ) diff --git a/opentelemetry-config/src/lib.rs b/opentelemetry-config/src/lib.rs index 24f07c42..2d697731 100644 --- a/opentelemetry-config/src/lib.rs +++ b/opentelemetry-config/src/lib.rs @@ -207,12 +207,28 @@ impl Display for ConfiguratorError { #[cfg(test)] mod tests { + use std::sync::atomic::AtomicI16; + use super::*; #[test] fn test_register_periodic_exporter_configurator() { // Arrange - struct MockPeriodicExporterConfigurator {} + struct MockPeriodicExporterConfigurator { + call_count: AtomicI16, + } + + impl MockPeriodicExporterConfigurator { + fn new() -> Self { + Self { + call_count: AtomicI16::new(0), + } + } + + pub fn get_call_count(&self) -> i16 { + self.call_count.load(std::sync::atomic::Ordering::SeqCst) + } + } impl MetricsReaderPeriodicExporterConfigurator for MockPeriodicExporterConfigurator { fn configure( @@ -220,6 +236,8 @@ mod tests { meter_provider_builder: MeterProviderBuilder, _config: &dyn std::any::Any, ) -> MeterProviderBuilder { + self.call_count + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); meter_provider_builder } @@ -228,7 +246,7 @@ mod tests { } } - let mock_configurator = Box::new(MockPeriodicExporterConfigurator {}); + let mock_configurator = Box::new(MockPeriodicExporterConfigurator::new()); let mut configurator_manager = ConfiguratorManager::new(); // Act @@ -242,5 +260,64 @@ mod tests { .metrics() .readers_periodic_exporters .contains_key(&type_name)); + + let configurator_option = configurator_manager + .metrics() + .readers_periodic_exporter::(); + if let Some(configurator) = configurator_option { + configurator.configure( + MeterProviderBuilder::default(), + &PeriodicExporterConsole { temporality: None }, + ); + let configurator_cast = configurator + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(configurator_cast.get_call_count(), 1); + } else { + panic!("Configurator not found"); + } + } + + #[test] + fn test_configurator_manager_default() { + let configurator_manager = ConfiguratorManager::default(); + assert!(configurator_manager + .metrics() + .readers_periodic_exporters + .is_empty()); + } + + #[test] + fn test_metrics_configurator_manager_default() { + let metrics_configurator_manager = MetricsConfiguratorManager::default(); + assert!(metrics_configurator_manager + .readers_periodic_exporters + .is_empty()); + } + + #[test] + fn test_telemetry_providers_default() { + let telemetry_providers = TelemetryProviders::default(); + assert!(telemetry_providers.meter_provider.is_none()); + assert!(telemetry_providers.traces_provider.is_none()); + assert!(telemetry_providers.logs_provider.is_none()); + } + + #[test] + fn test_telemetry_providers_with_methods() { + let meter_provider = SdkMeterProvider::builder().build(); + let traces_provider = SdkTracerProvider::builder().build(); + let logs_provider = SdkLoggerProvider::builder().build(); + + let telemetry_providers = TelemetryProviders { + meter_provider: Some(meter_provider), + traces_provider: Some(traces_provider), + logs_provider: Some(logs_provider), + }; + + assert!(telemetry_providers.meter_provider().is_some()); + assert!(telemetry_providers.traces_provider().is_some()); + assert!(telemetry_providers.logs_provider().is_some()); } } diff --git a/opentelemetry-config/src/model/metrics/reader.rs b/opentelemetry-config/src/model/metrics/reader.rs index f8fc05a2..a6d8b11f 100644 --- a/opentelemetry-config/src/model/metrics/reader.rs +++ b/opentelemetry-config/src/model/metrics/reader.rs @@ -107,3 +107,65 @@ pub enum Protocol { #[serde(rename = "http/json")] HttpJson, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_periodic_reader() { + let yaml_data = r#" + periodic: + exporter: + console: + temporality: cumulative + "#; + let reader: Reader = serde_yaml::from_str(yaml_data).unwrap(); + match reader { + Reader::Periodic(periodic) => { + assert!(periodic.exporter.is_some()); + let exporter = periodic.exporter.unwrap(); + assert!(exporter.console.is_some()); + let console = exporter.console.unwrap(); + match console.temporality { + Some(Temporality::Cumulative) => {} + _ => panic!("Expected Cumulative temporality"), + } + } + _ => panic!("Expected Periodic reader"), + } + } + + #[test] + fn test_deserialize_pull_reader() { + let yaml_data = r#" + pull: + exporter: + prometheus: + host: "localhost" + port: 9090 + "#; + let reader: Reader = serde_yaml::from_str(yaml_data).unwrap(); + match reader { + Reader::Pull(pull) => { + assert!(pull.exporter.is_some()); + let exporter = pull.exporter.unwrap(); + assert!(exporter.prometheus.is_some()); + let prometheus = exporter.prometheus.unwrap(); + assert_eq!(prometheus.host, "localhost"); + assert_eq!(prometheus.port, 9090); + } + _ => panic!("Expected Pull reader"), + } + } + + #[test] + fn test_deserialize_invalid_reader() { + let yaml_data = r#" + unknown: + some_field: value + "#; + let result: Result = serde_yaml::from_str(yaml_data); + assert!(result.is_err()); + } +} From 1d84ea6f8b2f5118152b9540fd05ebb1f5be5849 Mon Sep 17 00:00:00 2001 From: Andres Borja Date: Tue, 11 Nov 2025 18:14:49 +0000 Subject: [PATCH 3/7] Refactor configurators to providers --- opentelemetry-config-stdout/Cargo.toml | 3 + opentelemetry-config-stdout/README.md | 25 +- .../examples/console/src/main.rs | 58 +-- opentelemetry-config-stdout/src/lib.rs | 59 +-- opentelemetry-config/Cargo.toml | 3 + opentelemetry-config/README.md | 63 +-- .../examples/console/Cargo.toml | 1 + .../examples/console/src/main.rs | 70 ++-- .../src/configurators/metrics_configurator.rs | 49 --- .../reader_configurator.rs | 364 ------------------ opentelemetry-config/src/lib.rs | 152 +++----- .../src/{configurators.rs => providers.rs} | 113 +++--- .../src/providers/metrics_provider.rs | 47 +++ .../metrics_provider/reader_provider.rs | 344 +++++++++++++++++ 14 files changed, 666 insertions(+), 685 deletions(-) delete mode 100644 opentelemetry-config/src/configurators/metrics_configurator.rs delete mode 100644 opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs rename opentelemetry-config/src/{configurators.rs => providers.rs} (59%) create mode 100644 opentelemetry-config/src/providers/metrics_provider.rs create mode 100644 opentelemetry-config/src/providers/metrics_provider/reader_provider.rs diff --git a/opentelemetry-config-stdout/Cargo.toml b/opentelemetry-config-stdout/Cargo.toml index 97bf9d79..cc10948f 100644 --- a/opentelemetry-config-stdout/Cargo.toml +++ b/opentelemetry-config-stdout/Cargo.toml @@ -10,3 +10,6 @@ rust-version = "1.75.0" opentelemetry-config = { path = "../opentelemetry-config" } opentelemetry_sdk = { version = "0.31.0" } opentelemetry-stdout = { version = "0.31.0" } + +[lints] +workspace = true diff --git a/opentelemetry-config-stdout/README.md b/opentelemetry-config-stdout/README.md index 6a99ea74..32f1ec7e 100644 --- a/opentelemetry-config-stdout/README.md +++ b/opentelemetry-config-stdout/README.md @@ -17,7 +17,7 @@ This crate provides a declarative configuration extension for OpenTelemetry that - Console/stdout metrics exporter configuration via YAML - Support for both Delta and Cumulative temporality - Integration with OpenTelemetry declarative configuration -- Simple registration API for the configurator +- Simple registration API for declarative configuration ## Installation @@ -50,26 +50,27 @@ resource: ### 2. Load and Apply Configuration ```rust -use opentelemetry_config::{ConfiguratorManager, TelemetryConfigurator}; -use opentelemetry_config_stdout::ConsolePeriodicExporterConfigurator; +use opentelemetry_config::{ConfigurationProvidersRegistry, TelemetryProvider}; +use opentelemetry_config_stdout::ConsolePeriodicExporterProvider; fn main() -> Result<(), Box> { - // Create configurator manager and register stdout exporter - let mut configurator_manager = ConfiguratorManager::new(); - ConsolePeriodicExporterConfigurator::register_into(&mut configurator_manager); + // Create configuration registry and register stdout exporter + let mut registry = ConfigurationProvidersRegistry::new(); + ConsolePeriodicExporterRegistry::register_into(&mut registry); // Load configuration from YAML file - let telemetry_configurator = TelemetryConfigurator::new(); - let providers = telemetry_configurator - .configure_from_yaml_file(&configurator_manager, "otel-config.yaml")?; + let telemetry_provider = TelemetryProvider::new(); + let providers = telemetry_provider + .configure_from_yaml_file(®istry, "otel-config.yaml")?; // Use the configured providers if let Some(meter_provider) = providers.meter_provider() { // Your application code here + + // Shutdown the created meter provider. + meter_provider.shutdown()?; } - // Shutdown all providers - providers.shutdown()?; Ok(()) } ``` @@ -80,7 +81,7 @@ fn main() -> Result<(), Box> { See the [examples/console](examples/console) directory for a complete working example that demonstrates: -- Setting up a console exporter configurator +- Setting up a console exporter provider - Loading configuration from a YAML file - Configuring a meter provider - Proper shutdown handling diff --git a/opentelemetry-config-stdout/examples/console/src/main.rs b/opentelemetry-config-stdout/examples/console/src/main.rs index b67d2023..2a5fd523 100644 --- a/opentelemetry-config-stdout/examples/console/src/main.rs +++ b/opentelemetry-config-stdout/examples/console/src/main.rs @@ -3,8 +3,8 @@ //! This example demonstrates how to configure OpenTelemetry Metrics //! using the OpenTelemetry Config crate with a Console Exporter. -use opentelemetry_config::{configurators::TelemetryConfigurator, ConfiguratorManager}; -use opentelemetry_config_stdout::ConsolePeriodicExporterConfigurator; +use opentelemetry_config::{providers::TelemetryProvider, ConfigurationProvidersRegistry}; +use opentelemetry_config_stdout::ConsolePeriodicExporterProvider; use std::env; @@ -23,30 +23,34 @@ pub fn main() -> Result<(), Box> { } let config_file = &args[2]; - // Setup configurator manager with console exporter configurator - let mut configurator_manager = ConfiguratorManager::new(); - ConsolePeriodicExporterConfigurator::register_into(&mut configurator_manager); - - let telemetry_configurator = TelemetryConfigurator::new(); - let providers = telemetry_configurator - .configure_from_yaml_file(&configurator_manager, config_file)?; - - println!("Metrics configured with Console Exporter successfully."); - - println!( - "Meter provider configured: {}", - providers.meter_provider().is_some() - ); - println!( - "Logs provider configured: {}", - providers.logs_provider().is_some() - ); - println!( - "Traces provider configured: {}", - providers.traces_provider().is_some() - ); - - println!("Shutting down telemetry providers..."); - providers.shutdown()?; + // Setup configuration registry with console exporter provider. + let mut configuration_providers_registry = ConfigurationProvidersRegistry::new(); + ConsolePeriodicExporterProvider::register_into(&mut configuration_providers_registry); + + let telemetry_provider = TelemetryProvider::new(); + let providers = telemetry_provider + .provide_from_yaml_file(&configuration_providers_registry, config_file)?; + + if let Some(meter_provider) = providers.meter_provider() { + println!("Meter provider is configured. Shutting it down..."); + meter_provider.shutdown()?; + } else { + println!("No Meter Provider configured."); + } + + if let Some(logs_provider) = providers.logs_provider() { + println!("Logs provider is configured. Shutting it down..."); + logs_provider.shutdown()?; + } else { + println!("No Logs Provider configured."); + } + + if let Some(traces_provider) = providers.traces_provider() { + println!("Traces provider is configured. Shutting it down..."); + traces_provider.shutdown()?; + } else { + println!("No Traces Provider configured."); + } + Ok(()) } diff --git a/opentelemetry-config-stdout/src/lib.rs b/opentelemetry-config-stdout/src/lib.rs index bb89f09a..ca668a11 100644 --- a/opentelemetry-config-stdout/src/lib.rs +++ b/opentelemetry-config-stdout/src/lib.rs @@ -1,35 +1,37 @@ -//! # OpenTelemetry Dynamic Configurator module for Stdout (Console) exporter +//! # OpenTelemetry declarative configuration module for Stdout (Console) exporter //! -//! This module provides a configurator for OpenTelemetry Metrics +//! This module implements a provider for OpenTelemetry Metrics //! that enables exporting metrics to the console (stdout) using //! the OpenTelemetry Config crate. use opentelemetry_config::{ - model::metrics::reader::PeriodicExporterConsole, MetricsReaderPeriodicExporterConfigurator, + model::metrics::reader::PeriodicExporterConsole, MetricsReaderPeriodicExporterProvider, }; use opentelemetry_sdk::metrics::MeterProviderBuilder; #[derive(Clone)] -pub struct ConsolePeriodicExporterConfigurator {} +pub struct ConsolePeriodicExporterProvider {} -impl ConsolePeriodicExporterConfigurator { +impl ConsolePeriodicExporterProvider { pub fn new() -> Self { Self {} } - pub fn register_into(configurators_manager: &mut opentelemetry_config::ConfiguratorManager) { - let configurator = ConsolePeriodicExporterConfigurator::new(); - configurators_manager + pub fn register_into( + configuration_registry: &mut opentelemetry_config::ConfigurationProvidersRegistry, + ) { + let provider = ConsolePeriodicExporterProvider::new(); + configuration_registry .metrics_mut() - .register_periodic_exporter_configurator::(Box::new( - configurator.clone(), + .register_periodic_exporter_provider::(Box::new( + provider.clone(), )); - // TODO: Add logs and traces configurator registration. + // TODO: Add logs and traces providers registration. } } -impl MetricsReaderPeriodicExporterConfigurator for ConsolePeriodicExporterConfigurator { - fn configure( +impl MetricsReaderPeriodicExporterProvider for ConsolePeriodicExporterProvider { + fn provide( &self, mut meter_provider_builder: MeterProviderBuilder, config: &dyn std::any::Any, @@ -63,7 +65,7 @@ impl MetricsReaderPeriodicExporterConfigurator for ConsolePeriodicExporterConfig } } -impl Default for ConsolePeriodicExporterConfigurator { +impl Default for ConsolePeriodicExporterProvider { fn default() -> Self { Self::new() } @@ -74,31 +76,32 @@ mod tests { use super::*; #[test] - fn test_console_configurator_registration() { + fn test_console_provider_registration() { // Arrange - let mut configurator_manager = opentelemetry_config::ConfiguratorManager::new(); + let mut configuration_registry = + opentelemetry_config::ConfigurationProvidersRegistry::new(); // Act - ConsolePeriodicExporterConfigurator::register_into(&mut configurator_manager); + ConsolePeriodicExporterProvider::register_into(&mut configuration_registry); - let configurators_option = configurator_manager + let provider_option = configuration_registry .metrics() .readers_periodic_exporter::(); // Assert - assert!(configurators_option.is_some()); + assert!(provider_option.is_some()); } #[test] - fn test_console_configurator_configure_temporality_minimal() { + fn test_console_provider_configure_temporality_minimal() { // Arrange - let configurator = ConsolePeriodicExporterConfigurator::new(); + let provider = ConsolePeriodicExporterProvider::new(); let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); let config = PeriodicExporterConsole { temporality: None }; // Act - let configured_builder = configurator.configure(meter_provider_builder, &config); + let configured_builder = provider.provide(meter_provider_builder, &config); // Assert // Since the MeterProviderBuilder does not expose its internal state, @@ -110,9 +113,9 @@ mod tests { } #[test] - fn test_console_configurator_configure_temporality_delta() { + fn test_console_provider_configure_temporality_delta() { // Arrange - let configurator = ConsolePeriodicExporterConfigurator::new(); + let provider = ConsolePeriodicExporterProvider::new(); let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); let config = PeriodicExporterConsole { @@ -120,7 +123,7 @@ mod tests { }; // Act - let configured_builder = configurator.configure(meter_provider_builder, &config); + let configured_builder = provider.provide(meter_provider_builder, &config); // Assert // Since the MeterProviderBuilder does not expose its internal state, @@ -132,9 +135,9 @@ mod tests { } #[test] - fn test_console_configurator_configure_temporality_cumulative() { + fn test_console_provider_configure_temporality_cumulative() { // Arrange - let configurator = ConsolePeriodicExporterConfigurator::new(); + let provider = ConsolePeriodicExporterProvider::new(); let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); let config = PeriodicExporterConsole { @@ -144,7 +147,7 @@ mod tests { }; // Act - let configured_builder = configurator.configure(meter_provider_builder, &config); + let configured_builder = provider.provide(meter_provider_builder, &config); // Assert // Since the MeterProviderBuilder does not expose its internal state, diff --git a/opentelemetry-config/Cargo.toml b/opentelemetry-config/Cargo.toml index 9a80b63c..e6aca81a 100644 --- a/opentelemetry-config/Cargo.toml +++ b/opentelemetry-config/Cargo.toml @@ -23,3 +23,6 @@ opentelemetry = { version = "0.31.0" } opentelemetry_sdk = { version = "0.31.0", features = ["experimental_metrics_custom_reader"] } serde = { version = "1.0", features = ["derive"] } serde_yaml = { version = "0.9.34" } + +[lints] +workspace = true diff --git a/opentelemetry-config/README.md b/opentelemetry-config/README.md index 97fd49c0..de494272 100644 --- a/opentelemetry-config/README.md +++ b/opentelemetry-config/README.md @@ -12,10 +12,12 @@ Declarative configuration for applications instrumented with [`OpenTelemetry`]. This crate provides a declarative, YAML-based configuration approach for the OpenTelemetry Rust SDK. Instead of programmatically building telemetry providers with code, you can define your OpenTelemetry configuration in YAML files and load them at runtime. +The configuration model is aligned with the [OpenTelemetry Configuration Schema](https://github.com/open-telemetry/opentelemetry-configuration), following the standard defined in the [kitchen-sink.yaml](https://github.com/open-telemetry/opentelemetry-configuration/blob/main/examples/kitchen-sink.yaml) example. This ensures compatibility and consistency with OpenTelemetry implementations across different languages and platforms. + ### Features - **Declarative Configuration**: Define metrics, traces, and logs configuration in YAML -- **Extensible Architecture**: Register custom configurators for different exporters +- **Extensible Architecture**: Register custom providers for different exporters - **Type-Safe**: Strongly typed configuration models with serde deserialization - **Multiple Exporters**: Support for Console, OTLP, and custom exporters - **Resource Attributes**: Configure resource attributes for all telemetry signals @@ -52,26 +54,27 @@ resource: ```rust fn main() -> Result<(), Box> { - // Create configurator manager and register exporters - let mut configurator_manager = ConfiguratorManager::new(); - configurator_manager + // Create a configuration registry and register exporters + let mut registry = ConfigurationProvidersRegistry::new(); + registry .metrics_mut() - .register_periodic_exporter_configurator::( - Box::new(ConsoleExporterConfigurator) + .register_periodic_exporter_provider::( + Box::new(ConsoleExporterProvider) ); // Load configuration from YAML file - let telemetry_configurator = TelemetryConfigurator::new(); - let providers = telemetry_configurator - .configure_from_yaml_file(&configurator_manager, "otel-config.yaml")?; + let telemetry_provider = TelemetryProvider::new(); + let providers = telemetry_provider + .provide_from_yaml_file(®istry, "otel-config.yaml")?; // Use the configured providers if let Some(meter_provider) = providers.meter_provider() { // Your application code here + + // Shutdown the meter provider + meter_provider.shutdown()?; } - // Shutdown all providers - providers.shutdown()?; Ok(()) } ``` @@ -80,10 +83,24 @@ fn main() -> Result<(), Box> { ### Core Components -- **`ConfiguratorManager`**: Central registry for exporter configurators -- **`TelemetryConfigurator`**: Orchestrates the configuration process from YAML to providers +- **`ConfigurationProvidersRegistry`**: Central registry for configuration providers +- **`TelemetryProvider`**: Orchestrates the configuration process from YAML to providers - **`TelemetryProviders`**: Holds configured meter, tracer, and logger providers -- **`MetricsReaderPeriodicExporterConfigurator`**: Trait for implementing custom metric exporters +- **`MetricsReaderPeriodicExporterProvider`**: Trait for implementing custom metric exporters + +### Design Pattern + +This crate follows a **decoupled implementation pattern**: + +- **Centralized Configuration Model**: The configuration schema (YAML structure and data models) is defined and maintained centrally in this crate, ensuring alignment with the OpenTelemetry Configuration Standard +- **Decoupled Implementations**: Actual exporter implementations live in external crates, allowing the community to contribute custom exporters without modifying the core configuration model +- **Community Control**: By keeping the configuration model centralized and standardized, the community maintains consistency across all implementations while enabling extensibility + +This design enables: +- **Standard Compliance**: All configurations follow the official OpenTelemetry schema +- **Easy Extension**: Contributors can add new exporters by implementing traits in their own crates +- **Version Independence**: Exporter implementations can evolve independently from the configuration schema +- **Flexibility**: Users can mix official and custom exporters using the same configuration format ### Configuration Model @@ -100,7 +117,7 @@ The configuration is structured around the `Telemetry` model which includes: See the [examples/console](examples/console) directory for a complete working example that demonstrates: -- Setting up a console exporter configurator +- Setting up a console exporter provider - Loading configuration from a YAML file - Configuring a meter provider - Proper shutdown handling @@ -153,16 +170,16 @@ pub struct MyCustomExporter { } ``` -2. Implement the configurator trait: +2. Implement the provider trait: ```rust -impl MetricsReaderPeriodicExporterConfigurator for MyCustomConfigurator { - fn configure( +impl MetricsReaderPeriodicExporterProvider for MyCustomProvider { + fn provide( &self, meter_provider_builder: MeterProviderBuilder, config: &dyn std::any::Any, ) -> MeterProviderBuilder { - let config = config.downcast_ref::() + let config = config.downcast_ref::() .expect("Invalid config type"); // Build your exporter with the config @@ -176,13 +193,13 @@ impl MetricsReaderPeriodicExporterConfigurator for MyCustomConfigurator { } ``` -3. Register it with the configurator manager: +3. Register it with the ConfigurationProvidersRegistry: ```rust -configurator_manager +registry .metrics_mut() - .register_periodic_exporter_configurator::( - Box::new(MyCustomConfigurator) + .register_periodic_exporter_provider::( + Box::new(MyCustomProvider) ); ``` diff --git a/opentelemetry-config/examples/console/Cargo.toml b/opentelemetry-config/examples/console/Cargo.toml index 14f72d20..c3889cb9 100644 --- a/opentelemetry-config/examples/console/Cargo.toml +++ b/opentelemetry-config/examples/console/Cargo.toml @@ -10,3 +10,4 @@ rust-version = "1.75.0" [dependencies] opentelemetry-config = { path = "../../"} +opentelemetry_sdk = { version = "0.31.0" } \ No newline at end of file diff --git a/opentelemetry-config/examples/console/src/main.rs b/opentelemetry-config/examples/console/src/main.rs index b9e045ec..5a16e0bc 100644 --- a/opentelemetry-config/examples/console/src/main.rs +++ b/opentelemetry-config/examples/console/src/main.rs @@ -5,8 +5,8 @@ //! It is helpful to implement and test custom exporters. use opentelemetry_config::{ - configurators::TelemetryConfigurator, model::metrics::reader::PeriodicExporterConsole, - ConfiguratorManager, MetricsReaderPeriodicExporterConfigurator, + model::metrics::reader::PeriodicExporterConsole, providers::TelemetryProvider, + ConfigurationProvidersRegistry, MetricsReaderPeriodicExporterProvider, }; use opentelemetry_sdk::metrics::Temporality; use opentelemetry_sdk::{ @@ -32,43 +32,47 @@ pub fn main() -> Result<(), Box> { } let config_file = &args[2]; - // Setup configurator manager with console exporter configurator - let configurator = Box::new(MockPeriodicExporterConfigurator::new()); - let mut configurator_manager = ConfiguratorManager::new(); + // Setup configuration registry with console exporter provider. + let provider = Box::new(MockPeriodicExporterProvider::new()); + let mut registry = ConfigurationProvidersRegistry::new(); - // Register the console exporter configurator for the specific exporter type. - configurator_manager + // Register the console exporter provider for the specific exporter type. + registry .metrics_mut() - .register_periodic_exporter_configurator::(configurator); + .register_periodic_exporter_provider::(provider); - let telemetry_configurator = TelemetryConfigurator::new(); - let providers = telemetry_configurator - .configure_from_yaml_file(&configurator_manager, config_file) + let telemetry_provider = TelemetryProvider::new(); + let providers = telemetry_provider + .provide_from_yaml_file(®istry, config_file) .unwrap(); - assert!(providers.meter_provider().is_some()); - println!("Metrics configured with Console Exporter successfully."); - - println!( - "Meter provider configured: {}", - providers.meter_provider().is_some() - ); - println!( - "Logs provider configured: {}", - providers.logs_provider().is_some() - ); - println!( - "Traces provider configured: {}", - providers.traces_provider().is_some() - ); - - println!("Shutting down telemetry providers..."); - providers.shutdown()?; + + if let Some(meter_provider) = providers.meter_provider() { + println!("Meter provider configured successfully. Shutting it down..."); + meter_provider.shutdown()?; + } else { + println!("No Meter provider configured."); + } + + if let Some(logs_provider) = providers.logs_provider() { + println!("Logs provider configured successfully. Shutting it down..."); + logs_provider.shutdown()?; + } else { + println!("No Logs provider configured."); + } + + if let Some(traces_provider) = providers.traces_provider() { + println!("Traces provider configured successfully. Shutting it down..."); + traces_provider.shutdown()?; + } else { + println!("No Traces provider configured."); + } + Ok(()) } -pub struct MockPeriodicExporterConfigurator {} +pub struct MockPeriodicExporterProvider {} -impl MockPeriodicExporterConfigurator { +impl MockPeriodicExporterProvider { fn new() -> Self { Self {} } @@ -127,8 +131,8 @@ impl PushMetricExporter for MockConsoleExporter { } } -impl MetricsReaderPeriodicExporterConfigurator for MockPeriodicExporterConfigurator { - fn configure( +impl MetricsReaderPeriodicExporterProvider for MockPeriodicExporterProvider { + fn provide( &self, mut meter_provider_builder: MeterProviderBuilder, config: &dyn std::any::Any, diff --git a/opentelemetry-config/src/configurators/metrics_configurator.rs b/opentelemetry-config/src/configurators/metrics_configurator.rs deleted file mode 100644 index 9317b554..00000000 --- a/opentelemetry-config/src/configurators/metrics_configurator.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Configurator for Metrics telemetry -//! -//! This module provides functionality to configure Metrics telemetry -//! in OpenTelemetry SDKs using declarative YAML configurations. - -pub mod reader_configurator; - -use opentelemetry_sdk::metrics::MeterProviderBuilder; - -use crate::{ConfiguratorError, MetricsConfiguratorManager}; - -use crate::configurators::metrics_configurator::reader_configurator::ReaderConfigurator; - -/// Configurator for Metrics telemetry -pub struct MetricsConfigurator { - reader_configurator: ReaderConfigurator, -} - -impl MetricsConfigurator { - pub fn new() -> Self { - MetricsConfigurator { - reader_configurator: ReaderConfigurator::new(), - } - } - - /// Configures the Metrics provider based on the provided configuration - pub fn configure( - &self, - metrics_configurator_manager: &MetricsConfiguratorManager, - mut meter_provider_builder: MeterProviderBuilder, - config: &crate::model::metrics::Metrics, - ) -> Result { - for reader in &config.readers { - meter_provider_builder = self.reader_configurator.configure( - metrics_configurator_manager, - meter_provider_builder, - reader, - )?; - } - - Ok(meter_provider_builder) - } -} - -impl Default for MetricsConfigurator { - fn default() -> Self { - Self::new() - } -} diff --git a/opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs b/opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs deleted file mode 100644 index 3f073527..00000000 --- a/opentelemetry-config/src/configurators/metrics_configurator/reader_configurator.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! # Metrics reader configurator module. -//! -//! This module provides configurators for setting up metrics readers -//! in OpenTelemetry SDKs using declarative YAML configurations. - -use opentelemetry_sdk::metrics::MeterProviderBuilder; - -use crate::{ - model::metrics::reader::{PeriodicExporterConsole, PeriodicExporterOtlp, Reader}, - ConfiguratorError, MetricsConfiguratorManager, -}; - -/// Configurator for Metrics readers -pub struct ReaderConfigurator { - periodic_reader_configurator: PeriodicReaderConfigurator, -} - -impl ReaderConfigurator { - pub fn new() -> Self { - ReaderConfigurator { - periodic_reader_configurator: PeriodicReaderConfigurator::new(), - } - } - /// Configures a metrics reader based on the provided configuration - pub fn configure( - &self, - metrics_configurator_manager: &MetricsConfiguratorManager, - mut meter_provider_builder: MeterProviderBuilder, - config: &Reader, - ) -> Result { - match config { - crate::model::metrics::reader::Reader::Periodic(periodic_config) => { - meter_provider_builder = self.periodic_reader_configurator.configure( - metrics_configurator_manager, - meter_provider_builder, - periodic_config, - )?; - } - crate::model::metrics::reader::Reader::Pull(_pull_config) => { - // TODO: Implement pull reader configuration - } - } - Ok(meter_provider_builder) - } -} - -impl Default for ReaderConfigurator { - fn default() -> Self { - Self::new() - } -} - -/// Periodic reader configurator -pub struct PeriodicReaderConfigurator { - periodic_exporter_configurator: PeriodicExporterConfigurator, -} - -impl PeriodicReaderConfigurator { - /// Creates a new PeriodicReaderConfigurator - pub fn new() -> Self { - PeriodicReaderConfigurator { - periodic_exporter_configurator: PeriodicExporterConfigurator::new(), - } - } - - /// Configures a periodic metrics reader based on the provided configuration - pub fn configure( - &self, - metrics_configurator_manager: &MetricsConfiguratorManager, - mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, - config: &crate::model::metrics::reader::Periodic, - ) -> Result { - if let Some(exporter_config) = &config.exporter { - meter_provider_builder = self.periodic_exporter_configurator.configure( - metrics_configurator_manager, - meter_provider_builder, - exporter_config, - )?; - } - - Ok(meter_provider_builder) - } -} - -impl Default for PeriodicReaderConfigurator { - fn default() -> Self { - Self::new() - } -} - -/// Periodic exporter configurator -pub struct PeriodicExporterConfigurator {} - -impl PeriodicExporterConfigurator { - /// Creates a new PeriodicExporterConfigurator - pub fn new() -> Self { - PeriodicExporterConfigurator {} - } - - /// Configures a periodic metrics exporter based on the provided configuration - pub fn configure( - &self, - metrics_configurator_manager: &MetricsConfiguratorManager, - mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, - config: &crate::model::metrics::reader::PeriodicExporter, - ) -> Result { - if let Some(console_config) = &config.console { - let configurator_option = - metrics_configurator_manager.readers_periodic_exporter::(); - if let Some(configurator) = configurator_option { - meter_provider_builder = - configurator.configure(meter_provider_builder, console_config); - } else { - return Err(ConfiguratorError::NotRegisteredConfigurator("No configurator found for PeriodicExporterConsole. Make sure it is registered as configurator.".to_string())); - } - } - - if let Some(otlp_config) = &config.otlp { - let configurator_option = - metrics_configurator_manager.readers_periodic_exporter::(); - if let Some(configurator) = configurator_option { - meter_provider_builder = - configurator.configure(meter_provider_builder, otlp_config); - } else { - return Err(ConfiguratorError::NotRegisteredConfigurator("No configurator found for PeriodicExporterOtlp. Make sure it is registered as configurator.".to_string())); - } - } - - Ok(meter_provider_builder) - } -} - -impl Default for PeriodicExporterConfigurator { - fn default() -> Self { - Self::new() - } -} - -// Pull reader configurator -pub struct PullReaderConfigurator { - pull_exporter_configurator: PullExporterConfigurator, -} - -impl PullReaderConfigurator { - /// Creates a new PullReaderConfigurator - pub fn new() -> Self { - PullReaderConfigurator { - pull_exporter_configurator: PullExporterConfigurator::new(), - } - } - - /// Configures a pull metrics reader based on the provided configuration - pub fn configure( - &self, - metrics_configurator_manager: &MetricsConfiguratorManager, - mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, - config: &crate::model::metrics::reader::Pull, - ) -> Result { - if let Some(exporter_config) = &config.exporter { - meter_provider_builder = self.pull_exporter_configurator.configure( - metrics_configurator_manager, - meter_provider_builder, - exporter_config, - )?; - } - Ok(meter_provider_builder) - } -} - -impl Default for PullReaderConfigurator { - fn default() -> Self { - Self::new() - } -} - -/// Pull exporter configurator -pub struct PullExporterConfigurator {} - -impl PullExporterConfigurator { - /// Creates a new PullExporterConfigurator - pub fn new() -> Self { - PullExporterConfigurator {} - } - - pub fn configure( - &self, - _metrics_configurator_manager: &MetricsConfiguratorManager, - meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, - config: &crate::model::metrics::reader::PullExporter, - ) -> Result { - if let Some(_prometheus_config) = &config.prometheus { - // Explicitly Prometheus exporter is not supported in this configurator. - return Err(ConfiguratorError::UnsupportedExporter( - "Prometheus exporter is not supported.".to_string(), - )); - } - Ok(meter_provider_builder) - } -} - -impl Default for PullExporterConfigurator { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - - use opentelemetry_sdk::metrics::SdkMeterProvider; - - use crate::MetricsReaderPeriodicExporterConfigurator; - - use super::*; - - struct MockMetricsReadersPeriodicExporterConsoleConfigurator {} - - impl MockMetricsReadersPeriodicExporterConsoleConfigurator { - fn new() -> Self { - MockMetricsReadersPeriodicExporterConsoleConfigurator {} - } - - fn register_into(manager: &mut crate::ConfiguratorManager) { - manager - .metrics_mut() - .register_periodic_exporter_configurator::(Box::new( - Self::new(), - )); - } - } - - impl MetricsReaderPeriodicExporterConfigurator - for MockMetricsReadersPeriodicExporterConsoleConfigurator - { - fn configure( - &self, - meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, - _config: &dyn std::any::Any, - ) -> opentelemetry_sdk::metrics::MeterProviderBuilder { - // Mock implementation: just return the builder as is - meter_provider_builder - } - - fn as_any(&self) -> &dyn std::any::Any { - todo!() - } - } - - #[test] - fn test_reader_configurator_configure() { - let configurator = ReaderConfigurator::new(); - let mut configurator_manager = crate::ConfiguratorManager::new(); - MockMetricsReadersPeriodicExporterConsoleConfigurator::register_into( - &mut configurator_manager, - ); - let meter_provider_builder = SdkMeterProvider::builder(); - - let config = crate::model::metrics::reader::Reader::Periodic( - crate::model::metrics::reader::Periodic { - exporter: Some(crate::model::metrics::reader::PeriodicExporter { - console: Some(PeriodicExporterConsole { temporality: None }), - otlp: None, - }), - }, - ); - - let metrics_configurator_manager = configurator_manager.metrics(); - - _ = configurator - .configure( - metrics_configurator_manager, - meter_provider_builder, - &config, - ) - .unwrap(); - } - - #[test] - fn test_reader_configurator_configure_console_configurator_not_registered() { - let configurator = ReaderConfigurator::new(); - let metrics_configurator_manager = MetricsConfiguratorManager::new(); - let meter_provider_builder = SdkMeterProvider::builder(); - - let config = crate::model::metrics::reader::Reader::Periodic( - crate::model::metrics::reader::Periodic { - exporter: Some(crate::model::metrics::reader::PeriodicExporter { - console: Some(PeriodicExporterConsole { temporality: None }), - otlp: None, - }), - }, - ); - - let result = configurator.configure( - &metrics_configurator_manager, - meter_provider_builder, - &config, - ); - if let Err(e) = result { - assert!(e - .to_string() - .contains("No configurator found for PeriodicExporterConsole")); - } else { - panic!("Expected error due to missing configurator, but got Ok"); - } - } - - #[test] - fn test_reader_configurator_configure_otlp_configurator_not_registered() { - let configurator = ReaderConfigurator::new(); - let metrics_configurator_manager = MetricsConfiguratorManager::new(); - let meter_provider_builder = SdkMeterProvider::builder(); - - let config = crate::model::metrics::reader::Reader::Periodic( - crate::model::metrics::reader::Periodic { - exporter: Some(crate::model::metrics::reader::PeriodicExporter { - console: None, - otlp: Some(PeriodicExporterOtlp { - endpoint: None, - protocol: None, - temporality: None, - }), - }), - }, - ); - - let result = configurator.configure( - &metrics_configurator_manager, - meter_provider_builder, - &config, - ); - if let Err(e) = result { - assert!(e - .to_string() - .contains("No configurator found for PeriodicExporterOtlp")); - } else { - panic!("Expected error due to missing configurator, but got Ok"); - } - } - - #[test] - fn test_periodic_exporter_configurator_configure_unsupported_exporter() { - let configurator = PullExporterConfigurator::new(); - let metrics_configurator_manager = MetricsConfiguratorManager::new(); - let meter_provider_builder = SdkMeterProvider::builder(); - let config = crate::model::metrics::reader::PullExporter { - prometheus: Some(crate::model::metrics::reader::PullExporterPrometheus { - host: "localhost".to_string(), - port: 9090, - }), - }; - let result = configurator.configure( - &metrics_configurator_manager, - meter_provider_builder, - &config, - ); - if let Err(e) = result { - assert!(e - .to_string() - .contains("Prometheus exporter is not supported.")); - } else { - panic!("Expected error due to unsupported exporter, but got Ok"); - } - } -} diff --git a/opentelemetry-config/src/lib.rs b/opentelemetry-config/src/lib.rs index 2d697731..cc7aaa3f 100644 --- a/opentelemetry-config/src/lib.rs +++ b/opentelemetry-config/src/lib.rs @@ -12,69 +12,68 @@ use std::{ }; use opentelemetry_sdk::{ - error::OTelSdkResult, logs::SdkLoggerProvider, metrics::{MeterProviderBuilder, SdkMeterProvider}, trace::SdkTracerProvider, }; -use crate::model::metrics::reader::{PeriodicExporterConsole, PeriodicExporterOtlp}; - -pub mod configurators; pub mod model; +pub mod providers; -pub struct ConfiguratorManager { - metrics: MetricsConfiguratorManager, +/// Registry for different configuration providers. +pub struct ConfigurationProvidersRegistry { + metrics: MetricsProvidersRegistry, } -impl ConfiguratorManager { +impl ConfigurationProvidersRegistry { pub fn new() -> Self { Self { - metrics: MetricsConfiguratorManager::new(), + metrics: MetricsProvidersRegistry::new(), } } - pub fn metrics_mut(&mut self) -> &mut MetricsConfiguratorManager { + pub fn metrics_mut(&mut self) -> &mut MetricsProvidersRegistry { &mut self.metrics } - pub fn metrics(&self) -> &MetricsConfiguratorManager { + pub fn metrics(&self) -> &MetricsProvidersRegistry { &self.metrics } } -impl Default for ConfiguratorManager { +impl Default for ConfigurationProvidersRegistry { fn default() -> Self { Self::new() } } -pub struct MetricsConfiguratorManager { - readers_periodic_exporters: HashMap>, +/// Registry for metrics configuration providers. +pub struct MetricsProvidersRegistry { + readers_periodic_exporters: HashMap>, } -impl MetricsConfiguratorManager { +impl MetricsProvidersRegistry { pub fn new() -> Self { Self { readers_periodic_exporters: HashMap::new(), } } - pub fn register_periodic_exporter_configurator( + pub fn register_periodic_exporter_provider( &mut self, - configurator: Box, + provider: Box, ) { let name: String = type_name::().to_string(); self.readers_periodic_exporters.insert( name, - configurator as Box, + provider as Box, ); } pub fn readers_periodic_exporter( &self, //type_name: &str, - ) -> Option<&dyn MetricsReaderPeriodicExporterConfigurator> { + ) -> Option<&dyn MetricsReaderPeriodicExporterProvider> { let type_name = type_name::().to_string(); self.readers_periodic_exporters .get(&type_name) @@ -82,34 +81,15 @@ impl MetricsConfiguratorManager { } } -impl Default for MetricsConfiguratorManager { +impl Default for MetricsProvidersRegistry { fn default() -> Self { Self::new() } } -/// Trait for configuring Console Exporter for Periodic Metrics Reader -pub trait MetricsReadersPeriodicExporterConsoleConfigurator { - fn configure( - &self, - meter_provider_builder: MeterProviderBuilder, - config: &PeriodicExporterConsole, - ) -> MeterProviderBuilder; - - fn as_any(&self) -> &dyn std::any::Any; -} - -/// Trait for configuring OTLP Exporter for Periodic Metrics Reader -pub trait MetricsReadersPeriodicExporterOtlpConfigurator { - fn configure( - &self, - meter_provider_builder: MeterProviderBuilder, - config: &PeriodicExporterOtlp, - ) -> MeterProviderBuilder; -} - -pub trait MetricsReaderPeriodicExporterConfigurator { - fn configure( +/// Trait for providing metrics reader periodic exporter configurations. +pub trait MetricsReaderPeriodicExporterProvider { + fn provide( &self, meter_provider_builder: MeterProviderBuilder, config: &dyn std::any::Any, @@ -159,47 +139,36 @@ impl TelemetryProviders { pub fn logs_provider(&self) -> Option<&SdkLoggerProvider> { self.logs_provider.as_ref() } - - pub fn shutdown(self) -> OTelSdkResult { - if let Some(meter_provider) = self.meter_provider { - meter_provider.shutdown()?; - } - if let Some(traces_provider) = self.traces_provider { - traces_provider.shutdown()?; - } - if let Some(logs_provider) = self.logs_provider { - logs_provider.shutdown()?; - } - Ok(()) - } } +/// Default implementation for TelemetryProviders impl Default for TelemetryProviders { fn default() -> Self { Self::new() } } +/// Errors related to providers and configuration management. #[derive(Debug)] -pub enum ConfiguratorError { +pub enum ProviderError { InvalidConfiguration(String), UnsupportedExporter(String), - NotRegisteredConfigurator(String), + NotRegisteredProvider(String), } -impl error::Error for ConfiguratorError {} +impl error::Error for ProviderError {} -impl Display for ConfiguratorError { +impl Display for ProviderError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ConfiguratorError::InvalidConfiguration(details) => { + ProviderError::InvalidConfiguration(details) => { write!(f, "Invalid configuration: {}", details) } - ConfiguratorError::UnsupportedExporter(details) => { + ProviderError::UnsupportedExporter(details) => { write!(f, "Unsupported exporter: {}", details) } - ConfiguratorError::NotRegisteredConfigurator(details) => { - write!(f, "Not registered configurator: {}", details) + ProviderError::NotRegisteredProvider(details) => { + write!(f, "Not registered provider: {}", details) } } } @@ -207,37 +176,38 @@ impl Display for ConfiguratorError { #[cfg(test)] mod tests { - use std::sync::atomic::AtomicI16; + use std::cell::Cell; + + use crate::model::metrics::reader::PeriodicExporterConsole; use super::*; #[test] - fn test_register_periodic_exporter_configurator() { + fn test_register_periodic_exporter_provider() { // Arrange - struct MockPeriodicExporterConfigurator { - call_count: AtomicI16, + struct MockPeriodicExporterProvider { + call_count: Cell, } - impl MockPeriodicExporterConfigurator { + impl MockPeriodicExporterProvider { fn new() -> Self { Self { - call_count: AtomicI16::new(0), + call_count: Cell::new(0), } } pub fn get_call_count(&self) -> i16 { - self.call_count.load(std::sync::atomic::Ordering::SeqCst) + self.call_count.get() } } - impl MetricsReaderPeriodicExporterConfigurator for MockPeriodicExporterConfigurator { - fn configure( + impl MetricsReaderPeriodicExporterProvider for MockPeriodicExporterProvider { + fn provide( &self, meter_provider_builder: MeterProviderBuilder, _config: &dyn std::any::Any, ) -> MeterProviderBuilder { - self.call_count - .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + self.call_count.set(self.call_count.get() + 1); meter_provider_builder } @@ -246,52 +216,52 @@ mod tests { } } - let mock_configurator = Box::new(MockPeriodicExporterConfigurator::new()); - let mut configurator_manager = ConfiguratorManager::new(); + let mock_provider = Box::new(MockPeriodicExporterProvider::new()); + let mut registry = ConfigurationProvidersRegistry::new(); // Act - configurator_manager + registry .metrics_mut() - .register_periodic_exporter_configurator::(mock_configurator); + .register_periodic_exporter_provider::(mock_provider); // Assert let type_name = type_name::().to_string(); - assert!(configurator_manager + assert!(registry .metrics() .readers_periodic_exporters .contains_key(&type_name)); - let configurator_option = configurator_manager + let provider_option = registry .metrics() .readers_periodic_exporter::(); - if let Some(configurator) = configurator_option { - configurator.configure( + if let Some(provider) = provider_option { + provider.provide( MeterProviderBuilder::default(), &PeriodicExporterConsole { temporality: None }, ); - let configurator_cast = configurator + let provider_cast = provider .as_any() - .downcast_ref::() + .downcast_ref::() .unwrap(); - assert_eq!(configurator_cast.get_call_count(), 1); + assert_eq!(provider_cast.get_call_count(), 1); } else { - panic!("Configurator not found"); + panic!("Provider not found"); } } #[test] - fn test_configurator_manager_default() { - let configurator_manager = ConfiguratorManager::default(); - assert!(configurator_manager + fn test_provider_manager_default() { + let provider_manager = ConfigurationProvidersRegistry::default(); + assert!(provider_manager .metrics() .readers_periodic_exporters .is_empty()); } #[test] - fn test_metrics_configurator_manager_default() { - let metrics_configurator_manager = MetricsConfiguratorManager::default(); - assert!(metrics_configurator_manager + fn test_metrics_provider_manager_default() { + let metrics_provider_manager = MetricsProvidersRegistry::default(); + assert!(metrics_provider_manager .readers_periodic_exporters .is_empty()); } diff --git a/opentelemetry-config/src/configurators.rs b/opentelemetry-config/src/providers.rs similarity index 59% rename from opentelemetry-config/src/configurators.rs rename to opentelemetry-config/src/providers.rs index 0102da75..c7b0986a 100644 --- a/opentelemetry-config/src/configurators.rs +++ b/opentelemetry-config/src/providers.rs @@ -1,9 +1,9 @@ -//! # Configurator objects for OpenTelemetry SDKs +//! # Provider objects for OpenTelemetry SDKs //! -//! This module provides the different element configurators to configure +//! This module provides the different element providers to configure //! OpenTelemetry SDKs using declarative YAML configurations. -pub mod metrics_configurator; +pub mod metrics_provider; use std::collections::HashMap; @@ -11,35 +11,35 @@ use opentelemetry::KeyValue; use opentelemetry_sdk::{metrics::SdkMeterProvider, Resource}; use crate::{ - configurators::metrics_configurator::MetricsConfigurator, model::Telemetry, ConfiguratorError, - ConfiguratorManager, TelemetryProviders, + model::Telemetry, providers::metrics_provider::MetricsProvider, ConfigurationProvidersRegistry, + ProviderError, TelemetryProviders, }; -/// Configurator for Telemetry object -pub struct TelemetryConfigurator { - metrics_configurator: MetricsConfigurator, +/// Provider for Telemetry object +pub struct TelemetryProvider { + metrics_provider: MetricsProvider, } -impl TelemetryConfigurator { - /// Creates a new TelemetryConfigurator +impl TelemetryProvider { + /// Creates a new TelemetryProvider pub fn new() -> Self { Self { - metrics_configurator: MetricsConfigurator::new(), + metrics_provider: MetricsProvider::new(), } } - pub fn configure( + pub fn provide( &self, - configurator_manager: &ConfiguratorManager, + configuration_registry: &ConfigurationProvidersRegistry, config: &Telemetry, - ) -> Result { + ) -> Result { let mut providers = TelemetryProviders::new(); let resource: Resource = self.as_resource(&config.resource); if let Some(metrics_config) = &config.metrics { let mut meter_provider_builder = SdkMeterProvider::builder().with_resource(resource.clone()); - meter_provider_builder = self.metrics_configurator.configure( - configurator_manager.metrics(), + meter_provider_builder = self.metrics_provider.provide( + configuration_registry.metrics(), meter_provider_builder, metrics_config, )?; @@ -52,32 +52,32 @@ impl TelemetryConfigurator { Ok(providers) } - pub fn configure_from_yaml( + pub fn provide_from_yaml( &self, - configurator_manager: &ConfiguratorManager, + configuration_registry: &ConfigurationProvidersRegistry, yaml_str: &str, - ) -> Result { + ) -> Result { let config: crate::model::Telemetry = serde_yaml::from_str(yaml_str).map_err(|e| { - ConfiguratorError::InvalidConfiguration(format!( + ProviderError::InvalidConfiguration(format!( "Failed to parse YAML configuration: {}", e )) })?; - self.configure(configurator_manager, &config) + self.provide(configuration_registry, &config) } - pub fn configure_from_yaml_file( + pub fn provide_from_yaml_file( &self, - configurator_manager: &ConfiguratorManager, + configuration_registry: &ConfigurationProvidersRegistry, file_path: &str, - ) -> Result { + ) -> Result { let yaml_str = std::fs::read_to_string(file_path).map_err(|e| { - ConfiguratorError::InvalidConfiguration(format!( + ProviderError::InvalidConfiguration(format!( "Failed to read YAML configuration file: {}", e )) })?; - self.configure_from_yaml(configurator_manager, &yaml_str) + self.provide_from_yaml(configuration_registry, &yaml_str) } /// Converts resource attributes from HashMap to Resource @@ -111,7 +111,7 @@ impl TelemetryConfigurator { } } -impl Default for TelemetryConfigurator { +impl Default for TelemetryProvider { fn default() -> Self { Self::new() } @@ -119,43 +119,40 @@ impl Default for TelemetryConfigurator { #[cfg(test)] mod tests { - use std::{any::Any, sync::atomic::AtomicU16}; + use std::{any::Any, cell::Cell}; use opentelemetry_sdk::metrics::MeterProviderBuilder; use crate::{ - model::metrics::reader::PeriodicExporterConsole, MetricsReaderPeriodicExporterConfigurator, + model::metrics::reader::PeriodicExporterConsole, MetricsReaderPeriodicExporterProvider, }; use super::*; - struct MockMetricsReadersPeriodicExporterConsoleConfigurator { - call_count: AtomicU16, + struct MockMetricsReadersPeriodicExporterConsoleProvider { + call_count: Cell, } - impl MockMetricsReadersPeriodicExporterConsoleConfigurator { + impl MockMetricsReadersPeriodicExporterConsoleProvider { fn new() -> Self { Self { - call_count: AtomicU16::new(0), + call_count: Cell::new(0), } } pub fn get_call_count(&self) -> u16 { - self.call_count.load(std::sync::atomic::Ordering::SeqCst) + self.call_count.get() } } - impl MetricsReaderPeriodicExporterConfigurator - for MockMetricsReadersPeriodicExporterConsoleConfigurator - { - fn configure( + impl MetricsReaderPeriodicExporterProvider for MockMetricsReadersPeriodicExporterConsoleProvider { + fn provide( &self, meter_provider_builder: MeterProviderBuilder, config: &(dyn Any + 'static), ) -> MeterProviderBuilder { // Mock implementation: In a real scenario, configure the console exporter here - self.call_count - .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + self.call_count.set(self.call_count.get() + 1); let config = config .downcast_ref::() .expect("Invalid config type"); @@ -185,41 +182,41 @@ mod tests { development: true "#; - let configurator = Box::new(MockMetricsReadersPeriodicExporterConsoleConfigurator::new()); + let provider = Box::new(MockMetricsReadersPeriodicExporterConsoleProvider::new()); - let mut configurator_manager = ConfiguratorManager::new(); - let metrics_configurator_manager = configurator_manager.metrics_mut(); - //metrics_configurator_manager.register_periodic_exporter_console_configurator(configurator); - metrics_configurator_manager - .register_periodic_exporter_configurator::(configurator); + let mut configuration_registry = ConfigurationProvidersRegistry::new(); + let metrics_provider_manager = configuration_registry.metrics_mut(); + //metrics_provider_manager.register_periodic_exporter_console_provider(provider); + metrics_provider_manager + .register_periodic_exporter_provider::(provider); - let telemetry_configurator = TelemetryConfigurator::new(); - let providers = telemetry_configurator - .configure_from_yaml(&configurator_manager, yaml_str) + let telemetry_provider = TelemetryProvider::new(); + let providers = telemetry_provider + .provide_from_yaml(&configuration_registry, yaml_str) .unwrap(); assert!(providers.meter_provider.is_some()); - let configurator = configurator_manager + let provider = configuration_registry .metrics() .readers_periodic_exporter::() .unwrap(); - let configurator = configurator + let provider = provider .as_any() - .downcast_ref::() + .downcast_ref::() .unwrap(); - assert_eq!(configurator.get_call_count(), 1); + assert_eq!(provider.get_call_count(), 1); } #[test] - fn test_telemetry_configurator_default() { - let telemetry_configurator = TelemetryConfigurator::default(); - let configurator_manager = ConfiguratorManager::default(); + fn test_telemetry_provider_default() { + let telemetry_provider = TelemetryProvider::default(); + let configuration_registry = ConfigurationProvidersRegistry::default(); let telemetry = Telemetry { resource: HashMap::new(), metrics: None, }; - let providers = telemetry_configurator - .configure(&configurator_manager, &telemetry) + let providers = telemetry_provider + .provide(&configuration_registry, &telemetry) .unwrap(); assert!(providers.meter_provider.is_none()); } diff --git a/opentelemetry-config/src/providers/metrics_provider.rs b/opentelemetry-config/src/providers/metrics_provider.rs new file mode 100644 index 00000000..2403b116 --- /dev/null +++ b/opentelemetry-config/src/providers/metrics_provider.rs @@ -0,0 +1,47 @@ +//! Provider for Metrics telemetry +//! +//! This module provides functionality to configure Metrics telemetry +//! in OpenTelemetry SDKs using declarative YAML configurations. + +pub mod reader_provider; + +use opentelemetry_sdk::metrics::MeterProviderBuilder; + +use crate::{MetricsProvidersRegistry, ProviderError}; + +use crate::providers::metrics_provider::reader_provider::ReaderProvider; + +/// Provider for Metrics telemetry +pub struct MetricsProvider { + reader_provider: ReaderProvider, +} + +impl MetricsProvider { + pub fn new() -> Self { + MetricsProvider { + reader_provider: ReaderProvider::new(), + } + } + + /// Provisions the Metrics provider based on the provided configuration + pub fn provide( + &self, + metrics_registry: &MetricsProvidersRegistry, + mut meter_provider_builder: MeterProviderBuilder, + config: &crate::model::metrics::Metrics, + ) -> Result { + for reader in &config.readers { + meter_provider_builder = + self.reader_provider + .provide(metrics_registry, meter_provider_builder, reader)?; + } + + Ok(meter_provider_builder) + } +} + +impl Default for MetricsProvider { + fn default() -> Self { + Self::new() + } +} diff --git a/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs b/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs new file mode 100644 index 00000000..67ed9192 --- /dev/null +++ b/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs @@ -0,0 +1,344 @@ +//! # Metrics reader provider module. +//! +//! This module provides providers for setting up metrics readers +//! in OpenTelemetry SDKs using declarative YAML configurations. + +use opentelemetry_sdk::metrics::MeterProviderBuilder; + +use crate::{ + model::metrics::reader::{PeriodicExporterConsole, PeriodicExporterOtlp, Reader}, + MetricsProvidersRegistry, ProviderError, +}; + +/// Provider for Metrics readers +pub struct ReaderProvider { + periodic_reader_provider: PeriodicReaderProvider, +} + +impl ReaderProvider { + pub fn new() -> Self { + ReaderProvider { + periodic_reader_provider: PeriodicReaderProvider::new(), + } + } + /// Provisions a metrics reader based on the provided configuration + pub fn provide( + &self, + metrics_registry: &MetricsProvidersRegistry, + mut meter_provider_builder: MeterProviderBuilder, + config: &Reader, + ) -> Result { + match config { + crate::model::metrics::reader::Reader::Periodic(periodic_config) => { + meter_provider_builder = self.periodic_reader_provider.provide( + metrics_registry, + meter_provider_builder, + periodic_config, + )?; + } + crate::model::metrics::reader::Reader::Pull(_pull_config) => { + // TODO: Implement pull reader configuration + } + } + Ok(meter_provider_builder) + } +} + +impl Default for ReaderProvider { + fn default() -> Self { + Self::new() + } +} + +/// Periodic reader provider +pub struct PeriodicReaderProvider { + periodic_exporter_provider: PeriodicExporterProvider, +} + +impl PeriodicReaderProvider { + /// Creates a new PeriodicReaderProvider + pub fn new() -> Self { + PeriodicReaderProvider { + periodic_exporter_provider: PeriodicExporterProvider::new(), + } + } + + /// Provisions a periodic metrics reader based on the provided configuration + pub fn provide( + &self, + metrics_registry: &MetricsProvidersRegistry, + mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, + config: &crate::model::metrics::reader::Periodic, + ) -> Result { + if let Some(exporter_config) = &config.exporter { + meter_provider_builder = self.periodic_exporter_provider.provide( + metrics_registry, + meter_provider_builder, + exporter_config, + )?; + } + + Ok(meter_provider_builder) + } +} + +impl Default for PeriodicReaderProvider { + fn default() -> Self { + Self::new() + } +} + +/// Periodic exporter provider +pub struct PeriodicExporterProvider {} + +impl PeriodicExporterProvider { + /// Creates a new PeriodicExporterProvider + pub fn new() -> Self { + PeriodicExporterProvider {} + } + + /// Configures a periodic metrics exporter based on the provided configuration + pub fn provide( + &self, + metrics_registry: &MetricsProvidersRegistry, + mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, + config: &crate::model::metrics::reader::PeriodicExporter, + ) -> Result { + if let Some(console_config) = &config.console { + let provider_option = + metrics_registry.readers_periodic_exporter::(); + if let Some(provider) = provider_option { + meter_provider_builder = provider.provide(meter_provider_builder, console_config); + } else { + return Err(ProviderError::NotRegisteredProvider("No provider found for PeriodicExporterConsole. Make sure it is registered as provider.".to_string())); + } + } + + if let Some(otlp_config) = &config.otlp { + let provider_option = + metrics_registry.readers_periodic_exporter::(); + if let Some(provider) = provider_option { + meter_provider_builder = provider.provide(meter_provider_builder, otlp_config); + } else { + return Err(ProviderError::NotRegisteredProvider("No provider found for PeriodicExporterOtlp. Make sure it is registered as provider.".to_string())); + } + } + + Ok(meter_provider_builder) + } +} + +impl Default for PeriodicExporterProvider { + fn default() -> Self { + Self::new() + } +} + +// Pull reader provider +pub struct PullReaderProvider { + pull_exporter_provider: PullExporterProvider, +} + +impl PullReaderProvider { + /// Creates a new PullReaderProvider + pub fn new() -> Self { + PullReaderProvider { + pull_exporter_provider: PullExporterProvider::new(), + } + } + + /// Configures a pull metrics reader based on the provided configuration + pub fn configure( + &self, + metrics_registry: &MetricsProvidersRegistry, + mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, + config: &crate::model::metrics::reader::Pull, + ) -> Result { + if let Some(exporter_config) = &config.exporter { + meter_provider_builder = self.pull_exporter_provider.configure( + metrics_registry, + meter_provider_builder, + exporter_config, + )?; + } + Ok(meter_provider_builder) + } +} + +impl Default for PullReaderProvider { + fn default() -> Self { + Self::new() + } +} + +/// Pull exporter provider +pub struct PullExporterProvider {} + +impl PullExporterProvider { + /// Creates a new PullExporterProvider + pub fn new() -> Self { + PullExporterProvider {} + } + + pub fn configure( + &self, + _metrics_registry: &MetricsProvidersRegistry, + meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, + config: &crate::model::metrics::reader::PullExporter, + ) -> Result { + if let Some(_prometheus_config) = &config.prometheus { + // Explicitly Prometheus exporter is not supported in this provider. + return Err(ProviderError::UnsupportedExporter( + "Prometheus exporter is not supported.".to_string(), + )); + } + Ok(meter_provider_builder) + } +} + +impl Default for PullExporterProvider { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + + use opentelemetry_sdk::metrics::SdkMeterProvider; + + use crate::MetricsReaderPeriodicExporterProvider; + + use super::*; + + struct MockMetricsReadersPeriodicExporterConsoleProvider {} + + impl MockMetricsReadersPeriodicExporterConsoleProvider { + fn new() -> Self { + MockMetricsReadersPeriodicExporterConsoleProvider {} + } + + fn register_into(registry: &mut crate::ConfigurationProvidersRegistry) { + registry + .metrics_mut() + .register_periodic_exporter_provider::(Box::new( + Self::new(), + )); + } + } + + impl MetricsReaderPeriodicExporterProvider for MockMetricsReadersPeriodicExporterConsoleProvider { + fn provide( + &self, + meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, + _config: &dyn std::any::Any, + ) -> opentelemetry_sdk::metrics::MeterProviderBuilder { + // Mock implementation: just return the builder as is + meter_provider_builder + } + + fn as_any(&self) -> &dyn std::any::Any { + todo!() + } + } + + #[test] + fn test_reader_provider_configure() { + let provider = ReaderProvider::new(); + let mut configuration_registry = crate::ConfigurationProvidersRegistry::new(); + MockMetricsReadersPeriodicExporterConsoleProvider::register_into( + &mut configuration_registry, + ); + let meter_provider_builder = SdkMeterProvider::builder(); + + let config = crate::model::metrics::reader::Reader::Periodic( + crate::model::metrics::reader::Periodic { + exporter: Some(crate::model::metrics::reader::PeriodicExporter { + console: Some(PeriodicExporterConsole { temporality: None }), + otlp: None, + }), + }, + ); + + let metrics_registry = configuration_registry.metrics(); + + _ = provider + .provide(metrics_registry, meter_provider_builder, &config) + .unwrap(); + } + + #[test] + fn test_reader_provider_provide_console_provider_not_registered() { + let provider = ReaderProvider::new(); + let metrics_registry = MetricsProvidersRegistry::new(); + let meter_provider_builder = SdkMeterProvider::builder(); + + let config = crate::model::metrics::reader::Reader::Periodic( + crate::model::metrics::reader::Periodic { + exporter: Some(crate::model::metrics::reader::PeriodicExporter { + console: Some(PeriodicExporterConsole { temporality: None }), + otlp: None, + }), + }, + ); + + let result = provider.provide(&metrics_registry, meter_provider_builder, &config); + if let Err(e) = result { + assert!(e + .to_string() + .contains("No provider found for PeriodicExporterConsole")); + } else { + panic!("Expected error due to missing provider, but got Ok"); + } + } + + #[test] + fn test_reader_provider_provide_otlp_provider_not_registered() { + let provider = ReaderProvider::new(); + let metrics_registry = MetricsProvidersRegistry::new(); + let meter_provider_builder = SdkMeterProvider::builder(); + + let config = crate::model::metrics::reader::Reader::Periodic( + crate::model::metrics::reader::Periodic { + exporter: Some(crate::model::metrics::reader::PeriodicExporter { + console: None, + otlp: Some(PeriodicExporterOtlp { + endpoint: None, + protocol: None, + temporality: None, + }), + }), + }, + ); + + let result = provider.provide(&metrics_registry, meter_provider_builder, &config); + if let Err(e) = result { + assert!(e + .to_string() + .contains("No provider found for PeriodicExporterOtlp")); + } else { + panic!("Expected error due to missing provider, but got Ok"); + } + } + + #[test] + fn test_periodic_exporter_provider_configure_unsupported_exporter() { + let provider = PullExporterProvider::new(); + let metrics_provider_manager = MetricsProvidersRegistry::new(); + let meter_provider_builder = SdkMeterProvider::builder(); + let config = crate::model::metrics::reader::PullExporter { + prometheus: Some(crate::model::metrics::reader::PullExporterPrometheus { + host: "localhost".to_string(), + port: 9090, + }), + }; + let result = provider.configure(&metrics_provider_manager, meter_provider_builder, &config); + if let Err(e) = result { + assert!(e + .to_string() + .contains("Prometheus exporter is not supported.")); + } else { + panic!("Expected error due to unsupported exporter, but got Ok"); + } + } +} From 7803577efcca7623a0b08d711e45aa2a01e79e70 Mon Sep 17 00:00:00 2001 From: Andres Borja Date: Wed, 12 Nov 2025 02:58:27 +0000 Subject: [PATCH 4/7] Make the model extensible for external crates --- opentelemetry-config-stdout/Cargo.toml | 2 + opentelemetry-config-stdout/src/lib.rs | 55 +++--- opentelemetry-config/README.md | 160 +++++++++++++---- .../examples/console/Cargo.toml | 13 -- .../examples/custom/Cargo.toml | 15 ++ .../examples/{console => custom}/src/main.rs | 105 ++++++----- ...trics_console.yaml => metrics_custom.yaml} | 5 +- opentelemetry-config/src/lib.rs | 74 ++++---- opentelemetry-config/src/model.rs | 8 +- .../src/model/metrics/reader.rs | 37 +--- opentelemetry-config/src/providers.rs | 19 +- .../metrics_provider/reader_provider.rs | 164 +++++++++++------- 12 files changed, 400 insertions(+), 257 deletions(-) delete mode 100644 opentelemetry-config/examples/console/Cargo.toml create mode 100644 opentelemetry-config/examples/custom/Cargo.toml rename opentelemetry-config/examples/{console => custom}/src/main.rs (52%) rename opentelemetry-config/examples/{metrics_console.yaml => metrics_custom.yaml} (54%) diff --git a/opentelemetry-config-stdout/Cargo.toml b/opentelemetry-config-stdout/Cargo.toml index cc10948f..e3f50610 100644 --- a/opentelemetry-config-stdout/Cargo.toml +++ b/opentelemetry-config-stdout/Cargo.toml @@ -10,6 +10,8 @@ rust-version = "1.75.0" opentelemetry-config = { path = "../opentelemetry-config" } opentelemetry_sdk = { version = "0.31.0" } opentelemetry-stdout = { version = "0.31.0" } +serde = { version = "1.0", features = ["derive"] } +serde_yaml = { version = "0.9.34" } [lints] workspace = true diff --git a/opentelemetry-config-stdout/src/lib.rs b/opentelemetry-config-stdout/src/lib.rs index ca668a11..5f8e9294 100644 --- a/opentelemetry-config-stdout/src/lib.rs +++ b/opentelemetry-config-stdout/src/lib.rs @@ -4,10 +4,9 @@ //! that enables exporting metrics to the console (stdout) using //! the OpenTelemetry Config crate. -use opentelemetry_config::{ - model::metrics::reader::PeriodicExporterConsole, MetricsReaderPeriodicExporterProvider, -}; +use opentelemetry_config::{MetricsExporterId, MetricsReaderPeriodicExporterProvider}; use opentelemetry_sdk::metrics::MeterProviderBuilder; +use serde_yaml::Value; #[derive(Clone)] pub struct ConsolePeriodicExporterProvider {} @@ -21,34 +20,45 @@ impl ConsolePeriodicExporterProvider { configuration_registry: &mut opentelemetry_config::ConfigurationProvidersRegistry, ) { let provider = ConsolePeriodicExporterProvider::new(); + + let key = MetricsExporterId::PeriodicExporter.qualified_name("console"); configuration_registry .metrics_mut() - .register_periodic_exporter_provider::(Box::new( - provider.clone(), - )); + .register_periodic_exporter_provider(key, Box::new(provider.clone())); // TODO: Add logs and traces providers registration. } } +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] +pub struct PeriodicExporterConsole { + pub temporality: Option, +} + +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum Temporality { + Delta, + Cumulative, +} + impl MetricsReaderPeriodicExporterProvider for ConsolePeriodicExporterProvider { fn provide( &self, mut meter_provider_builder: MeterProviderBuilder, - config: &dyn std::any::Any, + config: &Value, ) -> MeterProviderBuilder { let mut exporter_builder = opentelemetry_stdout::MetricExporter::builder(); - let config = config - .downcast_ref::() - .expect("Invalid config type. Expected PeriodicExporterConsole."); + let config = serde_yaml::from_value::(config.clone()) + .expect("Failed to deserialize PeriodicExporterConsole configuration"); if let Some(temporality) = &config.temporality { match temporality { - opentelemetry_config::model::metrics::reader::Temporality::Delta => { + Temporality::Delta => { exporter_builder = exporter_builder .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta); } - opentelemetry_config::model::metrics::reader::Temporality::Cumulative => { + Temporality::Cumulative => { exporter_builder = exporter_builder .with_temporality(opentelemetry_sdk::metrics::Temporality::Cumulative); } @@ -84,9 +94,10 @@ mod tests { // Act ConsolePeriodicExporterProvider::register_into(&mut configuration_registry); + let key = MetricsExporterId::PeriodicExporter.qualified_name("console"); let provider_option = configuration_registry .metrics() - .readers_periodic_exporter::(); + .readers_periodic_exporter_provider(&key); // Assert assert!(provider_option.is_some()); @@ -100,8 +111,10 @@ mod tests { let config = PeriodicExporterConsole { temporality: None }; + let config_yaml = serde_yaml::to_value(config).unwrap(); + // Act - let configured_builder = provider.provide(meter_provider_builder, &config); + let configured_builder = provider.provide(meter_provider_builder, &config_yaml); // Assert // Since the MeterProviderBuilder does not expose its internal state, @@ -119,11 +132,13 @@ mod tests { let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); let config = PeriodicExporterConsole { - temporality: Some(opentelemetry_config::model::metrics::reader::Temporality::Delta), + temporality: Some(Temporality::Delta), }; + let config_yaml = serde_yaml::to_value(config).unwrap(); + // Act - let configured_builder = provider.provide(meter_provider_builder, &config); + let configured_builder = provider.provide(meter_provider_builder, &config_yaml); // Assert // Since the MeterProviderBuilder does not expose its internal state, @@ -141,13 +156,13 @@ mod tests { let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); let config = PeriodicExporterConsole { - temporality: Some( - opentelemetry_config::model::metrics::reader::Temporality::Cumulative, - ), + temporality: Some(Temporality::Cumulative), }; + let config_yaml = serde_yaml::to_value(config).unwrap(); + // Act - let configured_builder = provider.provide(meter_provider_builder, &config); + let configured_builder = provider.provide(meter_provider_builder, &config_yaml); // Assert // Since the MeterProviderBuilder does not expose its internal state, diff --git a/opentelemetry-config/README.md b/opentelemetry-config/README.md index de494272..81609981 100644 --- a/opentelemetry-config/README.md +++ b/opentelemetry-config/README.md @@ -42,7 +42,7 @@ metrics: readers: - periodic: exporter: - console: + custom: temporality: delta resource: @@ -50,17 +50,58 @@ resource: service.version: "1.0.0" ``` -### 2. Load and Apply Configuration +### 2. Implement an Exporter Provider + +```rust +use opentelemetry_config::{ + ConfigurationProvidersRegistry, + MetricsExporterId, + MetricsReaderPeriodicExporterProvider, +}; +use opentelemetry_sdk::metrics::MeterProviderBuilder; +use serde_yaml::Value; + +struct CustomExporterProvider; + +impl CustomExporterProvider { + pub fn register_into(registry: &mut ConfigurationProvidersRegistry) { + let key = MetricsExporterId::PeriodicExporter + .qualified_name("custom"); + registry + .metrics_mut() + .register_periodic_exporter_provider(key, Box::new(Self)); + } +} + +impl MetricsReaderPeriodicExporterProvider for CustomExporterProvider { + fn provide( + &self, + meter_provider_builder: MeterProviderBuilder, + _config: &Value, + ) -> MeterProviderBuilder { + let exporter = opentelemetry_stdout::MetricExporter::builder() + .build(); + + meter_provider_builder.with_periodic_exporter(exporter) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} +``` + +### 3. Load and Apply Configuration ```rust +use opentelemetry_config::providers::TelemetryProvider; + fn main() -> Result<(), Box> { // Create a configuration registry and register exporters let mut registry = ConfigurationProvidersRegistry::new(); - registry - .metrics_mut() - .register_periodic_exporter_provider::( - Box::new(ConsoleExporterProvider) - ); + + // Register the custom exporter provider + CustomExporterProvider::register_into(&mut registry); // Load configuration from YAML file let telemetry_provider = TelemetryProvider::new(); @@ -70,7 +111,7 @@ fn main() -> Result<(), Box> { // Use the configured providers if let Some(meter_provider) = providers.meter_provider() { // Your application code here - + // Shutdown the meter provider meter_provider.shutdown()?; } @@ -83,24 +124,31 @@ fn main() -> Result<(), Box> { ### Core Components -- **`ConfigurationProvidersRegistry`**: Central registry for configuration providers -- **`TelemetryProvider`**: Orchestrates the configuration process from YAML to providers +- **`ConfigurationProvidersRegistry`**: Central registry for configuration providers across all telemetry signals +- **`MetricsProvidersRegistry`**: Registry specifically for metrics exporter providers +- **`TelemetryProvider`**: Orchestrates the configuration process from YAML to SDK providers - **`TelemetryProviders`**: Holds configured meter, tracer, and logger providers -- **`MetricsReaderPeriodicExporterProvider`**: Trait for implementing custom metric exporters +- **`MetricsReaderPeriodicExporterProvider`**: Trait for implementing custom metric exporter providers +- **`MetricsExporterId`**: Enum for building qualified registry keys for different exporter types ### Design Pattern -This crate follows a **decoupled implementation pattern**: +This crate follows a **provider-based decoupled implementation pattern**: -- **Centralized Configuration Model**: The configuration schema (YAML structure and data models) is defined and maintained centrally in this crate, ensuring alignment with the OpenTelemetry Configuration Standard -- **Decoupled Implementations**: Actual exporter implementations live in external crates, allowing the community to contribute custom exporters without modifying the core configuration model -- **Community Control**: By keeping the configuration model centralized and standardized, the community maintains consistency across all implementations while enabling extensibility +- **Centralized Configuration Model**: The configuration schema (YAML structure and data models) is defined and maintained centrally in this crate, ensuring alignment with the OpenTelemetry Configuration Standard. The general structure (metrics, traces, logs, resource) is enforced to maintain compatibility. +- **Extensible Configuration**: While the top-level structure is controlled, exporter-specific configurations are fully extensible. Providers can define their own configuration schemas that are deserialized from the YAML at runtime, enabling custom properties without modifying the core model. +- **Decoupled Implementations**: Actual exporter implementations live in external crates or user code, allowing the community to contribute custom exporters without modifying the core configuration model. Each provider handles its own configuration deserialization and exporter instantiation. +- **Provider Trait Pattern**: Exporters are registered via provider traits that receive YAML configuration as `serde_yaml::Value`, allowing them to deserialize into any custom configuration structure they need. +- **Registry-Based Discovery**: A central registry maps exporter names to their provider implementations, enabling dynamic configuration. The registry key is built using `MetricsExporterId::qualified_name()` with the exporter name from the YAML. +- **Community Control**: By keeping the top-level configuration model centralized and standardized, the community maintains consistency across all implementations while enabling complete flexibility for exporter-specific configurations. This design enables: -- **Standard Compliance**: All configurations follow the official OpenTelemetry schema -- **Easy Extension**: Contributors can add new exporters by implementing traits in their own crates -- **Version Independence**: Exporter implementations can evolve independently from the configuration schema -- **Flexibility**: Users can mix official and custom exporters using the same configuration format +- **Standard Compliance**: All configurations follow the official OpenTelemetry schema at the top level +- **Easy Extension**: Contributors can add new exporters with custom configurations by implementing the provider trait in their own crates +- **Configuration Flexibility**: Each exporter can define its own configuration structure without requiring changes to the core crate +- **Version Independence**: Exporter implementations and their configurations can evolve independently from the core configuration schema +- **Mixed Exporters**: Users can combine official and custom exporters using the same configuration format +- **Type Safety**: Strong typing throughout the configuration pipeline with runtime validation and deserialization errors ### Configuration Model @@ -115,9 +163,10 @@ The configuration is structured around the `Telemetry` model which includes: ### Console Exporter Example -See the [examples/console](examples/console) directory for a complete working example that demonstrates: +See the [examples/custom](examples/custom) directory for a complete working example that demonstrates: -- Setting up a console exporter provider +- Implementing a custom exporter provider +- Registering the provider with the configuration registry - Loading configuration from a YAML file - Configuring a meter provider - Proper shutdown handling @@ -125,8 +174,8 @@ See the [examples/console](examples/console) directory for a complete working ex To run the example: ```bash -cd examples/console -cargo run -- --file ../metrics_console.yaml +cd examples/custom +cargo run -- --file ../metrics_custom.yaml ``` ## Configuration Schema @@ -160,11 +209,11 @@ resource: To add support for a custom exporter: -1. Define your exporter configuration model: +1. Define your exporter configuration model (optional): ```rust -#[derive(Debug, Deserialize)] -pub struct MyCustomExporter { +#[derive(Debug, serde::Deserialize)] +pub struct MyCustomConfig { pub endpoint: String, pub timeout: Option, } @@ -173,17 +222,24 @@ pub struct MyCustomExporter { 2. Implement the provider trait: ```rust -impl MetricsReaderPeriodicExporterProvider for MyCustomProvider { +use opentelemetry_config::MetricsReaderPeriodicExporterProvider; +use opentelemetry_sdk::metrics::MeterProviderBuilder; +use serde_yaml::Value; + +struct MyCustomExporterProvider; + +impl MetricsReaderPeriodicExporterProvider for MyCustomExporterProvider { fn provide( &self, meter_provider_builder: MeterProviderBuilder, - config: &dyn std::any::Any, + config: &Value, ) -> MeterProviderBuilder { - let config = config.downcast_ref::() - .expect("Invalid config type"); + // Deserialize your custom config + let custom_config = serde_yaml::from_value::(config.clone()) + .expect("Failed to deserialize custom config"); // Build your exporter with the config - let exporter = MyExporter::new(config); + let exporter = MyExporter::new(&custom_config); meter_provider_builder.with_periodic_exporter(exporter) } @@ -196,11 +252,47 @@ impl MetricsReaderPeriodicExporterProvider for MyCustomProvider { 3. Register it with the ConfigurationProvidersRegistry: ```rust +use opentelemetry_config::MetricsExporterId; + +let mut registry = ConfigurationProvidersRegistry::new(); +let key = MetricsExporterId::PeriodicExporter + .qualified_name("my-custom-exporter"); + registry .metrics_mut() - .register_periodic_exporter_provider::( - Box::new(MyCustomProvider) - ); + .register_periodic_exporter_provider(key, Box::new(MyCustomExporterProvider)); +``` + +4. Use it in your YAML configuration: + +```yaml +metrics: + readers: + - periodic: + exporter: + my-custom-exporter: + endpoint: "http://localhost:4318" + timeout: 5000 +``` + +### Helper Method Pattern + +For cleaner registration, add a helper method to your provider: + +```rust +impl MyCustomExporterProvider { + pub fn register_into(registry: &mut ConfigurationProvidersRegistry) { + let key = MetricsExporterId::PeriodicExporter + .qualified_name("my-custom-exporter"); + registry + .metrics_mut() + .register_periodic_exporter_provider(key, Box::new(Self)); + } +} + +// Usage: +let mut registry = ConfigurationProvidersRegistry::new(); +MyCustomExporterProvider::register_into(&mut registry); ``` ## Current Limitations diff --git a/opentelemetry-config/examples/console/Cargo.toml b/opentelemetry-config/examples/console/Cargo.toml deleted file mode 100644 index c3889cb9..00000000 --- a/opentelemetry-config/examples/console/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "opentelemetry-config-mock-exporter-example" -version = "0.1.0" -description = "Declarative configuration for OpenTelemetry SDK example using a mock console exporter configuration" -license = "Apache-2.0" -edition = "2021" -rust-version = "1.75.0" - -[workspace] - -[dependencies] -opentelemetry-config = { path = "../../"} -opentelemetry_sdk = { version = "0.31.0" } \ No newline at end of file diff --git a/opentelemetry-config/examples/custom/Cargo.toml b/opentelemetry-config/examples/custom/Cargo.toml new file mode 100644 index 00000000..8b8b0d98 --- /dev/null +++ b/opentelemetry-config/examples/custom/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "opentelemetry-config-custom-exporter-example" +version = "0.1.0" +description = "Declarative configuration for OpenTelemetry SDK example using a mock custom exporter configuration" +license = "Apache-2.0" +edition = "2021" +rust-version = "1.75.0" + +[workspace] + +[dependencies] +opentelemetry-config = { path = "../../"} +opentelemetry_sdk = { version = "0.31.0" } +serde = { version = "1.0", features = ["derive"] } +serde_yaml = { version = "0.9.34" } \ No newline at end of file diff --git a/opentelemetry-config/examples/console/src/main.rs b/opentelemetry-config/examples/custom/src/main.rs similarity index 52% rename from opentelemetry-config/examples/console/src/main.rs rename to opentelemetry-config/examples/custom/src/main.rs index 5a16e0bc..22ede33b 100644 --- a/opentelemetry-config/examples/console/src/main.rs +++ b/opentelemetry-config/examples/custom/src/main.rs @@ -1,45 +1,41 @@ -//! # Example OpenTelemetry Config Console +//! # Example OpenTelemetry Config Custom exporter //! //! This example demonstrates how to configure OpenTelemetry Metrics -//! using the OpenTelemetry Config crate with a Mock Console Exporter. +//! using the OpenTelemetry Config crate with a Mock Custom Exporter. //! It is helpful to implement and test custom exporters. use opentelemetry_config::{ - model::metrics::reader::PeriodicExporterConsole, providers::TelemetryProvider, - ConfigurationProvidersRegistry, MetricsReaderPeriodicExporterProvider, + providers::TelemetryProvider, ConfigurationProvidersRegistry, MetricsExporterId, + MetricsReaderPeriodicExporterProvider, }; -use opentelemetry_sdk::metrics::Temporality; use opentelemetry_sdk::{ error::OTelSdkResult, metrics::{data::ResourceMetrics, exporter::PushMetricExporter, MeterProviderBuilder}, }; -use std::time::Duration; - +use serde_yaml::Value; use std::env; +use std::time::Duration; pub fn main() -> Result<(), Box> { let args: Vec = env::args().collect(); if args.len() == 1 || (args.len() > 1 && args[1] == "--help") { - println!("Usage: cargo run -- --file ../metrics_console.yaml"); - println!("This example demonstrates how to configure OpenTelemetry Metrics using the OpenTelemetry Config crate with a Console Exporter."); + println!("Usage: cargo run -- --file ../metrics_custom.yaml"); + println!("This example demonstrates how to configure OpenTelemetry Metrics using the OpenTelemetry Config crate with a custom Exporter."); return Ok(()); } if args.len() < 3 || args[1] != "--file" { println!("Error: Configuration file path not provided."); - println!("Usage: cargo run -- --file ../metrics_console.yaml"); + println!("Usage: cargo run -- --file ../metrics_custom.yaml"); return Ok(()); } let config_file = &args[2]; - // Setup configuration registry with console exporter provider. - let provider = Box::new(MockPeriodicExporterProvider::new()); + // Setup configuration registry with custom exporter provider. let mut registry = ConfigurationProvidersRegistry::new(); - // Register the console exporter provider for the specific exporter type. - registry - .metrics_mut() - .register_periodic_exporter_provider::(provider); + // Register the custom exporter provider. + MockPeriodicExporterProvider::register_into(&mut registry); let telemetry_provider = TelemetryProvider::new(); let providers = telemetry_provider @@ -76,83 +72,84 @@ impl MockPeriodicExporterProvider { fn new() -> Self { Self {} } + + pub fn register_into(registry: &mut ConfigurationProvidersRegistry) { + let key = MetricsExporterId::PeriodicExporter.qualified_name("custom"); + registry + .metrics_mut() + .register_periodic_exporter_provider(key, Box::new(Self::new())); + } } -pub struct MockConsoleExporter { - temporality: Temporality, +pub struct MockCustomExporter { + custom_config: Option, } -impl MockConsoleExporter { +impl MockCustomExporter { fn new() -> Self { - let temporality = Temporality::Delta; - Self { temporality } + Self { + custom_config: None, + } } - pub fn set_temporality(&mut self, temporality: Temporality) { - self.temporality = temporality; + pub fn set_custom_config(&mut self, custom_config: MockCustomConfig) { + self.custom_config = Some(custom_config); } } -impl PushMetricExporter for MockConsoleExporter { +impl PushMetricExporter for MockCustomExporter { async fn export(&self, metrics: &ResourceMetrics) -> OTelSdkResult { println!( - "MockConsoleExporter exporting metrics {:?} with temporality: {:?}", - metrics, self.temporality + "MockCustomExporter exporting metrics {:?} with custom config: {:?}", + metrics, self.custom_config ); Ok(()) } - /// Flushes any metric data held by an exporter. fn force_flush(&self) -> OTelSdkResult { - println!("MockConsoleExporter force flushing metrics."); + println!("MockCustomExporter force flushing metrics."); Ok(()) } - /// Releases any held computational resources. - /// - /// After Shutdown is called, calls to Export will perform no operation and - /// instead will return an error indicating the shutdown state. fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { println!( - "MockConsoleExporter shutting down with timeout: {:?}", + "MockCustomExporter shutting down with timeout: {:?}", timeout ); Ok(()) } - /// Shutdown with the default timeout of 5 seconds. fn shutdown(&self) -> OTelSdkResult { self.shutdown_with_timeout(Duration::from_secs(5)) } - /// Access the [Temporality] of the MetricExporter. - fn temporality(&self) -> Temporality { - self.temporality + fn temporality(&self) -> opentelemetry_sdk::metrics::Temporality { + opentelemetry_sdk::metrics::Temporality::Cumulative } } +#[derive(serde::Deserialize, Debug)] +pub struct MockCustomConfig { + pub custom_string_field: String, + pub custom_int_field: i32, +} + impl MetricsReaderPeriodicExporterProvider for MockPeriodicExporterProvider { fn provide( &self, mut meter_provider_builder: MeterProviderBuilder, - config: &dyn std::any::Any, + config: &Value, ) -> MeterProviderBuilder { - let mut exporter = MockConsoleExporter::new(); - - let config = config - .downcast_ref::() - .expect("Invalid config type. Expected PeriodicExporterConsole."); - - if let Some(temporality) = &config.temporality { - match temporality { - opentelemetry_config::model::metrics::reader::Temporality::Delta => { - exporter.set_temporality(Temporality::Delta); - } - opentelemetry_config::model::metrics::reader::Temporality::Cumulative => { - exporter.set_temporality(Temporality::Cumulative); - } - } - } + let mut exporter = MockCustomExporter::new(); + + let config = serde_yaml::from_value::(config.clone()) + .expect("Failed to deserialize MockCustomConfig"); + println!( + "Configuring MockCustomExporter with string field: {} and int field: {}", + config.custom_string_field, config.custom_int_field + ); + + exporter.set_custom_config(config); meter_provider_builder = meter_provider_builder.with_periodic_exporter(exporter); meter_provider_builder diff --git a/opentelemetry-config/examples/metrics_console.yaml b/opentelemetry-config/examples/metrics_custom.yaml similarity index 54% rename from opentelemetry-config/examples/metrics_console.yaml rename to opentelemetry-config/examples/metrics_custom.yaml index 7cb0ae3e..9011726c 100644 --- a/opentelemetry-config/examples/metrics_console.yaml +++ b/opentelemetry-config/examples/metrics_custom.yaml @@ -2,8 +2,9 @@ metrics: readers: - periodic: exporter: - console: - temporality: delta + custom: + custom_string_field: delta + custom_int_field: 42 resource: service.name: "test-service" service.version: "1.0.0" diff --git a/opentelemetry-config/src/lib.rs b/opentelemetry-config/src/lib.rs index cc7aaa3f..afc7f110 100644 --- a/opentelemetry-config/src/lib.rs +++ b/opentelemetry-config/src/lib.rs @@ -5,7 +5,6 @@ //! for metrics, traces, and exporters in a structured manner. use std::{ - any::type_name, collections::HashMap, error, fmt::{self, Display}, @@ -16,6 +15,7 @@ use opentelemetry_sdk::{ metrics::{MeterProviderBuilder, SdkMeterProvider}, trace::SdkTracerProvider, }; +use serde_yaml::Value; pub mod model; pub mod providers; @@ -49,34 +49,34 @@ impl Default for ConfigurationProvidersRegistry { /// Registry for metrics configuration providers. pub struct MetricsProvidersRegistry { - readers_periodic_exporters: HashMap>, + readers_periodic_exporters_providers: + HashMap>, } impl MetricsProvidersRegistry { pub fn new() -> Self { Self { - readers_periodic_exporters: HashMap::new(), + readers_periodic_exporters_providers: HashMap::new(), } } - pub fn register_periodic_exporter_provider( + pub fn register_periodic_exporter_provider( &mut self, + key: String, provider: Box, ) { - let name: String = type_name::().to_string(); - self.readers_periodic_exporters.insert( - name, + self.readers_periodic_exporters_providers.insert( + key, provider as Box, ); } - pub fn readers_periodic_exporter( + pub fn readers_periodic_exporter_provider( &self, - //type_name: &str, + exporter_id: &str, ) -> Option<&dyn MetricsReaderPeriodicExporterProvider> { - let type_name = type_name::().to_string(); - self.readers_periodic_exporters - .get(&type_name) + self.readers_periodic_exporters_providers + .get(exporter_id) .map(|b| b.as_ref()) } } @@ -87,12 +87,25 @@ impl Default for MetricsProvidersRegistry { } } +/// Enum representing different metrics exporter identifiers. +pub enum MetricsExporterId { + PeriodicExporter, +} + +impl MetricsExporterId { + pub fn qualified_name(self, exporter_name: &str) -> String { + match self { + Self::PeriodicExporter => format!("readers::periodic::exporter::{}", exporter_name), + } + } +} + /// Trait for providing metrics reader periodic exporter configurations. pub trait MetricsReaderPeriodicExporterProvider { fn provide( &self, meter_provider_builder: MeterProviderBuilder, - config: &dyn std::any::Any, + config: &Value, ) -> MeterProviderBuilder; fn as_any(&self) -> &dyn std::any::Any; @@ -178,8 +191,6 @@ impl Display for ProviderError { mod tests { use std::cell::Cell; - use crate::model::metrics::reader::PeriodicExporterConsole; - use super::*; #[test] @@ -205,7 +216,7 @@ mod tests { fn provide( &self, meter_provider_builder: MeterProviderBuilder, - _config: &dyn std::any::Any, + _config: &Value, ) -> MeterProviderBuilder { self.call_count.set(self.call_count.get() + 1); meter_provider_builder @@ -220,25 +231,28 @@ mod tests { let mut registry = ConfigurationProvidersRegistry::new(); // Act + let key = MetricsExporterId::PeriodicExporter.qualified_name("console"); registry .metrics_mut() - .register_periodic_exporter_provider::(mock_provider); + .register_periodic_exporter_provider(key.clone(), mock_provider); // Assert - let type_name = type_name::().to_string(); assert!(registry .metrics() - .readers_periodic_exporters - .contains_key(&type_name)); - - let provider_option = registry - .metrics() - .readers_periodic_exporter::(); + .readers_periodic_exporters_providers + .contains_key(key.as_str())); + + let console_config = serde_yaml::to_value( + r#" + console: + temporality: cumulative + "#, + ) + .unwrap(); + + let provider_option = registry.metrics().readers_periodic_exporter_provider(&key); if let Some(provider) = provider_option { - provider.provide( - MeterProviderBuilder::default(), - &PeriodicExporterConsole { temporality: None }, - ); + provider.provide(MeterProviderBuilder::default(), &console_config); let provider_cast = provider .as_any() .downcast_ref::() @@ -254,7 +268,7 @@ mod tests { let provider_manager = ConfigurationProvidersRegistry::default(); assert!(provider_manager .metrics() - .readers_periodic_exporters + .readers_periodic_exporters_providers .is_empty()); } @@ -262,7 +276,7 @@ mod tests { fn test_metrics_provider_manager_default() { let metrics_provider_manager = MetricsProvidersRegistry::default(); assert!(metrics_provider_manager - .readers_periodic_exporters + .readers_periodic_exporters_providers .is_empty()); } diff --git a/opentelemetry-config/src/model.rs b/opentelemetry-config/src/model.rs index a47e8bb3..b4dadc93 100644 --- a/opentelemetry-config/src/model.rs +++ b/opentelemetry-config/src/model.rs @@ -54,8 +54,8 @@ resource: metrics: readers: - periodic: - exporter: - invalid_console: {} + exporter_invalid_field: + console: {} resource: service.name: "example-service" service.version: "1.0.0" @@ -63,7 +63,9 @@ resource: let telemetry_result: Result = serde_yaml::from_str(yaml_str); if let Err(e) = telemetry_result { - assert!(e.to_string().contains("unknown field `invalid_console`")); + assert!(e + .to_string() + .contains("unknown field `exporter_invalid_field`, expected `exporter`")); } else { panic!("Expected error due to invalid field, but got Ok"); } diff --git a/opentelemetry-config/src/model/metrics/reader.rs b/opentelemetry-config/src/model/metrics/reader.rs index a6d8b11f..cc8e41bd 100644 --- a/opentelemetry-config/src/model/metrics/reader.rs +++ b/opentelemetry-config/src/model/metrics/reader.rs @@ -48,28 +48,7 @@ impl<'de> Deserialize<'de> for Reader { #[derive(serde::Deserialize, Debug)] #[serde(deny_unknown_fields)] pub struct Periodic { - pub exporter: Option, -} - -#[derive(serde::Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct PeriodicExporter { - pub console: Option, - pub otlp: Option, -} - -#[derive(serde::Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct PeriodicExporterConsole { - pub temporality: Option, -} - -#[derive(serde::Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct PeriodicExporterOtlp { - pub protocol: Option, - pub endpoint: Option, - pub temporality: Option, + pub exporter: serde_yaml::Value, } #[derive(serde::Deserialize, Debug)] @@ -123,13 +102,13 @@ mod tests { let reader: Reader = serde_yaml::from_str(yaml_data).unwrap(); match reader { Reader::Periodic(periodic) => { - assert!(periodic.exporter.is_some()); - let exporter = periodic.exporter.unwrap(); - assert!(exporter.console.is_some()); - let console = exporter.console.unwrap(); - match console.temporality { - Some(Temporality::Cumulative) => {} - _ => panic!("Expected Cumulative temporality"), + let exporter = periodic.exporter; + if let serde_yaml::Value::Mapping(exporter_map) = exporter { + assert!(exporter_map + .get(&serde_yaml::Value::String("console".to_string())) + .is_some()); + } else { + panic!("Expected Mapping for exporter"); } } _ => panic!("Expected Periodic reader"), diff --git a/opentelemetry-config/src/providers.rs b/opentelemetry-config/src/providers.rs index c7b0986a..f419bff8 100644 --- a/opentelemetry-config/src/providers.rs +++ b/opentelemetry-config/src/providers.rs @@ -119,13 +119,12 @@ impl Default for TelemetryProvider { #[cfg(test)] mod tests { - use std::{any::Any, cell::Cell}; + use std::cell::Cell; use opentelemetry_sdk::metrics::MeterProviderBuilder; + use serde_yaml::Value; - use crate::{ - model::metrics::reader::PeriodicExporterConsole, MetricsReaderPeriodicExporterProvider, - }; + use crate::{MetricsExporterId, MetricsReaderPeriodicExporterProvider}; use super::*; @@ -149,13 +148,10 @@ mod tests { fn provide( &self, meter_provider_builder: MeterProviderBuilder, - config: &(dyn Any + 'static), + config: &Value, ) -> MeterProviderBuilder { // Mock implementation: In a real scenario, configure the console exporter here self.call_count.set(self.call_count.get() + 1); - let config = config - .downcast_ref::() - .expect("Invalid config type"); println!("Mock configure called with config: {:?}", config); meter_provider_builder } @@ -186,9 +182,8 @@ mod tests { let mut configuration_registry = ConfigurationProvidersRegistry::new(); let metrics_provider_manager = configuration_registry.metrics_mut(); - //metrics_provider_manager.register_periodic_exporter_console_provider(provider); - metrics_provider_manager - .register_periodic_exporter_provider::(provider); + let key = MetricsExporterId::PeriodicExporter.qualified_name("console"); + metrics_provider_manager.register_periodic_exporter_provider(key.clone(), provider); let telemetry_provider = TelemetryProvider::new(); let providers = telemetry_provider @@ -198,7 +193,7 @@ mod tests { let provider = configuration_registry .metrics() - .readers_periodic_exporter::() + .readers_periodic_exporter_provider(&key) .unwrap(); let provider = provider .as_any() diff --git a/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs b/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs index 67ed9192..ca469a06 100644 --- a/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs +++ b/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs @@ -4,10 +4,10 @@ //! in OpenTelemetry SDKs using declarative YAML configurations. use opentelemetry_sdk::metrics::MeterProviderBuilder; +use serde_yaml::Value; use crate::{ - model::metrics::reader::{PeriodicExporterConsole, PeriodicExporterOtlp, Reader}, - MetricsProvidersRegistry, ProviderError, + model::metrics::reader::Reader, MetricsExporterId, MetricsProvidersRegistry, ProviderError, }; /// Provider for Metrics readers @@ -18,7 +18,7 @@ pub struct ReaderProvider { impl ReaderProvider { pub fn new() -> Self { ReaderProvider { - periodic_reader_provider: PeriodicReaderProvider::new(), + periodic_reader_provider: PeriodicReaderProvider::default(), } } /// Provisions a metrics reader based on the provided configuration @@ -59,7 +59,7 @@ impl PeriodicReaderProvider { /// Creates a new PeriodicReaderProvider pub fn new() -> Self { PeriodicReaderProvider { - periodic_exporter_provider: PeriodicExporterProvider::new(), + periodic_exporter_provider: PeriodicExporterProvider::default(), } } @@ -70,14 +70,11 @@ impl PeriodicReaderProvider { mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, config: &crate::model::metrics::reader::Periodic, ) -> Result { - if let Some(exporter_config) = &config.exporter { - meter_provider_builder = self.periodic_exporter_provider.provide( - metrics_registry, - meter_provider_builder, - exporter_config, - )?; - } - + meter_provider_builder = self.periodic_exporter_provider.provide( + metrics_registry, + meter_provider_builder, + &config.exporter, + )?; Ok(meter_provider_builder) } } @@ -97,33 +94,54 @@ impl PeriodicExporterProvider { PeriodicExporterProvider {} } + fn registry_key(&self, exporter_name: &str) -> String { + MetricsExporterId::PeriodicExporter.qualified_name(exporter_name) + } + /// Configures a periodic metrics exporter based on the provided configuration pub fn provide( &self, metrics_registry: &MetricsProvidersRegistry, mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, - config: &crate::model::metrics::reader::PeriodicExporter, + config: &Value, ) -> Result { - if let Some(console_config) = &config.console { - let provider_option = - metrics_registry.readers_periodic_exporter::(); - if let Some(provider) = provider_option { - meter_provider_builder = provider.provide(meter_provider_builder, console_config); - } else { - return Err(ProviderError::NotRegisteredProvider("No provider found for PeriodicExporterConsole. Make sure it is registered as provider.".to_string())); + match config.as_mapping() { + Some(exporter_map) => { + for key in exporter_map.keys() { + match key { + Value::String(exporter_name) => { + let registry_key = self.registry_key(exporter_name); + let exporter_provider_option = + metrics_registry.readers_periodic_exporter_provider(®istry_key); + match exporter_provider_option { + Some(provider) => { + let config = + &exporter_map[&Value::String(exporter_name.clone())]; + meter_provider_builder = + provider.provide(meter_provider_builder, config); + } + None => { + return Err(ProviderError::NotRegisteredProvider(format!( + "No provider found for periodic exporter: {}. Make sure it is registered as provider.", + registry_key + ))); + } + } + } + _ => { + return Err(ProviderError::InvalidConfiguration( + "Exporter name must be a string.".to_string(), + )); + } + } + } } - } - - if let Some(otlp_config) = &config.otlp { - let provider_option = - metrics_registry.readers_periodic_exporter::(); - if let Some(provider) = provider_option { - meter_provider_builder = provider.provide(meter_provider_builder, otlp_config); - } else { - return Err(ProviderError::NotRegisteredProvider("No provider found for PeriodicExporterOtlp. Make sure it is registered as provider.".to_string())); + None => { + return Err(ProviderError::InvalidConfiguration( + "Expecting a configuration object for periodic exporter.".to_string(), + )); } } - Ok(meter_provider_builder) } } @@ -143,7 +161,7 @@ impl PullReaderProvider { /// Creates a new PullReaderProvider pub fn new() -> Self { PullReaderProvider { - pull_exporter_provider: PullExporterProvider::new(), + pull_exporter_provider: PullExporterProvider::default(), } } @@ -207,7 +225,7 @@ mod tests { use opentelemetry_sdk::metrics::SdkMeterProvider; - use crate::MetricsReaderPeriodicExporterProvider; + use crate::{model::metrics::reader::PullExporter, MetricsReaderPeriodicExporterProvider}; use super::*; @@ -219,11 +237,10 @@ mod tests { } fn register_into(registry: &mut crate::ConfigurationProvidersRegistry) { + let key = MetricsExporterId::PeriodicExporter.qualified_name("console"); registry .metrics_mut() - .register_periodic_exporter_provider::(Box::new( - Self::new(), - )); + .register_periodic_exporter_provider(key, Box::new(Self::new())); } } @@ -231,7 +248,7 @@ mod tests { fn provide( &self, meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, - _config: &dyn std::any::Any, + _config: &Value, ) -> opentelemetry_sdk::metrics::MeterProviderBuilder { // Mock implementation: just return the builder as is meter_provider_builder @@ -244,19 +261,23 @@ mod tests { #[test] fn test_reader_provider_configure() { - let provider = ReaderProvider::new(); + let provider = ReaderProvider::default(); let mut configuration_registry = crate::ConfigurationProvidersRegistry::new(); MockMetricsReadersPeriodicExporterConsoleProvider::register_into( &mut configuration_registry, ); let meter_provider_builder = SdkMeterProvider::builder(); + let console_object = serde_yaml::from_str( + r#" + console: + temporality: cumulative + "#, + ) + .unwrap(); let config = crate::model::metrics::reader::Reader::Periodic( crate::model::metrics::reader::Periodic { - exporter: Some(crate::model::metrics::reader::PeriodicExporter { - console: Some(PeriodicExporterConsole { temporality: None }), - otlp: None, - }), + exporter: console_object, }, ); @@ -269,24 +290,29 @@ mod tests { #[test] fn test_reader_provider_provide_console_provider_not_registered() { - let provider = ReaderProvider::new(); + let provider = ReaderProvider::default(); let metrics_registry = MetricsProvidersRegistry::new(); let meter_provider_builder = SdkMeterProvider::builder(); + let console_config = serde_yaml::from_str( + r#" + console: + temporality: cumulative + "#, + ) + .unwrap(); + let config = crate::model::metrics::reader::Reader::Periodic( crate::model::metrics::reader::Periodic { - exporter: Some(crate::model::metrics::reader::PeriodicExporter { - console: Some(PeriodicExporterConsole { temporality: None }), - otlp: None, - }), + exporter: console_config, }, ); let result = provider.provide(&metrics_registry, meter_provider_builder, &config); if let Err(e) = result { - assert!(e - .to_string() - .contains("No provider found for PeriodicExporterConsole")); + assert!(e.to_string().contains( + "No provider found for periodic exporter: readers::periodic::exporter::console" + )); } else { panic!("Expected error due to missing provider, but got Ok"); } @@ -298,24 +324,25 @@ mod tests { let metrics_registry = MetricsProvidersRegistry::new(); let meter_provider_builder = SdkMeterProvider::builder(); + let console_config = serde_yaml::from_str( + r#" + otlp: + temporality: cumulative + "#, + ) + .unwrap(); + let config = crate::model::metrics::reader::Reader::Periodic( crate::model::metrics::reader::Periodic { - exporter: Some(crate::model::metrics::reader::PeriodicExporter { - console: None, - otlp: Some(PeriodicExporterOtlp { - endpoint: None, - protocol: None, - temporality: None, - }), - }), + exporter: console_config, }, ); let result = provider.provide(&metrics_registry, meter_provider_builder, &config); if let Err(e) = result { - assert!(e - .to_string() - .contains("No provider found for PeriodicExporterOtlp")); + assert!(e.to_string().contains( + "No provider found for periodic exporter: readers::periodic::exporter::otlp" + )); } else { panic!("Expected error due to missing provider, but got Ok"); } @@ -341,4 +368,21 @@ mod tests { panic!("Expected error due to unsupported exporter, but got Ok"); } } + + #[test] + fn test_pull_reader_provider_configure_basic() { + let provider = PullReaderProvider::default(); + let configuration_registry = crate::ConfigurationProvidersRegistry::new(); + let meter_provider_builder = SdkMeterProvider::builder(); + + let config = crate::model::metrics::reader::Pull { + exporter: Some(PullExporter { prometheus: None }), + }; + + let metrics_registry = configuration_registry.metrics(); + + _ = provider + .configure(metrics_registry, meter_provider_builder, &config) + .unwrap(); + } } From 8506458592b660d59486f455919cadaecda57ff1 Mon Sep 17 00:00:00 2001 From: Andres Borja Date: Thu, 13 Nov 2025 01:52:30 +0000 Subject: [PATCH 5/7] Fix lint (ubuntu-22.04-arm) --- opentelemetry-config/src/model/metrics/reader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-config/src/model/metrics/reader.rs b/opentelemetry-config/src/model/metrics/reader.rs index cc8e41bd..cfdaa04f 100644 --- a/opentelemetry-config/src/model/metrics/reader.rs +++ b/opentelemetry-config/src/model/metrics/reader.rs @@ -105,7 +105,7 @@ mod tests { let exporter = periodic.exporter; if let serde_yaml::Value::Mapping(exporter_map) = exporter { assert!(exporter_map - .get(&serde_yaml::Value::String("console".to_string())) + .get(serde_yaml::Value::String("console".to_string())) .is_some()); } else { panic!("Expected Mapping for exporter"); From faca570679e2844f97778dc2aa5e4ee66933b3eb Mon Sep 17 00:00:00 2001 From: Andres Borja Date: Fri, 14 Nov 2025 19:53:52 +0000 Subject: [PATCH 6/7] Use registration function instead of trait --- opentelemetry-config-stdout/README.md | 23 +- .../examples/console/src/main.rs | 9 +- opentelemetry-config-stdout/src/lib.rs | 138 ++++++------ opentelemetry-config/README.md | 205 ++++++++++-------- .../examples/custom/src/main.rs | 78 +++---- opentelemetry-config/src/lib.rs | 176 ++++++++------- opentelemetry-config/src/model.rs | 36 +-- opentelemetry-config/src/providers.rs | 112 +++++----- .../src/providers/metrics_provider.rs | 61 +++++- .../metrics_provider/reader_provider.rs | 123 +++++------ 10 files changed, 521 insertions(+), 440 deletions(-) diff --git a/opentelemetry-config-stdout/README.md b/opentelemetry-config-stdout/README.md index 32f1ec7e..311af270 100644 --- a/opentelemetry-config-stdout/README.md +++ b/opentelemetry-config-stdout/README.md @@ -50,13 +50,16 @@ resource: ### 2. Load and Apply Configuration ```rust -use opentelemetry_config::{ConfigurationProvidersRegistry, TelemetryProvider}; -use opentelemetry_config_stdout::ConsolePeriodicExporterProvider; +use opentelemetry_config::{ConfigurationProvidersRegistry, providers::TelemetryProvider}; fn main() -> Result<(), Box> { - // Create configuration registry and register stdout exporter + // Create configuration registry and register console exporter let mut registry = ConfigurationProvidersRegistry::new(); - ConsolePeriodicExporterRegistry::register_into(&mut registry); + let metrics_registry = registry.metrics_mut(); + metrics_registry.register_periodic_exporter_factory( + "console".to_string(), + opentelemetry_config_stdout::register_console_exporter + ); // Load configuration from YAML file let telemetry_provider = TelemetryProvider::new(); @@ -67,7 +70,7 @@ fn main() -> Result<(), Box> { if let Some(meter_provider) = providers.meter_provider() { // Your application code here - // Shutdown the created meter provider. + // Shutdown the created meter provider when done. meter_provider.shutdown()?; } @@ -81,7 +84,7 @@ fn main() -> Result<(), Box> { See the [examples/console](examples/console) directory for a complete working example that demonstrates: -- Setting up a console exporter provider +- Setting up a console exporter factory - Loading configuration from a YAML file - Configuring a meter provider - Proper shutdown handling @@ -106,6 +109,12 @@ metrics: temporality: delta # or cumulative ``` +#### Configuration Options + +- **`temporality`** (optional): Controls how metrics are aggregated + - `delta`: Reports the change since the last export (useful for rate-based metrics like requests per second) + - `cumulative`: Reports the total accumulated value since the start (default, useful for gauges and cumulative counters) + ## Contributing Contributions are welcome! Please feel free to submit issues or pull requests. @@ -116,4 +125,4 @@ This project is licensed under the Apache-2.0 license. ## Release Notes -You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust-contrib/tree/main/opentelemetry-config-stdout/CHANGELOG.md). +You can find the release notes (changelog) [here](CHANGELOG.md). diff --git a/opentelemetry-config-stdout/examples/console/src/main.rs b/opentelemetry-config-stdout/examples/console/src/main.rs index 2a5fd523..62580a59 100644 --- a/opentelemetry-config-stdout/examples/console/src/main.rs +++ b/opentelemetry-config-stdout/examples/console/src/main.rs @@ -4,7 +4,6 @@ //! using the OpenTelemetry Config crate with a Console Exporter. use opentelemetry_config::{providers::TelemetryProvider, ConfigurationProvidersRegistry}; -use opentelemetry_config_stdout::ConsolePeriodicExporterProvider; use std::env; @@ -25,11 +24,15 @@ pub fn main() -> Result<(), Box> { // Setup configuration registry with console exporter provider. let mut configuration_providers_registry = ConfigurationProvidersRegistry::new(); - ConsolePeriodicExporterProvider::register_into(&mut configuration_providers_registry); + let metrics_registry = configuration_providers_registry.metrics_mut(); + metrics_registry.register_periodic_exporter_factory( + "console".to_string(), + opentelemetry_config_stdout::register_console_exporter, + ); let telemetry_provider = TelemetryProvider::new(); let providers = telemetry_provider - .provide_from_yaml_file(&configuration_providers_registry, config_file)?; + .configure_from_yaml_file(&configuration_providers_registry, config_file)?; if let Some(meter_provider) = providers.meter_provider() { println!("Meter provider is configured. Shutting it down..."); diff --git a/opentelemetry-config-stdout/src/lib.rs b/opentelemetry-config-stdout/src/lib.rs index 5f8e9294..3f4dbb59 100644 --- a/opentelemetry-config-stdout/src/lib.rs +++ b/opentelemetry-config-stdout/src/lib.rs @@ -4,29 +4,39 @@ //! that enables exporting metrics to the console (stdout) using //! the OpenTelemetry Config crate. -use opentelemetry_config::{MetricsExporterId, MetricsReaderPeriodicExporterProvider}; +use opentelemetry_config::ConfigurationError; use opentelemetry_sdk::metrics::MeterProviderBuilder; -use serde_yaml::Value; -#[derive(Clone)] -pub struct ConsolePeriodicExporterProvider {} - -impl ConsolePeriodicExporterProvider { - pub fn new() -> Self { - Self {} +pub fn register_console_exporter( + mut builder: MeterProviderBuilder, + config: &serde_yaml::Value, +) -> Result { + let mut exporter_builder = opentelemetry_stdout::MetricExporter::builder(); + + let config = + serde_yaml::from_value::(config.clone()).map_err(|e| { + ConfigurationError::InvalidConfiguration(format!( + "Failed to deserialize PeriodicExporterConsole configuration: {}", + e + )) + })?; + + if let Some(temporality) = &config.temporality { + match temporality { + Temporality::Delta => { + exporter_builder = exporter_builder + .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta); + } + Temporality::Cumulative => { + exporter_builder = exporter_builder + .with_temporality(opentelemetry_sdk::metrics::Temporality::Cumulative); + } + } } - pub fn register_into( - configuration_registry: &mut opentelemetry_config::ConfigurationProvidersRegistry, - ) { - let provider = ConsolePeriodicExporterProvider::new(); - - let key = MetricsExporterId::PeriodicExporter.qualified_name("console"); - configuration_registry - .metrics_mut() - .register_periodic_exporter_provider(key, Box::new(provider.clone())); - // TODO: Add logs and traces providers registration. - } + let exporter = exporter_builder.build(); + builder = builder.with_periodic_exporter(exporter); + Ok(builder) } #[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] @@ -41,46 +51,6 @@ pub enum Temporality { Cumulative, } -impl MetricsReaderPeriodicExporterProvider for ConsolePeriodicExporterProvider { - fn provide( - &self, - mut meter_provider_builder: MeterProviderBuilder, - config: &Value, - ) -> MeterProviderBuilder { - let mut exporter_builder = opentelemetry_stdout::MetricExporter::builder(); - - let config = serde_yaml::from_value::(config.clone()) - .expect("Failed to deserialize PeriodicExporterConsole configuration"); - - if let Some(temporality) = &config.temporality { - match temporality { - Temporality::Delta => { - exporter_builder = exporter_builder - .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta); - } - Temporality::Cumulative => { - exporter_builder = exporter_builder - .with_temporality(opentelemetry_sdk::metrics::Temporality::Cumulative); - } - } - } - - let exporter = exporter_builder.build(); - meter_provider_builder = meter_provider_builder.with_periodic_exporter(exporter); - meter_provider_builder - } - - fn as_any(&self) -> &dyn std::any::Any { - self - } -} - -impl Default for ConsolePeriodicExporterProvider { - fn default() -> Self { - Self::new() - } -} - #[cfg(test)] mod tests { use super::*; @@ -92,21 +62,19 @@ mod tests { opentelemetry_config::ConfigurationProvidersRegistry::new(); // Act - ConsolePeriodicExporterProvider::register_into(&mut configuration_registry); - - let key = MetricsExporterId::PeriodicExporter.qualified_name("console"); - let provider_option = configuration_registry - .metrics() - .readers_periodic_exporter_provider(&key); + let metrics_registry = configuration_registry.metrics_mut(); + metrics_registry + .register_periodic_exporter_factory("console".to_string(), register_console_exporter); // Assert - assert!(provider_option.is_some()); + assert!(metrics_registry + .periodic_exporter_factory("console") + .is_some()); } #[test] fn test_console_provider_configure_temporality_minimal() { // Arrange - let provider = ConsolePeriodicExporterProvider::new(); let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); let config = PeriodicExporterConsole { temporality: None }; @@ -114,7 +82,8 @@ mod tests { let config_yaml = serde_yaml::to_value(config).unwrap(); // Act - let configured_builder = provider.provide(meter_provider_builder, &config_yaml); + let configured_builder = + register_console_exporter(meter_provider_builder, &config_yaml).unwrap(); // Assert // Since the MeterProviderBuilder does not expose its internal state, @@ -128,7 +97,6 @@ mod tests { #[test] fn test_console_provider_configure_temporality_delta() { // Arrange - let provider = ConsolePeriodicExporterProvider::new(); let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); let config = PeriodicExporterConsole { @@ -138,7 +106,8 @@ mod tests { let config_yaml = serde_yaml::to_value(config).unwrap(); // Act - let configured_builder = provider.provide(meter_provider_builder, &config_yaml); + let configured_builder = + register_console_exporter(meter_provider_builder, &config_yaml).unwrap(); // Assert // Since the MeterProviderBuilder does not expose its internal state, @@ -152,7 +121,6 @@ mod tests { #[test] fn test_console_provider_configure_temporality_cumulative() { // Arrange - let provider = ConsolePeriodicExporterProvider::new(); let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); let config = PeriodicExporterConsole { @@ -162,7 +130,8 @@ mod tests { let config_yaml = serde_yaml::to_value(config).unwrap(); // Act - let configured_builder = provider.provide(meter_provider_builder, &config_yaml); + let configured_builder = + register_console_exporter(meter_provider_builder, &config_yaml).unwrap(); // Assert // Since the MeterProviderBuilder does not expose its internal state, @@ -172,4 +141,29 @@ mod tests { &opentelemetry_sdk::metrics::SdkMeterProvider::builder() )); } + + #[test] + fn test_console_provider_invalid_configuration() { + // Arrange + let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); + let invalid_config_yaml = serde_yaml::from_str::( + r#" + temporality: invalid_value + "#, + ) + .unwrap(); + + // Act + let result = register_console_exporter(meter_provider_builder, &invalid_config_yaml); + + // Assert + match result { + Err(ConfigurationError::InvalidConfiguration(details)) => { + assert!( + details.contains("Failed to deserialize PeriodicExporterConsole configuration") + ); + } + _ => panic!("Expected InvalidConfiguration error"), + } + } } diff --git a/opentelemetry-config/README.md b/opentelemetry-config/README.md index 81609981..b0e689d2 100644 --- a/opentelemetry-config/README.md +++ b/opentelemetry-config/README.md @@ -50,63 +50,82 @@ resource: service.version: "1.0.0" ``` -### 2. Implement an Exporter Provider +### 2. Implement an Exporter Factory ```rust -use opentelemetry_config::{ - ConfigurationProvidersRegistry, - MetricsExporterId, - MetricsReaderPeriodicExporterProvider, +use opentelemetry_config::{ConfigurationProvidersRegistry, ConfigurationError}; +use opentelemetry_sdk::{ + error::OTelSdkResult, + metrics::{MeterProviderBuilder, data::ResourceMetrics, exporter::PushMetricExporter}, }; -use opentelemetry_sdk::metrics::MeterProviderBuilder; use serde_yaml::Value; +use std::time::Duration; -struct CustomExporterProvider; +// Define your custom configuration model +#[derive(Debug, serde::Deserialize)] +pub struct CustomConfig { + pub custom_string_field: String, + pub custom_int_field: i32, +} -impl CustomExporterProvider { - pub fn register_into(registry: &mut ConfigurationProvidersRegistry) { - let key = MetricsExporterId::PeriodicExporter - .qualified_name("custom"); - registry - .metrics_mut() - .register_periodic_exporter_provider(key, Box::new(Self)); - } +// Implement your custom exporter +pub struct CustomExporter { + config: Option, } -impl MetricsReaderPeriodicExporterProvider for CustomExporterProvider { - fn provide( - &self, - meter_provider_builder: MeterProviderBuilder, - _config: &Value, - ) -> MeterProviderBuilder { - let exporter = opentelemetry_stdout::MetricExporter::builder() - .build(); - - meter_provider_builder.with_periodic_exporter(exporter) +impl CustomExporter { + fn new() -> Self { + Self { config: None } } - fn as_any(&self) -> &dyn std::any::Any { - self + pub fn set_config(&mut self, config: CustomConfig) { + self.config = Some(config); } } + +impl PushMetricExporter for CustomExporter { + // PushMetricExporter methods... +} + +// Factory function that creates and configures the exporter +fn register_custom_exporter( + mut builder: MeterProviderBuilder, + config: &Value, +) -> Result { + let mut exporter = CustomExporter::new(); + + // Deserialize your custom config from YAML + let custom_config = serde_yaml::from_value::(config.clone()) + .map_err(|e| ConfigurationError::InvalidConfiguration(e.to_string()))?; + + exporter.set_config(custom_config); + builder = builder.with_periodic_exporter(exporter); + + Ok(builder) +} ``` -### 3. Load and Apply Configuration +### 3. Register and Use Configuration ```rust use opentelemetry_config::providers::TelemetryProvider; fn main() -> Result<(), Box> { - // Create a configuration registry and register exporters + // Create a configuration registry let mut registry = ConfigurationProvidersRegistry::new(); - // Register the custom exporter provider - CustomExporterProvider::register_into(&mut registry); + // Register the custom exporter factory function + registry + .metrics_mut() + .register_periodic_exporter_factory( + "custom".to_string(), + register_custom_exporter, + ); // Load configuration from YAML file let telemetry_provider = TelemetryProvider::new(); let providers = telemetry_provider - .provide_from_yaml_file(®istry, "otel-config.yaml")?; + .configure_from_yaml_file(®istry, "otel-config.yaml")?; // Use the configured providers if let Some(meter_provider) = providers.meter_provider() { @@ -125,26 +144,26 @@ fn main() -> Result<(), Box> { ### Core Components - **`ConfigurationProvidersRegistry`**: Central registry for configuration providers across all telemetry signals -- **`MetricsProvidersRegistry`**: Registry specifically for metrics exporter providers +- **`MetricsProvidersRegistry`**: Registry specifically for metrics exporter factory functions - **`TelemetryProvider`**: Orchestrates the configuration process from YAML to SDK providers - **`TelemetryProviders`**: Holds configured meter, tracer, and logger providers -- **`MetricsReaderPeriodicExporterProvider`**: Trait for implementing custom metric exporter providers -- **`MetricsExporterId`**: Enum for building qualified registry keys for different exporter types +- **`MetricConfigFactory`**: Type alias for factory functions that create and configure metric exporters +- **`ConfigurationError`**: Error type for configuration and registration failures ### Design Pattern -This crate follows a **provider-based decoupled implementation pattern**: +This crate follows a **factory-based decoupled implementation pattern**: - **Centralized Configuration Model**: The configuration schema (YAML structure and data models) is defined and maintained centrally in this crate, ensuring alignment with the OpenTelemetry Configuration Standard. The general structure (metrics, traces, logs, resource) is enforced to maintain compatibility. -- **Extensible Configuration**: While the top-level structure is controlled, exporter-specific configurations are fully extensible. Providers can define their own configuration schemas that are deserialized from the YAML at runtime, enabling custom properties without modifying the core model. -- **Decoupled Implementations**: Actual exporter implementations live in external crates or user code, allowing the community to contribute custom exporters without modifying the core configuration model. Each provider handles its own configuration deserialization and exporter instantiation. -- **Provider Trait Pattern**: Exporters are registered via provider traits that receive YAML configuration as `serde_yaml::Value`, allowing them to deserialize into any custom configuration structure they need. -- **Registry-Based Discovery**: A central registry maps exporter names to their provider implementations, enabling dynamic configuration. The registry key is built using `MetricsExporterId::qualified_name()` with the exporter name from the YAML. +- **Extensible Configuration**: While the top-level structure is controlled, exporter-specific configurations are fully extensible. Factory functions can define their own configuration schemas that are deserialized from the YAML at runtime, enabling custom properties without modifying the core model. +- **Decoupled Implementations**: Actual exporter implementations live in external crates or user code, allowing the community to contribute custom exporters without modifying the core configuration model. Each factory function handles its own configuration deserialization and exporter instantiation. +- **Factory Function Pattern**: Exporters are registered via factory functions (`Fn(MeterProviderBuilder, &Value) -> Result`) that receive the meter provider builder and YAML configuration, allowing them to deserialize into any custom configuration structure they need. +- **Registry-Based Discovery**: A central registry maps exporter names to their factory functions, enabling dynamic configuration. Exporter names from the YAML are used as registry keys to look up the appropriate factory. - **Community Control**: By keeping the top-level configuration model centralized and standardized, the community maintains consistency across all implementations while enabling complete flexibility for exporter-specific configurations. This design enables: - **Standard Compliance**: All configurations follow the official OpenTelemetry schema at the top level -- **Easy Extension**: Contributors can add new exporters with custom configurations by implementing the provider trait in their own crates +- **Easy Extension**: Contributors can add new exporters with custom configurations by implementing factory functions in their own crates - **Configuration Flexibility**: Each exporter can define its own configuration structure without requiring changes to the core crate - **Version Independence**: Exporter implementations and their configurations can evolve independently from the core configuration schema - **Mixed Exporters**: Users can combine official and custom exporters using the same configuration format @@ -161,14 +180,15 @@ The configuration is structured around the `Telemetry` model which includes: ## Examples -### Console Exporter Example +### Custom Exporter Example See the [examples/custom](examples/custom) directory for a complete working example that demonstrates: -- Implementing a custom exporter provider -- Registering the provider with the configuration registry +- Implementing a custom exporter with `PushMetricExporter` trait +- Defining a custom configuration structure +- Creating a factory function that deserializes config and creates the exporter +- Registering the factory with the configuration registry - Loading configuration from a YAML file -- Configuring a meter provider - Proper shutdown handling To run the example: @@ -209,7 +229,7 @@ resource: To add support for a custom exporter: -1. Define your exporter configuration model (optional): +### 1. Define your exporter configuration model (optional): ```rust #[derive(Debug, serde::Deserialize)] @@ -219,51 +239,68 @@ pub struct MyCustomConfig { } ``` -2. Implement the provider trait: +### 2. Implement your exporter with `PushMetricExporter`: ```rust -use opentelemetry_config::MetricsReaderPeriodicExporterProvider; +use opentelemetry_sdk::{ + error::OTelSdkResult, + metrics::{data::ResourceMetrics, exporter::PushMetricExporter}, +}; + +pub struct MyCustomExporter { + config: MyCustomConfig, +} + +impl MyCustomExporter { + fn new(config: MyCustomConfig) -> Self { + Self { config } + } +} + +impl PushMetricExporter for MyCustomExporter { + // PushMetricExporter methods... +} +``` + +### 3. Create a factory function: + +```rust +use opentelemetry_config::ConfigurationError; use opentelemetry_sdk::metrics::MeterProviderBuilder; use serde_yaml::Value; -struct MyCustomExporterProvider; - -impl MetricsReaderPeriodicExporterProvider for MyCustomExporterProvider { - fn provide( - &self, - meter_provider_builder: MeterProviderBuilder, - config: &Value, - ) -> MeterProviderBuilder { - // Deserialize your custom config - let custom_config = serde_yaml::from_value::(config.clone()) - .expect("Failed to deserialize custom config"); - - // Build your exporter with the config - let exporter = MyExporter::new(&custom_config); - meter_provider_builder.with_periodic_exporter(exporter) - } +fn create_my_custom_exporter( + mut builder: MeterProviderBuilder, + config: &Value, +) -> Result { + // Deserialize your custom config + let custom_config = serde_yaml::from_value::(config.clone()) + .map_err(|e| ConfigurationError::InvalidConfiguration(e.to_string()))?; - fn as_any(&self) -> &dyn std::any::Any { - self - } + // Create and configure your exporter + let exporter = MyCustomExporter::new(custom_config); + builder = builder.with_periodic_exporter(exporter); + + Ok(builder) } ``` -3. Register it with the ConfigurationProvidersRegistry: +### 4. Register it with the ConfigurationProvidersRegistry: ```rust -use opentelemetry_config::MetricsExporterId; +use opentelemetry_config::ConfigurationProvidersRegistry; let mut registry = ConfigurationProvidersRegistry::new(); -let key = MetricsExporterId::PeriodicExporter - .qualified_name("my-custom-exporter"); registry .metrics_mut() - .register_periodic_exporter_provider(key, Box::new(MyCustomExporterProvider)); + .register_periodic_exporter_factory( + "my-custom-exporter".to_string(), + create_my_custom_exporter, + ); ``` -4. Use it in your YAML configuration: +### 5. Use it in your YAML configuration: ```yaml metrics: @@ -275,26 +312,6 @@ metrics: timeout: 5000 ``` -### Helper Method Pattern - -For cleaner registration, add a helper method to your provider: - -```rust -impl MyCustomExporterProvider { - pub fn register_into(registry: &mut ConfigurationProvidersRegistry) { - let key = MetricsExporterId::PeriodicExporter - .qualified_name("my-custom-exporter"); - registry - .metrics_mut() - .register_periodic_exporter_provider(key, Box::new(Self)); - } -} - -// Usage: -let mut registry = ConfigurationProvidersRegistry::new(); -MyCustomExporterProvider::register_into(&mut registry); -``` - ## Current Limitations - Only metrics configuration is currently implemented @@ -310,4 +327,4 @@ This project is licensed under the Apache-2.0 license. ## Release Notes -You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust-contrib/tree/main/opentelemetry-config/CHANGELOG.md). +You can find the release notes (changelog) [here](CHANGELOG.md). diff --git a/opentelemetry-config/examples/custom/src/main.rs b/opentelemetry-config/examples/custom/src/main.rs index 22ede33b..5ff5a11b 100644 --- a/opentelemetry-config/examples/custom/src/main.rs +++ b/opentelemetry-config/examples/custom/src/main.rs @@ -5,14 +5,12 @@ //! It is helpful to implement and test custom exporters. use opentelemetry_config::{ - providers::TelemetryProvider, ConfigurationProvidersRegistry, MetricsExporterId, - MetricsReaderPeriodicExporterProvider, + providers::TelemetryProvider, ConfigurationError, ConfigurationProvidersRegistry, }; use opentelemetry_sdk::{ error::OTelSdkResult, metrics::{data::ResourceMetrics, exporter::PushMetricExporter, MeterProviderBuilder}, }; -use serde_yaml::Value; use std::env; use std::time::Duration; @@ -35,11 +33,15 @@ pub fn main() -> Result<(), Box> { let mut registry = ConfigurationProvidersRegistry::new(); // Register the custom exporter provider. - MockPeriodicExporterProvider::register_into(&mut registry); + registry.metrics_mut().register_periodic_exporter_factory( + "custom".to_string(), + MockPeriodicExporterProvider::register_mock_exporter, + ); + // Configure telemetry from the provided YAML file. let telemetry_provider = TelemetryProvider::new(); let providers = telemetry_provider - .provide_from_yaml_file(®istry, config_file) + .configure_from_yaml_file(®istry, config_file) .unwrap(); if let Some(meter_provider) = providers.meter_provider() { @@ -69,18 +71,36 @@ pub fn main() -> Result<(), Box> { pub struct MockPeriodicExporterProvider {} impl MockPeriodicExporterProvider { - fn new() -> Self { - Self {} - } + pub fn register_mock_exporter( + mut meter_provider_builder: MeterProviderBuilder, + config: &serde_yaml::Value, + ) -> Result { + let mut exporter = MockCustomExporter::new(); + + let config = serde_yaml::from_value::(config.clone()).map_err(|e| { + ConfigurationError::InvalidConfiguration(format!( + "Failed to parse MockCustomConfig: {}", + e + )) + })?; + println!( + "Configuring MockCustomExporter with string field: {} and int field: {}", + config.custom_string_field, config.custom_int_field + ); + + exporter.set_custom_config(config); - pub fn register_into(registry: &mut ConfigurationProvidersRegistry) { - let key = MetricsExporterId::PeriodicExporter.qualified_name("custom"); - registry - .metrics_mut() - .register_periodic_exporter_provider(key, Box::new(Self::new())); + meter_provider_builder = meter_provider_builder.with_periodic_exporter(exporter); + Ok(meter_provider_builder) } } +#[derive(serde::Deserialize, Debug)] +pub struct MockCustomConfig { + pub custom_string_field: String, + pub custom_int_field: i32, +} + pub struct MockCustomExporter { custom_config: Option, } @@ -127,35 +147,3 @@ impl PushMetricExporter for MockCustomExporter { opentelemetry_sdk::metrics::Temporality::Cumulative } } - -#[derive(serde::Deserialize, Debug)] -pub struct MockCustomConfig { - pub custom_string_field: String, - pub custom_int_field: i32, -} - -impl MetricsReaderPeriodicExporterProvider for MockPeriodicExporterProvider { - fn provide( - &self, - mut meter_provider_builder: MeterProviderBuilder, - config: &Value, - ) -> MeterProviderBuilder { - let mut exporter = MockCustomExporter::new(); - - let config = serde_yaml::from_value::(config.clone()) - .expect("Failed to deserialize MockCustomConfig"); - println!( - "Configuring MockCustomExporter with string field: {} and int field: {}", - config.custom_string_field, config.custom_int_field - ); - - exporter.set_custom_config(config); - - meter_provider_builder = meter_provider_builder.with_periodic_exporter(exporter); - meter_provider_builder - } - - fn as_any(&self) -> &dyn std::any::Any { - self - } -} diff --git a/opentelemetry-config/src/lib.rs b/opentelemetry-config/src/lib.rs index afc7f110..8d08f345 100644 --- a/opentelemetry-config/src/lib.rs +++ b/opentelemetry-config/src/lib.rs @@ -6,7 +6,7 @@ use std::{ collections::HashMap, - error, + error::{self, Error}, fmt::{self, Display}, }; @@ -49,35 +49,31 @@ impl Default for ConfigurationProvidersRegistry { /// Registry for metrics configuration providers. pub struct MetricsProvidersRegistry { - readers_periodic_exporters_providers: - HashMap>, + periodic_exporter_factories: HashMap>, + // TODO: Add other types of providers registries. } impl MetricsProvidersRegistry { pub fn new() -> Self { Self { - readers_periodic_exporters_providers: HashMap::new(), + periodic_exporter_factories: HashMap::new(), } } - pub fn register_periodic_exporter_provider( + pub fn register_periodic_exporter_factory( &mut self, - key: String, - provider: Box, + name: String, + factory: impl Fn(MeterProviderBuilder, &Value) -> Result + + Send + + Sync + + 'static, ) { - self.readers_periodic_exporters_providers.insert( - key, - provider as Box, - ); + self.periodic_exporter_factories + .insert(name, Box::new(factory)); } - pub fn readers_periodic_exporter_provider( - &self, - exporter_id: &str, - ) -> Option<&dyn MetricsReaderPeriodicExporterProvider> { - self.readers_periodic_exporters_providers - .get(exporter_id) - .map(|b| b.as_ref()) + pub fn periodic_exporter_factory(&self, name: &str) -> Option<&Box> { + self.periodic_exporter_factories.get(name) } } @@ -87,29 +83,35 @@ impl Default for MetricsProvidersRegistry { } } -/// Enum representing different metrics exporter identifiers. -pub enum MetricsExporterId { - PeriodicExporter, +/// Errors reported by the component factory. +#[derive(Debug)] +pub enum ConfigurationError { + /// Indicates an invalid configuration was provided. + InvalidConfiguration(String), + + /// Indicates an error occurred while registering a component. + RegistrationError(String), } -impl MetricsExporterId { - pub fn qualified_name(self, exporter_name: &str) -> String { +impl Error for ConfigurationError {} + +impl fmt::Display for ConfigurationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::PeriodicExporter => format!("readers::periodic::exporter::{}", exporter_name), + ConfigurationError::InvalidConfiguration(details) => { + write!(f, "Invalid configuration: {}", details) + } + ConfigurationError::RegistrationError(details) => { + write!(f, "Registration error: {}", details) + } } } } -/// Trait for providing metrics reader periodic exporter configurations. -pub trait MetricsReaderPeriodicExporterProvider { - fn provide( - &self, - meter_provider_builder: MeterProviderBuilder, - config: &Value, - ) -> MeterProviderBuilder; - - fn as_any(&self) -> &dyn std::any::Any; -} +/// Type alias for metric configuration factory functions +pub type MetricConfigFactory = dyn Fn(MeterProviderBuilder, &Value) -> Result + + Send + + Sync; /// Holds the configured telemetry providers pub struct TelemetryProviders { @@ -167,6 +169,7 @@ pub enum ProviderError { InvalidConfiguration(String), UnsupportedExporter(String), NotRegisteredProvider(String), + RegistrationError(String), } impl error::Error for ProviderError {} @@ -183,64 +186,82 @@ impl Display for ProviderError { ProviderError::NotRegisteredProvider(details) => { write!(f, "Not registered provider: {}", details) } + ProviderError::RegistrationError(details) => { + write!(f, "Registration error: {}", details) + } } } } #[cfg(test)] mod tests { - use std::cell::Cell; + use std::sync::{atomic::AtomicI16, Arc}; + + use opentelemetry_sdk::{ + error::OTelSdkResult, + metrics::{data::ResourceMetrics, exporter::PushMetricExporter}, + }; use super::*; #[test] fn test_register_periodic_exporter_provider() { // Arrange - struct MockPeriodicExporterProvider { - call_count: Cell, - } + struct MockPeriodicExporter {} - impl MockPeriodicExporterProvider { - fn new() -> Self { - Self { - call_count: Cell::new(0), - } + impl PushMetricExporter for MockPeriodicExporter { + async fn export(&self, _metrics: &ResourceMetrics) -> OTelSdkResult { + Ok(()) } - pub fn get_call_count(&self) -> i16 { - self.call_count.get() + fn force_flush(&self) -> OTelSdkResult { + Ok(()) } - } - impl MetricsReaderPeriodicExporterProvider for MockPeriodicExporterProvider { - fn provide( - &self, - meter_provider_builder: MeterProviderBuilder, - _config: &Value, - ) -> MeterProviderBuilder { - self.call_count.set(self.call_count.get() + 1); - meter_provider_builder + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) } - fn as_any(&self) -> &dyn std::any::Any { - self + fn shutdown(&self) -> OTelSdkResult { + Ok(()) } + + fn temporality(&self) -> opentelemetry_sdk::metrics::Temporality { + opentelemetry_sdk::metrics::Temporality::Cumulative + } + } + + let call_count = Arc::new(AtomicI16::new(0)); + let call_count_clone = Arc::clone(&call_count); + + // Wrapper clousure to capture call_count_clone + let register_mock_exporter_clousure = + move |builder: MeterProviderBuilder, _config: &serde_yaml::Value| { + call_count_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + register_mock_exporter(builder, _config) + }; + + pub fn register_mock_exporter( + mut builder: MeterProviderBuilder, + _config: &serde_yaml::Value, + ) -> Result { + builder = builder.with_periodic_exporter(MockPeriodicExporter {}); + Ok(builder) } - let mock_provider = Box::new(MockPeriodicExporterProvider::new()); let mut registry = ConfigurationProvidersRegistry::new(); // Act - let key = MetricsExporterId::PeriodicExporter.qualified_name("console"); + let name = "console"; registry .metrics_mut() - .register_periodic_exporter_provider(key.clone(), mock_provider); + .register_periodic_exporter_factory(name.to_string(), register_mock_exporter_clousure); // Assert assert!(registry .metrics() - .readers_periodic_exporters_providers - .contains_key(key.as_str())); + .periodic_exporter_factories + .contains_key(name)); let console_config = serde_yaml::to_value( r#" @@ -250,14 +271,12 @@ mod tests { ) .unwrap(); - let provider_option = registry.metrics().readers_periodic_exporter_provider(&key); - if let Some(provider) = provider_option { - provider.provide(MeterProviderBuilder::default(), &console_config); - let provider_cast = provider - .as_any() - .downcast_ref::() - .unwrap(); - assert_eq!(provider_cast.get_call_count(), 1); + let factory_function_option = registry.metrics().periodic_exporter_factory(&name); + if let Some(factory_function) = factory_function_option { + let builder = MeterProviderBuilder::default(); + _ = factory_function(builder, &console_config).unwrap(); + // Verify that the factory function was called + assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 1); } else { panic!("Provider not found"); } @@ -268,15 +287,15 @@ mod tests { let provider_manager = ConfigurationProvidersRegistry::default(); assert!(provider_manager .metrics() - .readers_periodic_exporters_providers + .periodic_exporter_factories .is_empty()); } #[test] - fn test_metrics_provider_manager_default() { - let metrics_provider_manager = MetricsProvidersRegistry::default(); - assert!(metrics_provider_manager - .readers_periodic_exporters_providers + fn test_metrics_provider_registry_default() { + let metrics_provider_registry = MetricsProvidersRegistry::default(); + assert!(metrics_provider_registry + .periodic_exporter_factories .is_empty()); } @@ -294,11 +313,10 @@ mod tests { let traces_provider = SdkTracerProvider::builder().build(); let logs_provider = SdkLoggerProvider::builder().build(); - let telemetry_providers = TelemetryProviders { - meter_provider: Some(meter_provider), - traces_provider: Some(traces_provider), - logs_provider: Some(logs_provider), - }; + let telemetry_providers = TelemetryProviders::new() + .with_logs_provider(logs_provider) + .with_traces_provider(traces_provider) + .with_meter_provider(meter_provider); assert!(telemetry_providers.meter_provider().is_some()); assert!(telemetry_providers.traces_provider().is_some()); diff --git a/opentelemetry-config/src/model.rs b/opentelemetry-config/src/model.rs index b4dadc93..f3430bc2 100644 --- a/opentelemetry-config/src/model.rs +++ b/opentelemetry-config/src/model.rs @@ -32,15 +32,15 @@ mod tests { #[test] fn test_deserialize_telemetry() { let yaml_str = r#" -metrics: - readers: - - periodic: - exporter: - console: {} -resource: - service.name: "example-service" - service.version: "1.0.0" -"#; + metrics: + readers: + - periodic: + exporter: + console: {} + resource: + service.name: "example-service" + service.version: "1.0.0" + "#; let telemetry: Telemetry = serde_yaml::from_str(yaml_str).unwrap(); assert!(telemetry.metrics.is_some()); let resource = telemetry.resource; @@ -51,15 +51,15 @@ resource: #[test] fn test_deserialize_invalid_telemetry() { let yaml_str = r#" -metrics: - readers: - - periodic: - exporter_invalid_field: - console: {} -resource: - service.name: "example-service" - service.version: "1.0.0" -"#; + metrics: + readers: + - periodic: + exporter_invalid_field: + console: {} + resource: + service.name: "example-service" + service.version: "1.0.0" + "#; let telemetry_result: Result = serde_yaml::from_str(yaml_str); if let Err(e) = telemetry_result { diff --git a/opentelemetry-config/src/providers.rs b/opentelemetry-config/src/providers.rs index f419bff8..475575a5 100644 --- a/opentelemetry-config/src/providers.rs +++ b/opentelemetry-config/src/providers.rs @@ -28,7 +28,8 @@ impl TelemetryProvider { } } - pub fn provide( + /// Configures the Telemetry providers based on the provided configuration + pub fn configure( &self, configuration_registry: &ConfigurationProvidersRegistry, config: &Telemetry, @@ -38,7 +39,7 @@ impl TelemetryProvider { if let Some(metrics_config) = &config.metrics { let mut meter_provider_builder = SdkMeterProvider::builder().with_resource(resource.clone()); - meter_provider_builder = self.metrics_provider.provide( + meter_provider_builder = self.metrics_provider.configure( configuration_registry.metrics(), meter_provider_builder, metrics_config, @@ -52,7 +53,8 @@ impl TelemetryProvider { Ok(providers) } - pub fn provide_from_yaml( + /// Configures the Telemetry providers from a YAML string + pub fn configure_from_yaml( &self, configuration_registry: &ConfigurationProvidersRegistry, yaml_str: &str, @@ -63,10 +65,11 @@ impl TelemetryProvider { e )) })?; - self.provide(configuration_registry, &config) + self.configure(configuration_registry, &config) } - pub fn provide_from_yaml_file( + /// Configures the Telemetry providers from a YAML file + pub fn configure_from_yaml_file( &self, configuration_registry: &ConfigurationProvidersRegistry, file_path: &str, @@ -77,7 +80,7 @@ impl TelemetryProvider { e )) })?; - self.provide_from_yaml(configuration_registry, &yaml_str) + self.configure_from_yaml(configuration_registry, &yaml_str) } /// Converts resource attributes from HashMap to Resource @@ -119,48 +122,57 @@ impl Default for TelemetryProvider { #[cfg(test)] mod tests { - use std::cell::Cell; - - use opentelemetry_sdk::metrics::MeterProviderBuilder; - use serde_yaml::Value; - - use crate::{MetricsExporterId, MetricsReaderPeriodicExporterProvider}; + use crate::ConfigurationError; + use opentelemetry_sdk::{ + error::OTelSdkResult, + metrics::{ + data::ResourceMetrics, exporter::PushMetricExporter, MeterProviderBuilder, Temporality, + }, + }; use super::*; - struct MockMetricsReadersPeriodicExporterConsoleProvider { - call_count: Cell, + struct MockExporter {} + impl MockExporter { + fn new() -> Self { + Self {} + } } - impl MockMetricsReadersPeriodicExporterConsoleProvider { - fn new() -> Self { - Self { - call_count: Cell::new(0), - } + impl PushMetricExporter for MockExporter { + fn export( + &self, + _metrics: &ResourceMetrics, + ) -> impl std::future::Future + Send { + async move { Ok(()) } } - pub fn get_call_count(&self) -> u16 { - self.call_count.get() + fn force_flush(&self) -> OTelSdkResult { + Ok(()) } - } - impl MetricsReaderPeriodicExporterProvider for MockMetricsReadersPeriodicExporterConsoleProvider { - fn provide( - &self, - meter_provider_builder: MeterProviderBuilder, - config: &Value, - ) -> MeterProviderBuilder { - // Mock implementation: In a real scenario, configure the console exporter here - self.call_count.set(self.call_count.get() + 1); - println!("Mock configure called with config: {:?}", config); - meter_provider_builder + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } + + fn temporality(&self) -> Temporality { + Temporality::Delta } - fn as_any(&self) -> &dyn std::any::Any { - self + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(std::time::Duration::from_secs(5)) } } + pub fn register_mock_exporter( + mut builder: MeterProviderBuilder, + _config: &serde_yaml::Value, + ) -> Result { + let exporter = MockExporter::new(); + builder = builder.with_periodic_exporter(exporter); + Ok(builder) + } + #[test] fn test_configure_telemetry_from_yaml() { let yaml_str = r#" @@ -178,28 +190,17 @@ mod tests { development: true "#; - let provider = Box::new(MockMetricsReadersPeriodicExporterConsoleProvider::new()); - let mut configuration_registry = ConfigurationProvidersRegistry::new(); let metrics_provider_manager = configuration_registry.metrics_mut(); - let key = MetricsExporterId::PeriodicExporter.qualified_name("console"); - metrics_provider_manager.register_periodic_exporter_provider(key.clone(), provider); + let name = "console"; + metrics_provider_manager + .register_periodic_exporter_factory(name.to_string(), register_mock_exporter); let telemetry_provider = TelemetryProvider::new(); let providers = telemetry_provider - .provide_from_yaml(&configuration_registry, yaml_str) + .configure_from_yaml(&configuration_registry, yaml_str) .unwrap(); assert!(providers.meter_provider.is_some()); - - let provider = configuration_registry - .metrics() - .readers_periodic_exporter_provider(&key) - .unwrap(); - let provider = provider - .as_any() - .downcast_ref::() - .unwrap(); - assert_eq!(provider.get_call_count(), 1); } #[test] @@ -211,7 +212,18 @@ mod tests { metrics: None, }; let providers = telemetry_provider - .provide(&configuration_registry, &telemetry) + .configure(&configuration_registry, &telemetry) + .unwrap(); + assert!(providers.meter_provider.is_none()); + } + + #[test] + fn test_telemetry_provider_default_empty_yaml() { + let telemetry_provider = TelemetryProvider::default(); + let configuration_registry = ConfigurationProvidersRegistry::default(); + let telemetry: Telemetry = serde_yaml::from_str("").unwrap(); + let providers = telemetry_provider + .configure(&configuration_registry, &telemetry) .unwrap(); assert!(providers.meter_provider.is_none()); } diff --git a/opentelemetry-config/src/providers/metrics_provider.rs b/opentelemetry-config/src/providers/metrics_provider.rs index 2403b116..d11e1952 100644 --- a/opentelemetry-config/src/providers/metrics_provider.rs +++ b/opentelemetry-config/src/providers/metrics_provider.rs @@ -23,8 +23,8 @@ impl MetricsProvider { } } - /// Provisions the Metrics provider based on the provided configuration - pub fn provide( + /// Configures the Metrics provider based on the provided configuration + pub fn configure( &self, metrics_registry: &MetricsProvidersRegistry, mut meter_provider_builder: MeterProviderBuilder, @@ -33,7 +33,7 @@ impl MetricsProvider { for reader in &config.readers { meter_provider_builder = self.reader_provider - .provide(metrics_registry, meter_provider_builder, reader)?; + .configure(metrics_registry, meter_provider_builder, reader)?; } Ok(meter_provider_builder) @@ -45,3 +45,58 @@ impl Default for MetricsProvider { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::metrics::Metrics; + use opentelemetry_sdk::metrics::SdkMeterProvider; + use serde_yaml; + + #[test] + fn test_configure_metrics_provider() { + let yaml_str = r#" + readers: + - periodic: + exporter: + custom: {} + "#; + let metrics_config: Metrics = serde_yaml::from_str(yaml_str).unwrap(); + let mut registry = MetricsProvidersRegistry::new(); + + fn mock_factory( + builder: MeterProviderBuilder, + _config: &serde_yaml::Value, + ) -> Result { + Ok(builder) + } + + registry.register_periodic_exporter_factory("custom".to_string(), mock_factory); + let meter_provider_builder = SdkMeterProvider::builder(); + let metrics_provider = MetricsProvider::new(); + let result = metrics_provider.configure(®istry, meter_provider_builder, &metrics_config); + assert!(result.is_ok()); + } + + #[test] + fn test_configure_metrics_provider_with_unknown_exporter() { + let yaml_str = r#" + readers: + - periodic: + exporter: + unknown_exporter: {} + "#; + let metrics_config: Metrics = serde_yaml::from_str(yaml_str).unwrap(); + let registry = MetricsProvidersRegistry::default(); + let meter_provider_builder = SdkMeterProvider::builder(); + let metrics_provider = MetricsProvider::new(); + let result = metrics_provider.configure(®istry, meter_provider_builder, &metrics_config); + match result { + Err(ProviderError::NotRegisteredProvider(details)) => { + println!("Error details: {}", details); + assert!(details.contains("unknown_exporter")) + } + _ => panic!("Expected UnknownExporter error"), + } + } +} diff --git a/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs b/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs index ca469a06..4f5d9f20 100644 --- a/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs +++ b/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs @@ -6,9 +6,7 @@ use opentelemetry_sdk::metrics::MeterProviderBuilder; use serde_yaml::Value; -use crate::{ - model::metrics::reader::Reader, MetricsExporterId, MetricsProvidersRegistry, ProviderError, -}; +use crate::{model::metrics::reader::Reader, MetricsProvidersRegistry, ProviderError}; /// Provider for Metrics readers pub struct ReaderProvider { @@ -21,8 +19,8 @@ impl ReaderProvider { periodic_reader_provider: PeriodicReaderProvider::default(), } } - /// Provisions a metrics reader based on the provided configuration - pub fn provide( + /// Configures a metrics reader based on the provided configuration + pub fn configure( &self, metrics_registry: &MetricsProvidersRegistry, mut meter_provider_builder: MeterProviderBuilder, @@ -30,7 +28,7 @@ impl ReaderProvider { ) -> Result { match config { crate::model::metrics::reader::Reader::Periodic(periodic_config) => { - meter_provider_builder = self.periodic_reader_provider.provide( + meter_provider_builder = self.periodic_reader_provider.configure( metrics_registry, meter_provider_builder, periodic_config, @@ -63,14 +61,14 @@ impl PeriodicReaderProvider { } } - /// Provisions a periodic metrics reader based on the provided configuration - pub fn provide( + /// Configures a periodic metrics reader based on the provided configuration + pub fn configure( &self, metrics_registry: &MetricsProvidersRegistry, mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, config: &crate::model::metrics::reader::Periodic, ) -> Result { - meter_provider_builder = self.periodic_exporter_provider.provide( + meter_provider_builder = self.periodic_exporter_provider.configure( metrics_registry, meter_provider_builder, &config.exporter, @@ -94,12 +92,8 @@ impl PeriodicExporterProvider { PeriodicExporterProvider {} } - fn registry_key(&self, exporter_name: &str) -> String { - MetricsExporterId::PeriodicExporter.qualified_name(exporter_name) - } - /// Configures a periodic metrics exporter based on the provided configuration - pub fn provide( + pub fn configure( &self, metrics_registry: &MetricsProvidersRegistry, mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, @@ -110,20 +104,34 @@ impl PeriodicExporterProvider { for key in exporter_map.keys() { match key { Value::String(exporter_name) => { - let registry_key = self.registry_key(exporter_name); - let exporter_provider_option = - metrics_registry.readers_periodic_exporter_provider(®istry_key); - match exporter_provider_option { - Some(provider) => { + let exporter_factory_option = + metrics_registry.periodic_exporter_factory(&exporter_name); + match exporter_factory_option { + Some(factory_function) => { let config = &exporter_map[&Value::String(exporter_name.clone())]; - meter_provider_builder = - provider.provide(meter_provider_builder, config); + let meter_provider_builder_result = + factory_function(meter_provider_builder, config); + meter_provider_builder = match meter_provider_builder_result { + Ok(builder) => builder, + Err(e) => match e { + crate::ConfigurationError::InvalidConfiguration( + msg, + ) => { + return Err(ProviderError::InvalidConfiguration( + msg, + )); + } + crate::ConfigurationError::RegistrationError(msg) => { + return Err(ProviderError::RegistrationError(msg)); + } + }, + }; } None => { return Err(ProviderError::NotRegisteredProvider(format!( - "No provider found for periodic exporter: {}. Make sure it is registered as provider.", - registry_key + "No provider found for periodic exporter '{}'. Make sure it is registered with its factory.", + exporter_name ))); } } @@ -223,49 +231,25 @@ impl Default for PullExporterProvider { #[cfg(test)] mod tests { - use opentelemetry_sdk::metrics::SdkMeterProvider; - - use crate::{model::metrics::reader::PullExporter, MetricsReaderPeriodicExporterProvider}; - use super::*; + use crate::{model::metrics::reader::PullExporter, ConfigurationError}; + use opentelemetry_sdk::metrics::SdkMeterProvider; - struct MockMetricsReadersPeriodicExporterConsoleProvider {} - - impl MockMetricsReadersPeriodicExporterConsoleProvider { - fn new() -> Self { - MockMetricsReadersPeriodicExporterConsoleProvider {} - } - - fn register_into(registry: &mut crate::ConfigurationProvidersRegistry) { - let key = MetricsExporterId::PeriodicExporter.qualified_name("console"); - registry - .metrics_mut() - .register_periodic_exporter_provider(key, Box::new(Self::new())); - } - } - - impl MetricsReaderPeriodicExporterProvider for MockMetricsReadersPeriodicExporterConsoleProvider { - fn provide( - &self, - meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, - _config: &Value, - ) -> opentelemetry_sdk::metrics::MeterProviderBuilder { - // Mock implementation: just return the builder as is - meter_provider_builder - } - - fn as_any(&self) -> &dyn std::any::Any { - todo!() - } + pub fn register_mock_exporter( + builder: MeterProviderBuilder, + _config: &serde_yaml::Value, + ) -> Result { + // Mock implementation: just return the builder as is + Ok(builder) } #[test] fn test_reader_provider_configure() { let provider = ReaderProvider::default(); let mut configuration_registry = crate::ConfigurationProvidersRegistry::new(); - MockMetricsReadersPeriodicExporterConsoleProvider::register_into( - &mut configuration_registry, - ); + configuration_registry + .metrics_mut() + .register_periodic_exporter_factory("console".to_string(), register_mock_exporter); let meter_provider_builder = SdkMeterProvider::builder(); let console_object = serde_yaml::from_str( @@ -284,12 +268,12 @@ mod tests { let metrics_registry = configuration_registry.metrics(); _ = provider - .provide(metrics_registry, meter_provider_builder, &config) + .configure(metrics_registry, meter_provider_builder, &config) .unwrap(); } #[test] - fn test_reader_provider_provide_console_provider_not_registered() { + fn test_reader_provider_configure_console_factory_not_registered() { let provider = ReaderProvider::default(); let metrics_registry = MetricsProvidersRegistry::new(); let meter_provider_builder = SdkMeterProvider::builder(); @@ -308,18 +292,19 @@ mod tests { }, ); - let result = provider.provide(&metrics_registry, meter_provider_builder, &config); + let result = provider.configure(&metrics_registry, meter_provider_builder, &config); if let Err(e) = result { - assert!(e.to_string().contains( - "No provider found for periodic exporter: readers::periodic::exporter::console" - )); + println!("Error: {}", e); + assert!(e + .to_string() + .contains("No provider found for periodic exporter 'console'")); } else { panic!("Expected error due to missing provider, but got Ok"); } } #[test] - fn test_reader_provider_provide_otlp_provider_not_registered() { + fn test_reader_provider_provide_otlp_factory_not_registered() { let provider = ReaderProvider::new(); let metrics_registry = MetricsProvidersRegistry::new(); let meter_provider_builder = SdkMeterProvider::builder(); @@ -338,11 +323,11 @@ mod tests { }, ); - let result = provider.provide(&metrics_registry, meter_provider_builder, &config); + let result = provider.configure(&metrics_registry, meter_provider_builder, &config); if let Err(e) = result { - assert!(e.to_string().contains( - "No provider found for periodic exporter: readers::periodic::exporter::otlp" - )); + assert!(e + .to_string() + .contains("No provider found for periodic exporter 'otlp'")); } else { panic!("Expected error due to missing provider, but got Ok"); } From 0e39a7406068d2a046b106313034be6c6af84406 Mon Sep 17 00:00:00 2001 From: Andres Borja Date: Fri, 14 Nov 2025 20:00:50 +0000 Subject: [PATCH 7/7] Add missing new line at the end of Cargo.toml file --- opentelemetry-config/examples/custom/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-config/examples/custom/Cargo.toml b/opentelemetry-config/examples/custom/Cargo.toml index 8b8b0d98..9bb893c5 100644 --- a/opentelemetry-config/examples/custom/Cargo.toml +++ b/opentelemetry-config/examples/custom/Cargo.toml @@ -12,4 +12,4 @@ rust-version = "1.75.0" opentelemetry-config = { path = "../../"} opentelemetry_sdk = { version = "0.31.0" } serde = { version = "1.0", features = ["derive"] } -serde_yaml = { version = "0.9.34" } \ No newline at end of file +serde_yaml = { version = "0.9.34" }