diff --git a/Cargo.toml b/Cargo.toml index bae3bd5566..930af2f175 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "opentelemetry-*", "opentelemetry-*/examples/*", "opentelemetry-otlp/tests/*", + "opentelemetry-declarative-config", "examples/*", "stress", ] diff --git a/opentelemetry-declarative-config/CHANGELOG.md b/opentelemetry-declarative-config/CHANGELOG.md new file mode 100644 index 0000000000..0abd81ea27 --- /dev/null +++ b/opentelemetry-declarative-config/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## vNext + +## v0.1.0 + +### Added + +- Initial declarative configuration diff --git a/opentelemetry-declarative-config/Cargo.toml b/opentelemetry-declarative-config/Cargo.toml new file mode 100644 index 0000000000..7ff39b1fe5 --- /dev/null +++ b/opentelemetry-declarative-config/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "opentelemetry-declarative-config" +version = "0.29.1" +description = "Declarative configuration for OpenTelemetry SDK" +homepage = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-declarative-config" +repository = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-declarative-config" +readme = "README.md" +categories = [ + "development-tools::debugging", + "development-tools::profiling", +] +keywords = ["opentelemetry", "declarative", "metrics", "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"] } +opentelemetry-stdout = { version = "0.31.0" } +opentelemetry-otlp = { version = "0.31.0" } +opentelemetry-http = { workspace = true, optional = true, default-features = false } +serde = { workspace = true, features = ["derive"] } +reqwest = { workspace = true, optional = true } +serde_yaml = "0.9.34" + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } + +[features] +tonic-client = ["opentelemetry-otlp/grpc-tonic", "opentelemetry-otlp/trace", "opentelemetry-otlp/logs", "opentelemetry-otlp/metrics"] +hyper-client = ["opentelemetry-http/hyper"] +reqwest-client = ["reqwest", "opentelemetry-http/reqwest"] +reqwest-blocking-client = ["reqwest/blocking", "opentelemetry-http/reqwest-blocking"] + +# Keep tonic as the default client +default = ["tonic-client"] + +[package.metadata.cargo-machete] +ignored = [ + "reqwest", # needed for otlp features + "opentelemetry-http" # needed for otlp features +] diff --git a/opentelemetry-declarative-config/README.md b/opentelemetry-declarative-config/README.md new file mode 100644 index 0000000000..217771696e --- /dev/null +++ b/opentelemetry-declarative-config/README.md @@ -0,0 +1,30 @@ +# 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 Overview + +OpenTelemetry is an Observability framework and toolkit designed to create and +manage telemetry data such as traces, metrics, and logs. OpenTelemetry is +vendor- and tool-agnostic, meaning that it can be used with a broad variety of +Observability backends, including open source tools like [Jaeger] and +[Prometheus], as well as commercial offerings. + +OpenTelemetry is *not* an observability backend like Jaeger, Prometheus, or other +commercial vendors. OpenTelemetry is focused on the generation, collection, +management, and export of telemetry. A major goal of OpenTelemetry is that you +can easily instrument your applications or systems, no matter their language, +infrastructure, or runtime environment. Crucially, the storage and visualization +of telemetry is intentionally left to other tools. + +[`Prometheus`]: https://prometheus.io +[`OpenTelemetry`]: https://crates.io/crates/opentelemetry +[`Jaeger`]: https://www.jaegertracing.io + +## Release Notes + +You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-declarative-config/CHANGELOG.md). diff --git a/opentelemetry-declarative-config/examples/declarative_basic.rs b/opentelemetry-declarative-config/examples/declarative_basic.rs new file mode 100644 index 0000000000..db65a1eda3 --- /dev/null +++ b/opentelemetry-declarative-config/examples/declarative_basic.rs @@ -0,0 +1,44 @@ +use opentelemetry_declarative_config::Configurator; + +/// Example of configuring OpenTelemetry telemetry using declarative YAML configuration. + +#[tokio::main] +pub async fn main() -> Result<(), Box> { + let configurator = Configurator::new(); + let config_yaml = r#" + metrics: + readers: + - periodic: + exporter: + otlp: + protocol: http/protobuf + endpoint: https://backend:4318 + stdout: + temporality: cumulative + logs: + processors: + - batch: + exporter: + stdout: + otlp: + protocol: http/protobuf + endpoint: https://backend:4318 + resource: + service.name: sample-service + service.version: "1.0.0" + "#; + let result = configurator.configure_telemetry_from_yaml(config_yaml.into()); + if let Err(ref e) = result { + panic!("Failed to configure telemetry from YAML string: {}", e); + } + assert!(result.is_ok()); + let telemetry_providers = result.unwrap(); + assert!(telemetry_providers.meter_provider().is_some()); + assert!(telemetry_providers.logs_provider().is_some()); + assert!(telemetry_providers.traces_provider().is_none()); + + println!("All the expected telemetry providers were configured successfully. Shutting down..."); + + telemetry_providers.shutdown()?; + Ok(()) +} diff --git a/opentelemetry-declarative-config/src/lib.rs b/opentelemetry-declarative-config/src/lib.rs new file mode 100644 index 0000000000..b3162157b7 --- /dev/null +++ b/opentelemetry-declarative-config/src/lib.rs @@ -0,0 +1,353 @@ +//! # OpenTelemetry declarative configuration +//! +//! This crate provides a declarative way to configure OpenTelemetry SDKs using YAML files. + +pub mod logs; +pub mod metrics; +pub mod telemetry_config; + +use opentelemetry::global; +use opentelemetry_sdk::{ + error::OTelSdkResult, logs::SdkLoggerProvider, metrics::SdkMeterProvider, + trace::SdkTracerProvider, +}; + +use crate::{ + logs::{BatchExporterFactory, LogsBatchExporterFactory, LogsConfig}, + metrics::{ + MetricsConfig, MetricsPeriodicExporterFactory, MetricsPullExporterFactory, + PeriodicExporterFactory, PullExporterFactory, + }, + telemetry_config::TelemetryConfig, +}; + +pub struct Configurator {} + +impl Configurator { + pub fn new() -> Self { + Self {} + } + + pub fn configure_telemetry_from_yaml( + &self, + telemetry_config_str: String, + ) -> Result> { + let config = TelemetryConfig::from_yaml(&telemetry_config_str)?; + self.configure_telemetry(config) + } + + pub fn configure_telemetry_from_yaml_file( + &self, + file_path: &str, + ) -> Result> { + let config = TelemetryConfig::from_yaml_file(file_path)?; + self.configure_telemetry(config) + } + + pub fn configure_telemetry( + &self, + telemetry_config: TelemetryConfig, + ) -> Result> { + let mut configured_telemetry_providers = TelemetryProviders::new(); + + let resource = Self::as_resource(telemetry_config.resource); + if let Some(metrics_config) = telemetry_config.metrics { + let sdk_meter_provider_option = + self.build_metrics_sdk_provider(metrics_config, resource.clone())?; + configured_telemetry_providers = + configured_telemetry_providers.with_meter_provider(sdk_meter_provider_option); + } + + if let Some(logs_config) = telemetry_config.logs { + let sdk_logger_provider_option = + self.build_logs_sdk_provider(logs_config, resource.clone())?; + configured_telemetry_providers = + configured_telemetry_providers.with_logs_provider(sdk_logger_provider_option); + } + + //TODO: Add similar configuration for traces when implemented + Ok(configured_telemetry_providers) + } + + fn build_metrics_sdk_provider( + &self, + metrics_config: MetricsConfig, + resource: opentelemetry_sdk::Resource, + ) -> Result> { + let mut provider_builder = SdkMeterProvider::builder().with_resource(resource); + + for reader_config in metrics_config.readers { + if let Some(periodic_reader) = reader_config.periodic { + let periodic_exporter_config = periodic_reader.exporter; + if let Some(periodic_exporter_mapping) = periodic_exporter_config.as_mapping() { + for (key, value) in periodic_exporter_mapping { + if let Some(periodic_exporter_factory_name) = key.as_str() { + let exporter_factory = + PeriodicExporterFactory::from_name(periodic_exporter_factory_name)?; + let config = value; + match exporter_factory { + PeriodicExporterFactory::Stdout(factory) => { + let periodic_exporter = + factory.create_metrics_periodic_exporter(config)?; + provider_builder = + provider_builder.with_periodic_exporter(periodic_exporter); + } + PeriodicExporterFactory::Otlp(factory) => { + let periodic_exporter = + factory.create_metrics_periodic_exporter(config)?; + provider_builder = + provider_builder.with_periodic_exporter(periodic_exporter); + } + } + } else { + return Err("Invalid periodic exporter factory configuration".into()); + } + } + } else { + return Err("Periodic exporter configuration must be defined".into()); + } + } + if let Some(pull_reader) = reader_config.pull { + let pull_exporter_config = pull_reader.exporter; + if let Some(pull_exporter_mapping) = pull_exporter_config.as_mapping() { + for (key, value) in pull_exporter_mapping { + if let Some(pull_exporter_factory_name) = key.as_str() { + let exporter_factory = + PullExporterFactory::from_name(pull_exporter_factory_name)?; + let config = value; + match exporter_factory { + PullExporterFactory::Prometheus(factory) => { + let pull_exporter = + factory.create_metrics_pull_exporter(config)?; + provider_builder = provider_builder.with_reader(pull_exporter); + } + } + } else { + return Err("Invalid pull exporter factory configuration".into()); + } + } + } else { + return Err("Pull exporter configuration must be defined".into()); + } + } + } + + let provider = provider_builder.build(); + global::set_meter_provider(provider.clone()); + Ok(provider) + } + + fn build_logs_sdk_provider( + &self, + logs_config: LogsConfig, + resource: opentelemetry_sdk::Resource, + ) -> Result> { + let mut provider_builder = SdkLoggerProvider::builder().with_resource(resource); + + for processor_config in logs_config.processors { + if let Some(batch_processor) = processor_config.batch { + let batch_exporter_config = batch_processor.exporter; + if let Some(batch_exporter_mapping) = batch_exporter_config.as_mapping() { + for (key, value) in batch_exporter_mapping { + if let Some(batch_exporter_factory_name) = key.as_str() { + let exporter_factory = + BatchExporterFactory::from_name(batch_exporter_factory_name)?; + let config = value; + match exporter_factory { + BatchExporterFactory::Stdout(factory) => { + let batch_exporter = + factory.create_logs_batch_exporter(config)?; + provider_builder = + provider_builder.with_batch_exporter(batch_exporter); + } + BatchExporterFactory::Otlp(factory) => { + let batch_exporter = + factory.create_logs_batch_exporter(config)?; + provider_builder = + provider_builder.with_batch_exporter(batch_exporter); + } + } + } else { + return Err("Invalid batch exporter factory configuration".into()); + } + } + } else { + return Err("Batch exporter configuration must be defined".into()); + } + } + } + + let provider = provider_builder.build(); + Ok(provider) + } + + fn as_resource( + resource_attributes: std::collections::HashMap, + ) -> opentelemetry_sdk::Resource { + let mut resource_builder = opentelemetry_sdk::Resource::builder(); + for (key, value) in resource_attributes { + resource_builder = + resource_builder.with_attribute(opentelemetry::KeyValue::new(key, value)); + } + resource_builder.build() + } +} + +impl Default for Configurator { + fn default() -> Self { + Self::new() + } +} + +/// 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() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_config_empty() -> Result<(), Box> { + let config = Configurator::new(); + let telemetry_config = TelemetryConfig::default(); + let telemetry_providers = config.configure_telemetry(telemetry_config)?; + + assert!(telemetry_providers.meter_provider().is_none()); + assert!(telemetry_providers.traces_provider().is_none()); + assert!(telemetry_providers.logs_provider().is_none()); + + assert!(telemetry_providers.shutdown().is_ok()); + Ok(()) + } + + #[test] + fn test_shutdown_empty_providers() { + let providers = TelemetryProviders::new(); + assert!(providers.shutdown().is_ok()); + } + + #[test] + fn test_configurator_default() -> Result<(), Box> { + let config = Configurator::default(); + let telemetry_config = TelemetryConfig::default(); + let telemetry_providers = config.configure_telemetry(telemetry_config)?; + + assert!(telemetry_providers.meter_provider().is_none()); + assert!(telemetry_providers.traces_provider().is_none()); + assert!(telemetry_providers.logs_provider().is_none()); + + assert!(telemetry_providers.shutdown().is_ok()); + Ok(()) + } + + #[test] + fn test_configurator_from_yaml() -> Result<(), Box> { + let config = Configurator::new(); + let yaml_str = r#" + resource: + service.name: test-service + metrics: + readers: + - periodic: + exporter: + name: stdout + logs: + processors: + - batch: + exporter: + name: otlp + "#; + let telemetry_providers = config.configure_telemetry_from_yaml(yaml_str.into())?; + + assert!(telemetry_providers.meter_provider().is_some()); + assert!(telemetry_providers.logs_provider().is_some()); + assert!(telemetry_providers.traces_provider().is_none()); + + assert!(telemetry_providers.shutdown().is_ok()); + Ok(()) + } + + #[test] + fn test_telemetry_providers_default() { + let providers = TelemetryProviders::default(); + assert!(providers.meter_provider().is_none()); + assert!(providers.traces_provider().is_none()); + assert!(providers.logs_provider().is_none()); + } + + #[test] + fn test_telemetry_providers_getters() { + let meter_provider = SdkMeterProvider::builder().build(); + let logs_provider = SdkLoggerProvider::builder().build(); + let traces_provider = SdkTracerProvider::builder().build(); + + let providers = TelemetryProviders::new() + .with_logs_provider(logs_provider) + .with_meter_provider(meter_provider) + .with_traces_provider(traces_provider); + + assert!(providers.meter_provider().is_some()); + assert!(providers.traces_provider().is_some()); + assert!(providers.logs_provider().is_some()); + providers.shutdown().unwrap(); + } +} diff --git a/opentelemetry-declarative-config/src/logs.rs b/opentelemetry-declarative-config/src/logs.rs new file mode 100644 index 0000000000..906de47b69 --- /dev/null +++ b/opentelemetry-declarative-config/src/logs.rs @@ -0,0 +1,76 @@ +//! # Logs Configuration module +//! +//! This module defines the configuration structures for Logs telemetry +//! used in OpenTelemetry SDKs. + +pub mod exporters; +pub mod processor_config; + +use opentelemetry_sdk::logs::LogExporter; +use serde::Deserialize; +use serde_yaml::Value; + +use crate::logs::{ + exporters::{ + otlp_batch_exporter::OtlpLogsBatchExporterFactory, + stdout_batch_exporter::StdoutLogsBatchExporterFactory, + }, + processor_config::ProcessorConfig, +}; + +/// Configuration for Logs telemetry +#[derive(Deserialize)] +pub struct LogsConfig { + pub processors: Vec, +} + +/// Factory trait implemented by the different logs exporters +pub trait LogsBatchExporterFactory { + /// Creates a LogsExporter based on the provided configuration + fn create_logs_batch_exporter( + &self, + config: &Value, + ) -> Result>; +} + +/// Enum representing different Batch Exporter Factories +pub enum BatchExporterFactory { + Stdout(StdoutLogsBatchExporterFactory), + Otlp(OtlpLogsBatchExporterFactory), +} + +impl BatchExporterFactory { + /// Creates a Stdout Logs Batch Exporter Factory + pub fn stdout() -> Self { + BatchExporterFactory::Stdout(StdoutLogsBatchExporterFactory::new()) + } + /// Creates an OTLP Logs Batch Exporter Factory + pub fn otlp() -> Self { + BatchExporterFactory::Otlp(OtlpLogsBatchExporterFactory::new()) + } + + // Get factory by name (useful for configuration-driven creation) + pub fn from_name(name: &str) -> Result> { + match name { + "stdout" => Ok(Self::stdout()), + "otlp" => Ok(Self::otlp()), + _ => Err(format!("Unknown logs exporter factory: {}", name).into()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_logs_batch_exporter_factory_from_name() { + let stdout_factory = BatchExporterFactory::from_name("stdout").unwrap(); + let otlp_factory = BatchExporterFactory::from_name("otlp").unwrap(); + let unknown_factory = BatchExporterFactory::from_name("unknown"); + + assert!(matches!(stdout_factory, BatchExporterFactory::Stdout(_))); + assert!(matches!(otlp_factory, BatchExporterFactory::Otlp(_))); + assert!(unknown_factory.is_err()); + } +} diff --git a/opentelemetry-declarative-config/src/logs/exporters.rs b/opentelemetry-declarative-config/src/logs/exporters.rs new file mode 100644 index 0000000000..052e96257e --- /dev/null +++ b/opentelemetry-declarative-config/src/logs/exporters.rs @@ -0,0 +1,8 @@ +//! # Logs Exporters module. +//! +//! This module contains the definitions and common implementations for various logs exporters +//! that can be used with OpenTelemetry SDKs. Exporters are responsible for sending +//! collected logs data to different backends or systems. + +pub mod otlp_batch_exporter; +pub mod stdout_batch_exporter; diff --git a/opentelemetry-declarative-config/src/logs/exporters/otlp_batch_exporter.rs b/opentelemetry-declarative-config/src/logs/exporters/otlp_batch_exporter.rs new file mode 100644 index 0000000000..1dee671bd8 --- /dev/null +++ b/opentelemetry-declarative-config/src/logs/exporters/otlp_batch_exporter.rs @@ -0,0 +1,139 @@ +//! # OTLP log exporter module. +//! +//! This module contains the definitions and common implementations for the OTLP logs exporter +//! that can be used with OpenTelemetry SDKs. Exporters are responsible for sending +//! collected logs data to OTLP-compatible backends or systems. + +use opentelemetry_sdk::logs::LogExporter; +use serde::{Deserialize, Deserializer}; +use serde_yaml::Value; + +use crate::logs::LogsBatchExporterFactory; + +/// Factory for creating OTLP Logs Batch Exporters +pub struct OtlpLogsBatchExporterFactory {} + +impl OtlpLogsBatchExporterFactory { + /// Creates a new OtlpLogsBatchExporterFactory + pub fn new() -> Self { + OtlpLogsBatchExporterFactory {} + } +} + +impl Default for OtlpLogsBatchExporterFactory { + fn default() -> Self { + Self::new() + } +} + +impl LogsBatchExporterFactory for OtlpLogsBatchExporterFactory { + /// Creates a LogExporter based on the provided configuration + fn create_logs_batch_exporter( + &self, + _config: &Value, + ) -> Result> { + let exporter_builder = opentelemetry_otlp::LogExporter::builder(); + + #[cfg(feature = "tonic-client")] + let exporter_builder = exporter_builder.with_tonic(); + #[cfg(not(feature = "tonic-client"))] + #[cfg(any( + feature = "hyper-client", + feature = "reqwest-client", + feature = "reqwest-blocking-client" + ))] + let exporter_builder = exporter_builder.with_http(); + + /* + let config_parsed = serde_yaml::from_value::(config.clone())?; + */ + + // TODO: Configure the exporter based on config_parsed fields. There are no methods in the builder to pass its parameters yet.. + + let exporter = exporter_builder.build()?; + + Ok(exporter) + } +} + +/// Configuration for OTLP Logs Exporter +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OtlpConfig { + #[serde(default, deserialize_with = "deserialize_protocol")] + pub protocol: Option, + + #[serde(default)] + pub endpoint: Option, +} + +fn deserialize_protocol<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + match s.trim().to_lowercase().as_str() { + "grpc" => Ok(Some(opentelemetry_otlp::Protocol::Grpc)), + "http/protobuf" => Ok(Some(opentelemetry_otlp::Protocol::HttpBinary)), + "http/json" => Ok(Some(opentelemetry_otlp::Protocol::HttpJson)), + _ => Err(serde::de::Error::custom(format!("Invalid protocol: {}", s))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_yaml::from_str; + + #[tokio::test] + async fn test_default_otlp_logs_batch_exporter_factory() { + let factory = OtlpLogsBatchExporterFactory::default(); + assert!(factory.create_logs_batch_exporter(&Value::Null).is_ok()); + } + + #[test] + fn test_deserialize_otlp_config() { + let yaml_data = r#" + protocol: "grpc" + endpoint: "http://localhost:4317" + "#; + let config: OtlpConfig = from_str(yaml_data).unwrap(); + assert_eq!(config.protocol, Some(opentelemetry_otlp::Protocol::Grpc)); + assert_eq!(config.endpoint, Some("http://localhost:4317".into())); + } + + #[test] + fn test_deserialize_protocol() { + let yaml_grpc = r#" + protocol: "grpc" + "#; + let config: OtlpConfig = from_str(yaml_grpc).unwrap(); + assert_eq!(config.protocol, Some(opentelemetry_otlp::Protocol::Grpc)); + + let yaml_http_binary = r#" + protocol: "http/protobuf" + "#; + let config: OtlpConfig = from_str(yaml_http_binary).unwrap(); + assert_eq!( + config.protocol, + Some(opentelemetry_otlp::Protocol::HttpBinary) + ); + + let yaml_http_json = r#" + protocol: "http/json" + "#; + let config: OtlpConfig = from_str(yaml_http_json).unwrap(); + assert_eq!( + config.protocol, + Some(opentelemetry_otlp::Protocol::HttpJson) + ); + + let yaml_unknown = r#" + protocol: "http/unknown" + "#; + let config: Result = from_str(yaml_unknown); + assert!(config.is_err()); + } +} diff --git a/opentelemetry-declarative-config/src/logs/exporters/stdout_batch_exporter.rs b/opentelemetry-declarative-config/src/logs/exporters/stdout_batch_exporter.rs new file mode 100644 index 0000000000..ba1c9964bc --- /dev/null +++ b/opentelemetry-declarative-config/src/logs/exporters/stdout_batch_exporter.rs @@ -0,0 +1,48 @@ +//! # Stdout logs exporter module. +//! +//! This module contains the definitions and common implementations for the stdout logs exporter +//! that can be used with OpenTelemetry SDKs. Exporters are responsible for sending +//! collected logs data to different backends or systems. + +use opentelemetry_sdk::logs::LogExporter; +use serde_yaml::Value; + +use crate::logs::LogsBatchExporterFactory; + +/// Factory for creating Stdout Logs Batch Exporters +pub struct StdoutLogsBatchExporterFactory {} + +impl StdoutLogsBatchExporterFactory { + /// Creates a new StdoutLogsBatchExporterFactory + pub fn new() -> Self { + StdoutLogsBatchExporterFactory {} + } +} + +impl Default for StdoutLogsBatchExporterFactory { + fn default() -> Self { + Self::new() + } +} + +impl LogsBatchExporterFactory for StdoutLogsBatchExporterFactory { + /// Creates a LogExporter based on the provided configuration + fn create_logs_batch_exporter( + &self, + _config: &Value, + ) -> Result> { + let exporter = opentelemetry_stdout::LogExporter::default(); + Ok(exporter) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_stdout_logs_batch_exporter_factory() { + let factory = StdoutLogsBatchExporterFactory::default(); + assert!(factory.create_logs_batch_exporter(&Value::Null).is_ok()); + } +} diff --git a/opentelemetry-declarative-config/src/logs/processor_config.rs b/opentelemetry-declarative-config/src/logs/processor_config.rs new file mode 100644 index 0000000000..42cc61abde --- /dev/null +++ b/opentelemetry-declarative-config/src/logs/processor_config.rs @@ -0,0 +1,18 @@ +//! # Log Processor Configuration module +//! +//! This module defines the configuration structures for log processors + +use serde::Deserialize; +use serde_yaml::Value; + +/// Configuration for Log Processors +#[derive(Deserialize)] +pub struct ProcessorConfig { + pub batch: Option, +} + +/// Configuration for Batch Log Processor +#[derive(Deserialize)] +pub struct ProcessorBatchConfig { + pub exporter: Value, +} diff --git a/opentelemetry-declarative-config/src/metrics.rs b/opentelemetry-declarative-config/src/metrics.rs new file mode 100644 index 0000000000..87e0ac9776 --- /dev/null +++ b/opentelemetry-declarative-config/src/metrics.rs @@ -0,0 +1,118 @@ +//! # Metrics Configuration module +//! +//! This module defines the configuration structures for Metrics telemetry +//! used in OpenTelemetry SDKs. + +pub mod exporters; +pub mod reader_config; + +use opentelemetry_sdk::metrics::{exporter::PushMetricExporter, reader::MetricReader}; +use serde::Deserialize; +use serde_yaml::Value; + +use crate::metrics::{ + exporters::{ + otlp_periodic_exporter::OtlpMetricsPeriodicExporterFactory, + prometheus_pull_exporter::PrometheusMetricsPullExporterFactory, + stdout_periodic_exporter::StdoutMetricsPeriodicExporterFactory, + }, + reader_config::ReaderConfig, +}; + +/// Configuration for Metrics telemetry +#[derive(Deserialize)] +pub struct MetricsConfig { + pub readers: Vec, +} + +/// Factory trait implemented by the different periodic metric exporters +pub trait MetricsPeriodicExporterFactory { + /// Creates a PushMetricExporter based on the provided configuration + fn create_metrics_periodic_exporter( + &self, + config: &Value, + ) -> Result>; +} + +/// Factory trait implemented by the different pull metric exporters +pub trait MetricsPullExporterFactory { + type Exporter: MetricReader; + /// Creates a MetricReader based on the provided configuration + fn create_metrics_pull_exporter( + &self, + config: &Value, + ) -> Result>; +} + +/// Factory enum to create different types of exporters +pub enum PeriodicExporterFactory { + Stdout(StdoutMetricsPeriodicExporterFactory), + Otlp(OtlpMetricsPeriodicExporterFactory), + //TODO: Add other variants as needed +} + +impl PeriodicExporterFactory { + /// Creates a Stdout Metrics Periodic Exporter Factory + pub fn stdout() -> Self { + PeriodicExporterFactory::Stdout(StdoutMetricsPeriodicExporterFactory::new()) + } + + /// Creates an OTLP Metrics Periodic Exporter Factory + pub fn otlp() -> Self { + PeriodicExporterFactory::Otlp(OtlpMetricsPeriodicExporterFactory::new()) + } + + // Get factory by name (useful for configuration-driven creation) + pub fn from_name(name: &str) -> Result> { + match name { + "stdout" => Ok(Self::stdout()), + "otlp" => Ok(Self::otlp()), + _ => Err(format!("Unknown exporter factory: {}", name).into()), + } + } +} + +/// Factory enum to create different types of pull exporters +pub enum PullExporterFactory { + Prometheus(PrometheusMetricsPullExporterFactory), +} + +impl PullExporterFactory { + /// Creates a Prometheus Metrics Pull Exporter Factory + pub fn prometheus() -> Self { + PullExporterFactory::Prometheus(PrometheusMetricsPullExporterFactory::new()) + } + + // Get factory by name (useful for configuration-driven creation) + pub fn from_name(name: &str) -> Result> { + match name { + "prometheus" => Ok(Self::prometheus()), + _ => Err(format!("Unknown pull exporter factory: {}", name).into()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_periodic_exporter_factory_from_name() { + let stdout_factory = PeriodicExporterFactory::from_name("stdout").unwrap(); + let otlp_factory = PeriodicExporterFactory::from_name("otlp").unwrap(); + let unknown_factory = PeriodicExporterFactory::from_name("unknown"); + assert!(matches!(stdout_factory, PeriodicExporterFactory::Stdout(_))); + assert!(matches!(otlp_factory, PeriodicExporterFactory::Otlp(_))); + assert!(unknown_factory.is_err()); + } + + #[test] + fn test_pull_exporter_factory_from_name() { + let prometheus_factory = PullExporterFactory::from_name("prometheus").unwrap(); + let unknown_factory = PullExporterFactory::from_name("unknown"); + assert!(matches!( + prometheus_factory, + PullExporterFactory::Prometheus(_) + )); + assert!(unknown_factory.is_err()); + } +} diff --git a/opentelemetry-declarative-config/src/metrics/exporters.rs b/opentelemetry-declarative-config/src/metrics/exporters.rs new file mode 100644 index 0000000000..826245e687 --- /dev/null +++ b/opentelemetry-declarative-config/src/metrics/exporters.rs @@ -0,0 +1,93 @@ +//! # Metrics Exporters module. +//! +//! This module contains the definitions and common implementations for various metrics exporters +//! that can be used with OpenTelemetry SDKs. Exporters are responsible for sending +//! collected metrics data to different backends or systems. + +use serde::{Deserialize, Deserializer}; + +pub mod otlp_periodic_exporter; +pub mod prometheus_pull_exporter; +pub mod stdout_periodic_exporter; + +/// Deserializes an optional Temporality from a string +pub fn deserialize_temporality<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt_string = Option::::deserialize(deserializer)?; + match opt_string { + Some(s) => match s.trim().to_lowercase().as_str() { + "cumulative" => Ok(Some(opentelemetry_sdk::metrics::Temporality::Cumulative)), + "delta" => Ok(Some(opentelemetry_sdk::metrics::Temporality::Delta)), + _ => Err(serde::de::Error::custom(format!( + "Invalid temporality: {}", + s + ))), + }, + None => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, Deserialize)] + struct MockTemporalityHolder { + #[serde(default, deserialize_with = "deserialize_temporality")] + pub temporality: Option, + } + + #[test] + fn test_deserialize_cumulative_temporality() { + let yaml_cumulative = r#" + temporality: cumulative + "#; + let temporality_holder: MockTemporalityHolder = + serde_yaml::from_str(yaml_cumulative).unwrap(); + assert_eq!( + temporality_holder.temporality, + Some(opentelemetry_sdk::metrics::Temporality::Cumulative) + ); + } + + #[test] + fn test_deserialize_delta_temporality() { + let yaml_cumulative = r#" + temporality: delta + "#; + let temporality_holder: MockTemporalityHolder = + serde_yaml::from_str(yaml_cumulative).unwrap(); + assert_eq!( + temporality_holder.temporality, + Some(opentelemetry_sdk::metrics::Temporality::Delta) + ); + } + + #[test] + fn test_deserialize_unknown_temporality() { + let yaml_cumulative = r#" + temporality: unknown + "#; + let temporality_holder_result = + serde_yaml::from_str::(yaml_cumulative); + if let Err(e) = temporality_holder_result { + let err_msg = e.to_string(); + assert!(err_msg.contains("Invalid temporality")); + } else { + panic!("Expected error for unknown temporality"); + } + } + + #[test] + fn test_deserialize_no_temporality() { + let yaml_cumulative = r#" + "#; + let temporality_holder: MockTemporalityHolder = + serde_yaml::from_str(yaml_cumulative).unwrap(); + assert_eq!(temporality_holder.temporality, None); + } +} diff --git a/opentelemetry-declarative-config/src/metrics/exporters/otlp_periodic_exporter.rs b/opentelemetry-declarative-config/src/metrics/exporters/otlp_periodic_exporter.rs new file mode 100644 index 0000000000..5feb705af2 --- /dev/null +++ b/opentelemetry-declarative-config/src/metrics/exporters/otlp_periodic_exporter.rs @@ -0,0 +1,152 @@ +//! # Otlp metrics periodic exporter module. +//! +//! This module contains the definitions and common implementations for the OTLP metrics periodic exporter +//! that can be used with OpenTelemetry SDKs. Exporters are responsible for sending +//! collected metrics data to different backends or systems. + +use opentelemetry_otlp::{MetricExporter, WithExportConfig}; +use opentelemetry_sdk::metrics::exporter::PushMetricExporter; + +use serde::{Deserialize, Deserializer}; +use serde_yaml::Value; + +use crate::metrics::exporters::deserialize_temporality; +use crate::metrics::MetricsPeriodicExporterFactory; + +/// Factory for creating OTLP Metrics Periodic Exporters +pub struct OtlpMetricsPeriodicExporterFactory {} + +impl OtlpMetricsPeriodicExporterFactory { + pub fn new() -> Self { + OtlpMetricsPeriodicExporterFactory {} + } +} + +impl Default for OtlpMetricsPeriodicExporterFactory { + fn default() -> Self { + Self::new() + } +} + +impl MetricsPeriodicExporterFactory for OtlpMetricsPeriodicExporterFactory { + fn create_metrics_periodic_exporter( + &self, + config_raw: &Value, + ) -> Result> { + let exporter_builder = MetricExporter::builder(); + + #[cfg(feature = "tonic-client")] + let mut exporter_builder = exporter_builder.with_tonic(); + #[cfg(not(feature = "tonic-client"))] + #[cfg(any( + feature = "hyper-client", + feature = "reqwest-client", + feature = "reqwest-blocking-client" + ))] + let mut exporter_builder = exporter_builder.with_http(); + + let config_parsed = serde_yaml::from_value::(config_raw.clone())?; + + if let Some(temporality) = config_parsed.temporality { + exporter_builder = exporter_builder.with_temporality(temporality); + } + + if let Some(protocol) = config_parsed.protocol { + exporter_builder = exporter_builder.with_protocol(protocol); + } + + if let Some(endpoint) = config_parsed.endpoint { + exporter_builder = exporter_builder.with_endpoint(&endpoint); + } + + let exporter = exporter_builder.build()?; + + Ok(exporter) + } +} + +/// Configuration for OTLP Metrics Exporter +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OtlpConfig { + #[serde(default, deserialize_with = "deserialize_temporality")] + pub temporality: Option, + + #[serde(default, deserialize_with = "deserialize_protocol")] + pub protocol: Option, + + #[serde(default)] + pub endpoint: Option, +} + +fn deserialize_protocol<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + match s.trim().to_lowercase().as_str() { + "grpc" => Ok(Some(opentelemetry_otlp::Protocol::Grpc)), + "http/protobuf" => Ok(Some(opentelemetry_otlp::Protocol::HttpBinary)), + "http/json" => Ok(Some(opentelemetry_otlp::Protocol::HttpJson)), + _ => Err(serde::de::Error::custom(format!("Invalid protocol: {}", s))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_default_otlp_metrics_periodic_exporter_factory() { + let factory = OtlpMetricsPeriodicExporterFactory::default(); + assert!(factory + .create_metrics_periodic_exporter(&Value::Null) + .is_ok()); + } + + #[test] + fn test_deserialize_config_all_fields_in() { + let yaml_grpc = r#" + protocol: grpc + endpoint: https://backend:4318 + temporality: cumulative + "#; + + let config: OtlpConfig = serde_yaml::from_str(yaml_grpc).unwrap(); + assert_eq!(config.protocol, Some(opentelemetry_otlp::Protocol::Grpc)); + assert_eq!(config.endpoint, Some("https://backend:4318".into())); + assert_eq!( + config.temporality, + Some(opentelemetry_sdk::metrics::Temporality::Cumulative) + ); + } + + #[test] + fn test_deserialize_config_no_fields_in() { + let yaml_grpc = r#" + "#; + + let config: OtlpConfig = serde_yaml::from_str(yaml_grpc).unwrap(); + assert_eq!(config.protocol, None); + assert_eq!(config.endpoint, None); + assert_eq!(config.temporality, None); + } + + #[test] + fn test_deserialize_config_extra_fields_in() { + let yaml_grpc = r#" + protocol: grpc + endpoint: https://backend:4318 + unknown: field + "#; + + let config_result = serde_yaml::from_str::(yaml_grpc); + assert!(config_result.is_err()); + if let Err(e) = config_result { + let err_msg = e.to_string(); + assert!(err_msg.contains("unknown field")); + } + } +} diff --git a/opentelemetry-declarative-config/src/metrics/exporters/prometheus_pull_exporter.rs b/opentelemetry-declarative-config/src/metrics/exporters/prometheus_pull_exporter.rs new file mode 100644 index 0000000000..d2c46220b2 --- /dev/null +++ b/opentelemetry-declarative-config/src/metrics/exporters/prometheus_pull_exporter.rs @@ -0,0 +1,131 @@ +//! # Prometheus metrics pull exporter module. +//! +//! Prometheus is not currently maintained in the OpenTelemetry Rust SDK. +//! This module provides a factory for creating a mock Prometheus metrics pull exporter +//! that can be used for configuration parsing purposes. + +use std::{sync::Weak, time::Duration}; + +use opentelemetry_sdk::{ + error::OTelSdkResult, + metrics::{data::ResourceMetrics, reader::MetricReader, InstrumentKind, Pipeline, Temporality}, +}; +use serde::Deserialize; +use serde_yaml::Value; + +use crate::metrics::MetricsPullExporterFactory; + +pub struct PrometheusMetricsPullExporterFactory {} + +impl PrometheusMetricsPullExporterFactory { + pub fn new() -> Self { + PrometheusMetricsPullExporterFactory {} + } +} + +impl Default for PrometheusMetricsPullExporterFactory { + fn default() -> Self { + Self::new() + } +} + +impl MetricsPullExporterFactory for PrometheusMetricsPullExporterFactory { + type Exporter = MockPrometheusExporter; + fn create_metrics_pull_exporter( + &self, + _config: &Value, + ) -> Result> { + Err(Box::new(std::io::Error::other( + "Prometheus exporter is not maintained currently", + ))) + } +} + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct PrometheusConfig { + pub host: Option, + pub port: Option, +} + +/// As prometheus exporter is not maintained, implementing a mock version for parsing purposes +#[derive(Debug)] +pub struct MockPrometheusExporter { + _config: PrometheusConfig, +} + +impl MetricReader for MockPrometheusExporter { + fn register_pipeline(&self, _pipeline: Weak) {} + + fn collect(&self, _rm: &mut ResourceMetrics) -> OTelSdkResult { + Ok(()) + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + Ok(()) + } + + /// shutdown with default timeout + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } + + fn temporality(&self, _kind: InstrumentKind) -> Temporality { + Temporality::Cumulative + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_prometheus_metrics_pull_exporter_factory() { + let factory = PrometheusMetricsPullExporterFactory::default(); + if let Err(e) = factory.create_metrics_pull_exporter(&Value::Null) { + let err_msg = e.to_string(); + assert!(err_msg.contains("not maintained")); + } else { + panic!("Expected error for unmaintained Prometheus exporter"); + } + } + + #[test] + fn test_deserialize_config_all_fields_in() { + let yaml_grpc = r#" + host: localhost + port: 9090 + "#; + + let config: PrometheusConfig = serde_yaml::from_str(yaml_grpc).unwrap(); + assert_eq!(config.host, Some("localhost".into())); + assert_eq!(config.port, Some(9090)); + } + + #[test] + fn test_deserialize_config_no_fields_in() { + let yaml_grpc = r#" + "#; + + let config: PrometheusConfig = serde_yaml::from_str(yaml_grpc).unwrap(); + assert_eq!(config.host, None); + } + + #[test] + fn test_deserialize_config_extra_fields_in() { + let yaml_grpc = r#" + unknown: field + "#; + + let config_result: Result = serde_yaml::from_str(yaml_grpc); + assert!(config_result.is_err()); + if let Err(e) = config_result { + let err_msg = e.to_string(); + assert!(err_msg.contains("unknown field")); + } + } +} diff --git a/opentelemetry-declarative-config/src/metrics/exporters/stdout_periodic_exporter.rs b/opentelemetry-declarative-config/src/metrics/exporters/stdout_periodic_exporter.rs new file mode 100644 index 0000000000..e0b1f0e064 --- /dev/null +++ b/opentelemetry-declarative-config/src/metrics/exporters/stdout_periodic_exporter.rs @@ -0,0 +1,103 @@ +//! # Stdout metrics periodic exporter module. +//! +//! This module contains the definitions and common implementations for the Stdout metrics periodic exporter +//! that can be used with OpenTelemetry SDKs. Exporters are responsible for sending +//! collected metrics data to different backends or systems. + +use opentelemetry_sdk::metrics::exporter::PushMetricExporter; + +use serde::Deserialize; +use serde_yaml::Value; + +use crate::metrics::exporters::deserialize_temporality; +use crate::metrics::MetricsPeriodicExporterFactory; + +/// Factory for creating Stdout Metrics Periodic Exporters +pub struct StdoutMetricsPeriodicExporterFactory {} + +impl StdoutMetricsPeriodicExporterFactory { + pub fn new() -> Self { + StdoutMetricsPeriodicExporterFactory {} + } +} + +impl Default for StdoutMetricsPeriodicExporterFactory { + fn default() -> Self { + Self::new() + } +} + +impl MetricsPeriodicExporterFactory for StdoutMetricsPeriodicExporterFactory { + fn create_metrics_periodic_exporter( + &self, + config: &Value, + ) -> Result> { + let mut exporter_builder = opentelemetry_stdout::MetricExporter::builder(); + + let config_parsed = serde_yaml::from_value::(config.clone())?; + + if let Some(temporality) = config_parsed.temporality { + exporter_builder = exporter_builder.with_temporality(temporality); + } + + let exporter = exporter_builder.build(); + Ok(exporter) + } +} + +/// Configuration for Stdout Metrics Exporter +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct StdoutConfig { + #[serde(default, deserialize_with = "deserialize_temporality")] + pub temporality: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_stdout_metrics_periodic_exporter_factory() { + let factory = StdoutMetricsPeriodicExporterFactory::default(); + assert!(factory + .create_metrics_periodic_exporter(&Value::Null) + .is_ok()); + } + + #[test] + fn test_deserialize_config_all_fields_in() { + let yaml_grpc = r#" + temporality: cumulative + "#; + + let config: StdoutConfig = serde_yaml::from_str(yaml_grpc).unwrap(); + assert_eq!( + config.temporality, + Some(opentelemetry_sdk::metrics::Temporality::Cumulative) + ); + } + + #[test] + fn test_deserialize_config_no_fields_in() { + let yaml_grpc = r#" + "#; + + let config: StdoutConfig = serde_yaml::from_str(yaml_grpc).unwrap(); + assert_eq!(config.temporality, None); + } + + #[test] + fn test_deserialize_config_extra_fields_in() { + let yaml_grpc = r#" + unknown: field + "#; + + let config_result: Result = serde_yaml::from_str(yaml_grpc); + assert!(config_result.is_err()); + if let Err(e) = config_result { + let err_msg = e.to_string(); + assert!(err_msg.contains("unknown field")); + } + } +} diff --git a/opentelemetry-declarative-config/src/metrics/reader_config.rs b/opentelemetry-declarative-config/src/metrics/reader_config.rs new file mode 100644 index 0000000000..cfad539ffa --- /dev/null +++ b/opentelemetry-declarative-config/src/metrics/reader_config.rs @@ -0,0 +1,27 @@ +//! # Metrics Reader Configuration module +//! +//! This module defines the configuration structures for metrics readers +//! used in OpenTelemetry SDKs. Readers are responsible for collecting +//! metrics data and exporting it to various backends or systems. + +use serde::Deserialize; +use serde_yaml::Value; + +/// Configuration for Metrics Readers +#[derive(Deserialize)] +pub struct ReaderConfig { + pub pull: Option, + pub periodic: Option, +} + +/// Configuration for Periodic Metrics Reader +#[derive(Deserialize)] +pub struct PeriodicReaderConfig { + pub exporter: Value, +} + +/// Configuration for Pull Metrics Reader +#[derive(Deserialize)] +pub struct PullReaderConfig { + pub exporter: Value, +} diff --git a/opentelemetry-declarative-config/src/telemetry_config.rs b/opentelemetry-declarative-config/src/telemetry_config.rs new file mode 100644 index 0000000000..52d2bfbb10 --- /dev/null +++ b/opentelemetry-declarative-config/src/telemetry_config.rs @@ -0,0 +1,86 @@ +//! # Telemetry Configuration module +//! +//! This module defines the configuration structures for telemetry +//! used in OpenTelemetry SDKs. + +use std::{collections::HashMap, error::Error}; + +use serde::Deserialize; + +use crate::{logs::LogsConfig, metrics::MetricsConfig}; + +/// Configuration for Telemetry +#[derive(Deserialize)] +pub struct TelemetryConfig { + /// Metrics telemetry configuration + pub metrics: Option, + + /// Logs telemetry configuration + pub logs: Option, + + /// Resource attributes to be associated with all telemetry data + #[serde(default)] + pub resource: HashMap, +} + +impl TelemetryConfig { + pub fn new() -> Self { + TelemetryConfig { + metrics: None, + logs: None, + resource: HashMap::new(), + } + } + + /// Creates a TelemetryConfig from a YAML string + pub fn from_yaml(yaml_str: &str) -> Result> { + let config: TelemetryConfig = serde_yaml::from_str(yaml_str)?; + Ok(config) + } + + /// Creates a TelemetryConfig from a YAML file + pub fn from_yaml_file(file_path: &str) -> Result> { + let yaml_str = std::fs::read_to_string(file_path)?; + Self::from_yaml(&yaml_str) + } +} + +impl Default for TelemetryConfig { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_telemetry_config_from_yaml() { + let yaml_str = r#" + resource: + service.name: test-service + metrics: + readers: + - periodic: + exporter: + name: stdout + logs: + processors: + - batch: + exporter: + name: otlp + "#; + let config = TelemetryConfig::from_yaml(yaml_str).unwrap(); + assert!(config.metrics.is_some()); + assert!(config.logs.is_some()); + assert_eq!(config.resource.get("service.name").unwrap(), "test-service"); + } + + #[test] + fn test_telemetry_config_default() { + let config = TelemetryConfig::default(); + assert!(config.metrics.is_none()); + assert!(config.logs.is_none()); + assert!(config.resource.is_empty()); + } +} diff --git a/opentelemetry-declarative-config/tests/configurator_tests.rs b/opentelemetry-declarative-config/tests/configurator_tests.rs new file mode 100644 index 0000000000..26cbc466e7 --- /dev/null +++ b/opentelemetry-declarative-config/tests/configurator_tests.rs @@ -0,0 +1,44 @@ +use opentelemetry_declarative_config::Configurator; + +#[tokio::test] +async fn test_configure_telemetry_from_yaml_file_sample1() -> Result<(), Box> +{ + let configurator = Configurator::new(); + let result = configurator.configure_telemetry_from_yaml_file("tests/sample1.yaml"); + if let Err(ref e) = result { + panic!("Failed to configure telemetry from YAML file: {}", e); + } + assert!(result.is_ok()); + let telemetry_providers = result.unwrap(); + assert!(telemetry_providers.meter_provider().is_some()); + assert!(telemetry_providers.logs_provider().is_some()); + assert!(telemetry_providers.traces_provider().is_none()); + + telemetry_providers.shutdown()?; + Ok(()) +} + +#[tokio::test] +async fn test_configure_telemetry_from_yaml_file_with_extra_field( +) -> Result<(), Box> { + let configurator = Configurator::new(); + let result = configurator.configure_telemetry_from_yaml_file("tests/extra_field.yaml"); + assert!(result.is_err()); + if let Err(ref e) = result { + assert!(e.to_string().contains("unknown field")); + } + Ok(()) +} + +#[tokio::test] +async fn test_configure_telemetry_from_yaml_file_non_implemented_exporter( +) -> Result<(), Box> { + let configurator = Configurator::new(); + let result = + configurator.configure_telemetry_from_yaml_file("tests/non_maintained_exporter.yaml"); + assert!(result.is_err()); + if let Err(ref e) = result { + assert!(e.to_string().contains("not maintained")); + } + Ok(()) +} diff --git a/opentelemetry-declarative-config/tests/extra_field.yaml b/opentelemetry-declarative-config/tests/extra_field.yaml new file mode 100644 index 0000000000..d0a8879d13 --- /dev/null +++ b/opentelemetry-declarative-config/tests/extra_field.yaml @@ -0,0 +1,9 @@ +metrics: + readers: + - periodic: + exporter: + otlp: + protocol: http/protobuf + endpoint: https://backend:4318 + stdout: + other: extra.thing diff --git a/opentelemetry-declarative-config/tests/non_maintained_exporter.yaml b/opentelemetry-declarative-config/tests/non_maintained_exporter.yaml new file mode 100644 index 0000000000..11820e4330 --- /dev/null +++ b/opentelemetry-declarative-config/tests/non_maintained_exporter.yaml @@ -0,0 +1,17 @@ +metrics: + readers: + - periodic: + exporter: + otlp: + protocol: http/protobuf + endpoint: https://backend:4318 + stdout: + temporality: cumulative + - pull: + exporter: + prometheus: + host: "0.0.0.0" + port: 8888 +resource: + service.name: sample-service + service.version: "1.0.0" diff --git a/opentelemetry-declarative-config/tests/sample1.yaml b/opentelemetry-declarative-config/tests/sample1.yaml new file mode 100644 index 0000000000..6094ea4d2b --- /dev/null +++ b/opentelemetry-declarative-config/tests/sample1.yaml @@ -0,0 +1,22 @@ +metrics: + readers: + - periodic: + exporter: + otlp: + protocol: http/protobuf + endpoint: https://backend:4318 + stdout: + temporality: cumulative + +logs: + processors: + - batch: + exporter: + stdout: {} + otlp: + protocol: http/protobuf + endpoint: https://backend:4318 + +resource: + service.name: sample-service + service.version: "1.0.0" diff --git a/opentelemetry-declarative-config/tests/telemetry_config_tests.rs b/opentelemetry-declarative-config/tests/telemetry_config_tests.rs new file mode 100644 index 0000000000..461419d854 --- /dev/null +++ b/opentelemetry-declarative-config/tests/telemetry_config_tests.rs @@ -0,0 +1,74 @@ +use opentelemetry_declarative_config::telemetry_config::TelemetryConfig; + +#[test] +fn test_telemetry_config_from_yaml_sample1() { + let yaml_str = std::fs::read_to_string("tests/sample1.yaml").unwrap(); + let config = TelemetryConfig::from_yaml(&yaml_str).unwrap(); + + // Validate resource attributes + let resource = &config.resource; + if let Some(service_name) = resource.get("service.name") { + assert_eq!(service_name, "sample-service"); + } else { + panic!("service.name not found in resource attributes"); + } + + if let Some(service_version) = resource.get("service.version") { + assert_eq!(service_version, "1.0.0"); + } else { + panic!("service.version not found in resource attributes"); + } + + // Validate metrics configuration + assert!(config.metrics.is_some()); + + let metrics_config = config.metrics.unwrap(); + + assert_eq!(metrics_config.readers.len(), 1); + + let reader = &metrics_config.readers[0]; + + if let Some(periodic_reader) = &reader.periodic { + let otlp_exporter_config = &periodic_reader.exporter.get("otlp").unwrap(); + + assert_eq!( + otlp_exporter_config + .get("protocol") + .unwrap() + .as_str() + .unwrap(), + "http/protobuf" + ); + assert_eq!( + otlp_exporter_config + .get("endpoint") + .unwrap() + .as_str() + .unwrap(), + "https://backend:4318" + ); + } else { + panic!("Expected Periodic reader"); + } + + // validate logs configuration + assert!(config.logs.is_some()); + let logs_config = config.logs.unwrap(); + assert_eq!(logs_config.processors.len(), 1); + let processor = &logs_config.processors[0]; + if let Some(batch_processor) = &processor.batch { + let exporter_config = &batch_processor.exporter; + assert!(exporter_config.get("otlp").is_some()); + } else { + panic!("Expected Batch processor"); + } +} + +#[test] +fn test_telemetry_config_from_empty_yaml() { + let yaml_str = r#""#; + let config = TelemetryConfig::from_yaml(yaml_str).unwrap(); + + assert!(config.metrics.is_none()); + assert!(config.resource.is_empty()); +}