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..e3f50610 --- /dev/null +++ b/opentelemetry-config-stdout/Cargo.toml @@ -0,0 +1,17 @@ +[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" } +serde = { version = "1.0", features = ["derive"] } +serde_yaml = { version = "0.9.34" } + +[lints] +workspace = true diff --git a/opentelemetry-config-stdout/README.md b/opentelemetry-config-stdout/README.md new file mode 100644 index 00000000..311af270 --- /dev/null +++ b/opentelemetry-config-stdout/README.md @@ -0,0 +1,128 @@ +# 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 declarative configuration + +## 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::{ConfigurationProvidersRegistry, providers::TelemetryProvider}; + +fn main() -> Result<(), Box> { + // Create configuration registry and register console exporter + let mut registry = ConfigurationProvidersRegistry::new(); + 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(); + 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 when done. + meter_provider.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 factory +- 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 +``` + +#### 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. + +## License + +This project is licensed under the Apache-2.0 license. + +## Release Notes + +You can find the release notes (changelog) [here](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..3256ae56 --- /dev/null +++ b/opentelemetry-config-stdout/examples/console/Cargo.toml @@ -0,0 +1,13 @@ +[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" } 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..62580a59 --- /dev/null +++ b/opentelemetry-config-stdout/examples/console/src/main.rs @@ -0,0 +1,59 @@ +//! # Example OpenTelemetry Config Console +//! +//! This example demonstrates how to configure OpenTelemetry Metrics +//! using the OpenTelemetry Config crate with a Console Exporter. + +use opentelemetry_config::{providers::TelemetryProvider, ConfigurationProvidersRegistry}; + +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 configuration registry with console exporter provider. + let mut configuration_providers_registry = ConfigurationProvidersRegistry::new(); + 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 + .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..."); + 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/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..3f4dbb59 --- /dev/null +++ b/opentelemetry-config-stdout/src/lib.rs @@ -0,0 +1,169 @@ +//! # OpenTelemetry declarative configuration module for Stdout (Console) exporter +//! +//! This module implements a provider for OpenTelemetry Metrics +//! that enables exporting metrics to the console (stdout) using +//! the OpenTelemetry Config crate. + +use opentelemetry_config::ConfigurationError; +use opentelemetry_sdk::metrics::MeterProviderBuilder; + +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); + } + } + } + + let exporter = exporter_builder.build(); + builder = builder.with_periodic_exporter(exporter); + Ok(builder) +} + +#[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, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_console_provider_registration() { + // Arrange + let mut configuration_registry = + opentelemetry_config::ConfigurationProvidersRegistry::new(); + + // Act + let metrics_registry = configuration_registry.metrics_mut(); + metrics_registry + .register_periodic_exporter_factory("console".to_string(), register_console_exporter); + + // Assert + assert!(metrics_registry + .periodic_exporter_factory("console") + .is_some()); + } + + #[test] + fn test_console_provider_configure_temporality_minimal() { + // Arrange + let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); + + let config = PeriodicExporterConsole { temporality: None }; + + let config_yaml = serde_yaml::to_value(config).unwrap(); + + // Act + let configured_builder = + register_console_exporter(meter_provider_builder, &config_yaml).unwrap(); + + // 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_provider_configure_temporality_delta() { + // Arrange + let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); + + let config = PeriodicExporterConsole { + temporality: Some(Temporality::Delta), + }; + + let config_yaml = serde_yaml::to_value(config).unwrap(); + + // Act + let configured_builder = + register_console_exporter(meter_provider_builder, &config_yaml).unwrap(); + + // 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_provider_configure_temporality_cumulative() { + // Arrange + let meter_provider_builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); + + let config = PeriodicExporterConsole { + temporality: Some(Temporality::Cumulative), + }; + + let config_yaml = serde_yaml::to_value(config).unwrap(); + + // Act + let configured_builder = + register_console_exporter(meter_provider_builder, &config_yaml).unwrap(); + + // 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_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/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..e6aca81a --- /dev/null +++ b/opentelemetry-config/Cargo.toml @@ -0,0 +1,28 @@ +[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" } + +[lints] +workspace = true diff --git a/opentelemetry-config/README.md b/opentelemetry-config/README.md new file mode 100644 index 00000000..b0e689d2 --- /dev/null +++ b/opentelemetry-config/README.md @@ -0,0 +1,330 @@ +# 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. + +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 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 + +## 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: + custom: + temporality: delta + +resource: + service.name: "my-service" + service.version: "1.0.0" +``` + +### 2. Implement an Exporter Factory + +```rust +use opentelemetry_config::{ConfigurationProvidersRegistry, ConfigurationError}; +use opentelemetry_sdk::{ + error::OTelSdkResult, + metrics::{MeterProviderBuilder, data::ResourceMetrics, exporter::PushMetricExporter}, +}; +use serde_yaml::Value; +use std::time::Duration; + +// Define your custom configuration model +#[derive(Debug, serde::Deserialize)] +pub struct CustomConfig { + pub custom_string_field: String, + pub custom_int_field: i32, +} + +// Implement your custom exporter +pub struct CustomExporter { + config: Option, +} + +impl CustomExporter { + fn new() -> Self { + Self { config: None } + } + + 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. Register and Use Configuration + +```rust +use opentelemetry_config::providers::TelemetryProvider; + +fn main() -> Result<(), Box> { + // Create a configuration registry + let mut registry = ConfigurationProvidersRegistry::new(); + + // 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 + .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 meter provider + meter_provider.shutdown()?; + } + + Ok(()) +} +``` + +## Architecture + +### Core Components + +- **`ConfigurationProvidersRegistry`**: Central registry for configuration providers across all telemetry signals +- **`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 +- **`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 **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. 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 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 +- **Type Safety**: Strong typing throughout the configuration pipeline with runtime validation and deserialization errors + +### 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 + +### Custom Exporter Example + +See the [examples/custom](examples/custom) directory for a complete working example that demonstrates: + +- 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 +- Proper shutdown handling + +To run the example: + +```bash +cd examples/custom +cargo run -- --file ../metrics_custom.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 (optional): + +```rust +#[derive(Debug, serde::Deserialize)] +pub struct MyCustomConfig { + pub endpoint: String, + pub timeout: Option, +} +``` + +### 2. Implement your exporter with `PushMetricExporter`: + +```rust +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; + +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()))?; + + // Create and configure your exporter + let exporter = MyCustomExporter::new(custom_config); + builder = builder.with_periodic_exporter(exporter); + + Ok(builder) +} +``` + +### 4. Register it with the ConfigurationProvidersRegistry: + +```rust +use opentelemetry_config::ConfigurationProvidersRegistry; + +let mut registry = ConfigurationProvidersRegistry::new(); + +registry + .metrics_mut() + .register_periodic_exporter_factory( + "my-custom-exporter".to_string(), + create_my_custom_exporter, + ); +``` + +### 5. Use it in your YAML configuration: + +```yaml +metrics: + readers: + - periodic: + exporter: + my-custom-exporter: + endpoint: "http://localhost:4318" + timeout: 5000 +``` + +## 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](CHANGELOG.md). diff --git a/opentelemetry-config/examples/custom/Cargo.toml b/opentelemetry-config/examples/custom/Cargo.toml new file mode 100644 index 00000000..9bb893c5 --- /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" } diff --git a/opentelemetry-config/examples/custom/src/main.rs b/opentelemetry-config/examples/custom/src/main.rs new file mode 100644 index 00000000..5ff5a11b --- /dev/null +++ b/opentelemetry-config/examples/custom/src/main.rs @@ -0,0 +1,149 @@ +//! # Example OpenTelemetry Config Custom exporter +//! +//! This example demonstrates how to configure OpenTelemetry Metrics +//! using the OpenTelemetry Config crate with a Mock Custom Exporter. +//! It is helpful to implement and test custom exporters. + +use opentelemetry_config::{ + providers::TelemetryProvider, ConfigurationError, ConfigurationProvidersRegistry, +}; +use opentelemetry_sdk::{ + error::OTelSdkResult, + metrics::{data::ResourceMetrics, exporter::PushMetricExporter, MeterProviderBuilder}, +}; +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_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_custom.yaml"); + return Ok(()); + } + let config_file = &args[2]; + + // Setup configuration registry with custom exporter provider. + let mut registry = ConfigurationProvidersRegistry::new(); + + // Register the custom exporter provider. + 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 + .configure_from_yaml_file(®istry, config_file) + .unwrap(); + + 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 MockPeriodicExporterProvider {} + +impl MockPeriodicExporterProvider { + 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); + + 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, +} + +impl MockCustomExporter { + fn new() -> Self { + Self { + custom_config: None, + } + } + + pub fn set_custom_config(&mut self, custom_config: MockCustomConfig) { + self.custom_config = Some(custom_config); + } +} + +impl PushMetricExporter for MockCustomExporter { + async fn export(&self, metrics: &ResourceMetrics) -> OTelSdkResult { + println!( + "MockCustomExporter exporting metrics {:?} with custom config: {:?}", + metrics, self.custom_config + ); + Ok(()) + } + + fn force_flush(&self) -> OTelSdkResult { + println!("MockCustomExporter force flushing metrics."); + Ok(()) + } + + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + println!( + "MockCustomExporter shutting down with timeout: {:?}", + timeout + ); + Ok(()) + } + + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } + + fn temporality(&self) -> opentelemetry_sdk::metrics::Temporality { + opentelemetry_sdk::metrics::Temporality::Cumulative + } +} diff --git a/opentelemetry-config/examples/metrics_custom.yaml b/opentelemetry-config/examples/metrics_custom.yaml new file mode 100644 index 00000000..9011726c --- /dev/null +++ b/opentelemetry-config/examples/metrics_custom.yaml @@ -0,0 +1,10 @@ +metrics: + readers: + - periodic: + exporter: + 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 new file mode 100644 index 00000000..8d08f345 --- /dev/null +++ b/opentelemetry-config/src/lib.rs @@ -0,0 +1,325 @@ +//! # 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::{ + collections::HashMap, + error::{self, Error}, + fmt::{self, Display}, +}; + +use opentelemetry_sdk::{ + logs::SdkLoggerProvider, + metrics::{MeterProviderBuilder, SdkMeterProvider}, + trace::SdkTracerProvider, +}; +use serde_yaml::Value; + +pub mod model; +pub mod providers; + +/// Registry for different configuration providers. +pub struct ConfigurationProvidersRegistry { + metrics: MetricsProvidersRegistry, +} + +impl ConfigurationProvidersRegistry { + pub fn new() -> Self { + Self { + metrics: MetricsProvidersRegistry::new(), + } + } + + pub fn metrics_mut(&mut self) -> &mut MetricsProvidersRegistry { + &mut self.metrics + } + + pub fn metrics(&self) -> &MetricsProvidersRegistry { + &self.metrics + } +} + +impl Default for ConfigurationProvidersRegistry { + fn default() -> Self { + Self::new() + } +} + +/// Registry for metrics configuration providers. +pub struct MetricsProvidersRegistry { + periodic_exporter_factories: HashMap>, + // TODO: Add other types of providers registries. +} + +impl MetricsProvidersRegistry { + pub fn new() -> Self { + Self { + periodic_exporter_factories: HashMap::new(), + } + } + + pub fn register_periodic_exporter_factory( + &mut self, + name: String, + factory: impl Fn(MeterProviderBuilder, &Value) -> Result + + Send + + Sync + + 'static, + ) { + self.periodic_exporter_factories + .insert(name, Box::new(factory)); + } + + pub fn periodic_exporter_factory(&self, name: &str) -> Option<&Box> { + self.periodic_exporter_factories.get(name) + } +} + +impl Default for MetricsProvidersRegistry { + fn default() -> Self { + Self::new() + } +} + +/// 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 Error for ConfigurationError {} + +impl fmt::Display for ConfigurationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConfigurationError::InvalidConfiguration(details) => { + write!(f, "Invalid configuration: {}", details) + } + ConfigurationError::RegistrationError(details) => { + write!(f, "Registration error: {}", details) + } + } + } +} + +/// 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 { + 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() + } +} + +/// Default implementation for TelemetryProviders +impl Default for TelemetryProviders { + fn default() -> Self { + Self::new() + } +} + +/// Errors related to providers and configuration management. +#[derive(Debug)] +pub enum ProviderError { + InvalidConfiguration(String), + UnsupportedExporter(String), + NotRegisteredProvider(String), + RegistrationError(String), +} + +impl error::Error for ProviderError {} + +impl Display for ProviderError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProviderError::InvalidConfiguration(details) => { + write!(f, "Invalid configuration: {}", details) + } + ProviderError::UnsupportedExporter(details) => { + write!(f, "Unsupported exporter: {}", details) + } + ProviderError::NotRegisteredProvider(details) => { + write!(f, "Not registered provider: {}", details) + } + ProviderError::RegistrationError(details) => { + write!(f, "Registration error: {}", details) + } + } + } +} + +#[cfg(test)] +mod tests { + 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 MockPeriodicExporter {} + + impl PushMetricExporter for MockPeriodicExporter { + async fn export(&self, _metrics: &ResourceMetrics) -> OTelSdkResult { + Ok(()) + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } + + 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 mut registry = ConfigurationProvidersRegistry::new(); + + // Act + let name = "console"; + registry + .metrics_mut() + .register_periodic_exporter_factory(name.to_string(), register_mock_exporter_clousure); + + // Assert + assert!(registry + .metrics() + .periodic_exporter_factories + .contains_key(name)); + + let console_config = serde_yaml::to_value( + r#" + console: + temporality: cumulative + "#, + ) + .unwrap(); + + 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"); + } + } + + #[test] + fn test_provider_manager_default() { + let provider_manager = ConfigurationProvidersRegistry::default(); + assert!(provider_manager + .metrics() + .periodic_exporter_factories + .is_empty()); + } + + #[test] + fn test_metrics_provider_registry_default() { + let metrics_provider_registry = MetricsProvidersRegistry::default(); + assert!(metrics_provider_registry + .periodic_exporter_factories + .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::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()); + assert!(telemetry_providers.logs_provider().is_some()); + } +} diff --git a/opentelemetry-config/src/model.rs b/opentelemetry-config/src/model.rs new file mode 100644 index 00000000..f3430bc2 --- /dev/null +++ b/opentelemetry-config/src/model.rs @@ -0,0 +1,73 @@ +//! # 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_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 { + 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.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..cfdaa04f --- /dev/null +++ b/opentelemetry-config/src/model/metrics/reader.rs @@ -0,0 +1,150 @@ +//! 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: serde_yaml::Value, +} + +#[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, +} + +#[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) => { + 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"), + } + } + + #[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()); + } +} diff --git a/opentelemetry-config/src/providers.rs b/opentelemetry-config/src/providers.rs new file mode 100644 index 00000000..475575a5 --- /dev/null +++ b/opentelemetry-config/src/providers.rs @@ -0,0 +1,230 @@ +//! # Provider objects for OpenTelemetry SDKs +//! +//! This module provides the different element providers to configure +//! OpenTelemetry SDKs using declarative YAML configurations. + +pub mod metrics_provider; + +use std::collections::HashMap; + +use opentelemetry::KeyValue; +use opentelemetry_sdk::{metrics::SdkMeterProvider, Resource}; + +use crate::{ + model::Telemetry, providers::metrics_provider::MetricsProvider, ConfigurationProvidersRegistry, + ProviderError, TelemetryProviders, +}; + +/// Provider for Telemetry object +pub struct TelemetryProvider { + metrics_provider: MetricsProvider, +} + +impl TelemetryProvider { + /// Creates a new TelemetryProvider + pub fn new() -> Self { + Self { + metrics_provider: MetricsProvider::new(), + } + } + + /// Configures the Telemetry providers based on the provided configuration + pub fn configure( + &self, + configuration_registry: &ConfigurationProvidersRegistry, + 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_provider.configure( + configuration_registry.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) + } + + /// Configures the Telemetry providers from a YAML string + pub fn configure_from_yaml( + &self, + configuration_registry: &ConfigurationProvidersRegistry, + yaml_str: &str, + ) -> Result { + let config: crate::model::Telemetry = serde_yaml::from_str(yaml_str).map_err(|e| { + ProviderError::InvalidConfiguration(format!( + "Failed to parse YAML configuration: {}", + e + )) + })?; + self.configure(configuration_registry, &config) + } + + /// Configures the Telemetry providers from a YAML file + pub fn configure_from_yaml_file( + &self, + configuration_registry: &ConfigurationProvidersRegistry, + file_path: &str, + ) -> Result { + let yaml_str = std::fs::read_to_string(file_path).map_err(|e| { + ProviderError::InvalidConfiguration(format!( + "Failed to read YAML configuration file: {}", + e + )) + })?; + self.configure_from_yaml(configuration_registry, &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 TelemetryProvider { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use crate::ConfigurationError; + use opentelemetry_sdk::{ + error::OTelSdkResult, + metrics::{ + data::ResourceMetrics, exporter::PushMetricExporter, MeterProviderBuilder, Temporality, + }, + }; + + use super::*; + + struct MockExporter {} + impl MockExporter { + fn new() -> Self { + Self {} + } + } + + impl PushMetricExporter for MockExporter { + fn export( + &self, + _metrics: &ResourceMetrics, + ) -> impl std::future::Future + Send { + async move { Ok(()) } + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } + + fn temporality(&self) -> Temporality { + Temporality::Delta + } + + 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#" + metrics: + readers: + - periodic: + exporter: + console: + temporality: delta + resource: + service.name: "test-service" + service.version: "1.0.0" + replica.count: 3 + cores: 4.5 + development: true + "#; + + let mut configuration_registry = ConfigurationProvidersRegistry::new(); + let metrics_provider_manager = configuration_registry.metrics_mut(); + 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 + .configure_from_yaml(&configuration_registry, yaml_str) + .unwrap(); + assert!(providers.meter_provider.is_some()); + } + + #[test] + 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_provider + .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 new file mode 100644 index 00000000..d11e1952 --- /dev/null +++ b/opentelemetry-config/src/providers/metrics_provider.rs @@ -0,0 +1,102 @@ +//! 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(), + } + } + + /// Configures the Metrics provider based on the provided configuration + pub fn configure( + &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 + .configure(metrics_registry, meter_provider_builder, reader)?; + } + + Ok(meter_provider_builder) + } +} + +impl Default for MetricsProvider { + fn default() -> Self { + 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 new file mode 100644 index 00000000..4f5d9f20 --- /dev/null +++ b/opentelemetry-config/src/providers/metrics_provider/reader_provider.rs @@ -0,0 +1,373 @@ +//! # 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 serde_yaml::Value; + +use crate::{model::metrics::reader::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::default(), + } + } + /// Configures a metrics reader based on the provided configuration + pub fn configure( + &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.configure( + 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::default(), + } + } + + /// 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.configure( + metrics_registry, + meter_provider_builder, + &config.exporter, + )?; + 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 configure( + &self, + metrics_registry: &MetricsProvidersRegistry, + mut meter_provider_builder: opentelemetry_sdk::metrics::MeterProviderBuilder, + config: &Value, + ) -> Result { + match config.as_mapping() { + Some(exporter_map) => { + for key in exporter_map.keys() { + match key { + Value::String(exporter_name) => { + 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())]; + 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 with its factory.", + exporter_name + ))); + } + } + } + _ => { + return Err(ProviderError::InvalidConfiguration( + "Exporter name must be a string.".to_string(), + )); + } + } + } + } + None => { + return Err(ProviderError::InvalidConfiguration( + "Expecting a configuration object for periodic exporter.".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::default(), + } + } + + /// 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 super::*; + use crate::{model::metrics::reader::PullExporter, ConfigurationError}; + use opentelemetry_sdk::metrics::SdkMeterProvider; + + 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(); + 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( + r#" + console: + temporality: cumulative + "#, + ) + .unwrap(); + let config = crate::model::metrics::reader::Reader::Periodic( + crate::model::metrics::reader::Periodic { + exporter: console_object, + }, + ); + + let metrics_registry = configuration_registry.metrics(); + + _ = provider + .configure(metrics_registry, meter_provider_builder, &config) + .unwrap(); + } + + #[test] + fn test_reader_provider_configure_console_factory_not_registered() { + 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: console_config, + }, + ); + + let result = provider.configure(&metrics_registry, meter_provider_builder, &config); + if let Err(e) = result { + 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_factory_not_registered() { + let provider = ReaderProvider::new(); + 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: console_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 'otlp'")); + } 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"); + } + } + + #[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(); + } +}