diff --git a/Cargo.lock b/Cargo.lock index f2d7ab1b..65edc5f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -593,7 +593,7 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", - "tower 0.5.3", + "tower", "tracing", ] @@ -743,7 +743,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", "tracing", @@ -780,7 +780,7 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-tungstenite", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", "tracing", @@ -4180,10 +4180,11 @@ dependencies = [ "mockall", "nanoid", "once_cell", - "opentelemetry 0.27.1", + "opentelemetry", "opentelemetry-appender-tracing", - "opentelemetry-otlp 0.27.0", - "opentelemetry_sdk 0.27.1", + "opentelemetry-otlp", + "opentelemetry-stdout", + "opentelemetry_sdk", "paste", "pctx_code_mode", "posthog-rs", @@ -4396,7 +4397,7 @@ dependencies = [ "tokio-stream", "tokio-tungstenite", "tokio-util", - "tower 0.5.3", + "tower", "tower-http", "tracing", "tracing-appender", @@ -6591,20 +6592,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "opentelemetry" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab70038c28ed37b97d8ed414b6429d343a8bbf44c9f79ec854f3a643029ba6d7" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 1.0.69", - "tracing", -] - [[package]] name = "opentelemetry" version = "0.31.0" @@ -6621,29 +6608,16 @@ dependencies = [ [[package]] name = "opentelemetry-appender-tracing" -version = "0.27.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5feffc321035ad94088a7e5333abb4d84a8726e54a802e736ce9dd7237e85b" +checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2" dependencies = [ - "opentelemetry 0.27.1", + "opentelemetry", "tracing", "tracing-core", "tracing-subscriber", ] -[[package]] -name = "opentelemetry-http" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a8a7f5f6ba7c1b286c2fbca0454eaba116f63bbe69ed250b642d36fbb04d80" -dependencies = [ - "async-trait", - "bytes", - "http 1.4.0", - "opentelemetry 0.27.1", - "reqwest 0.12.28", -] - [[package]] name = "opentelemetry-http" version = "0.31.0" @@ -6653,31 +6627,10 @@ dependencies = [ "async-trait", "bytes", "http 1.4.0", - "opentelemetry 0.31.0", + "opentelemetry", "reqwest 0.12.28", ] -[[package]] -name = "opentelemetry-otlp" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" -dependencies = [ - "async-trait", - "futures-core", - "http 1.4.0", - "opentelemetry 0.27.1", - "opentelemetry-http 0.27.0", - "opentelemetry-proto 0.27.0", - "opentelemetry_sdk 0.27.1", - "prost 0.13.5", - "reqwest 0.12.28", - "thiserror 1.0.69", - "tokio", - "tonic 0.12.3", - "tracing", -] - [[package]] name = "opentelemetry-otlp" version = "0.31.0" @@ -6685,62 +6638,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" dependencies = [ "http 1.4.0", - "opentelemetry 0.31.0", - "opentelemetry-http 0.31.0", - "opentelemetry-proto 0.31.0", - "opentelemetry_sdk 0.31.0", - "prost 0.14.3", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", "reqwest 0.12.28", "thiserror 2.0.18", "tokio", - "tonic 0.14.3", + "tonic", "tracing", ] -[[package]] -name = "opentelemetry-proto" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6" -dependencies = [ - "opentelemetry 0.27.1", - "opentelemetry_sdk 0.27.1", - "prost 0.13.5", - "tonic 0.12.3", -] - [[package]] name = "opentelemetry-proto" version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ - "opentelemetry 0.31.0", - "opentelemetry_sdk 0.31.0", - "prost 0.14.3", - "tonic 0.14.3", + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", "tonic-prost", ] [[package]] -name = "opentelemetry_sdk" -version = "0.27.1" +name = "opentelemetry-stdout" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "231e9d6ceef9b0b2546ddf52335785ce41252bc7474ee8ba05bfad277be13ab8" +checksum = "bc8887887e169414f637b18751487cce4e095be787d23fad13c454e2fb1b3811" dependencies = [ - "async-trait", - "futures-channel", - "futures-executor", - "futures-util", - "glob", - "opentelemetry 0.27.1", - "percent-encoding", - "rand 0.8.5", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tokio-stream", - "tracing", + "chrono", + "opentelemetry", + "opentelemetry_sdk", ] [[package]] @@ -6752,7 +6683,7 @@ dependencies = [ "futures-channel", "futures-executor", "futures-util", - "opentelemetry 0.31.0", + "opentelemetry", "percent-encoding", "rand 0.9.2", "thiserror 2.0.18", @@ -6946,8 +6877,8 @@ dependencies = [ "http 1.4.0", "indexmap 2.13.0", "keyring", - "opentelemetry-otlp 0.31.0", - "opentelemetry_sdk 0.31.0", + "opentelemetry-otlp", + "opentelemetry_sdk", "reqwest 0.12.28", "rmcp 0.14.0", "serde", @@ -6955,7 +6886,7 @@ dependencies = [ "shlex", "thiserror 2.0.18", "tokio", - "tonic 0.14.3", + "tonic", "tracing", "url", ] @@ -7375,16 +7306,6 @@ dependencies = [ "windows 0.62.2", ] -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive 0.13.5", -] - [[package]] name = "prost" version = "0.14.3" @@ -7392,20 +7313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", - "prost-derive 0.14.3", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.114", + "prost-derive", ] [[package]] @@ -7940,7 +7848,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.4", "tokio-util", - "tower 0.5.3", + "tower", "tower-http", "tower-service", "url", @@ -10555,36 +10463,6 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" -[[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.7.9", - "base64 0.22.1", - "bytes", - "h2 0.4.13", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.8.1", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost 0.13.5", - "socket2 0.5.10", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tonic" version = "0.14.3" @@ -10608,7 +10486,7 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-stream", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", "tracing", @@ -10621,28 +10499,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6c55a2d6a14174563de34409c9f92ff981d006f56da9c6ecd40d9d4a31500b0" dependencies = [ "bytes", - "prost 0.14.3", - "tonic 0.14.3", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.5", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", + "prost", + "tonic", ] [[package]] @@ -10688,7 +10546,7 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", "tracing", @@ -10764,14 +10622,12 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.28.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a971f6058498b5c0f1affa23e7ea202057a7301dbff68e968b2d578bcbd053" +checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" dependencies = [ "js-sys", - "once_cell", - "opentelemetry 0.27.1", - "opentelemetry_sdk 0.27.1", + "opentelemetry", "smallvec", "tracing", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index b9ad84c9..193a4979 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,3 +62,9 @@ wiremock = "0.6" serial_test = "3.2.0" test-case = "3.3.1" url = "2.5.8" +opentelemetry = "0.31" +opentelemetry_sdk = { version = "0.31", features = ["metrics"] } +opentelemetry-otlp = "0.31" +opentelemetry-appender-tracing = "0.31" +opentelemetry-stdout = { version = "0.31", features = ["trace", "metrics", "logs"] } +tracing-opentelemetry = "0.32" diff --git a/crates/goose-cli/src/logging.rs b/crates/goose-cli/src/logging.rs index 4c59a996..c9763408 100644 --- a/crates/goose-cli/src/logging.rs +++ b/crates/goose-cli/src/logging.rs @@ -6,7 +6,8 @@ use tracing_subscriber::{ Registry, }; -use goose::tracing::{langfuse_layer, otlp_layer}; +use goose::otel::otlp; +use goose::tracing::langfuse_layer; // Used to ensure we only set up tracing once static INIT: Once = Once::new(); @@ -68,25 +69,7 @@ fn setup_logging_internal(name: Option<&str>, force: bool) -> Result<()> { ]; if !force { - if let Ok((otlp_tracing_layer, otlp_metrics_layer, otlp_logs_layer)) = - otlp_layer::init_otlp() - { - layers.push( - otlp_tracing_layer - .with_filter(otlp_layer::create_otlp_tracing_filter()) - .boxed(), - ); - layers.push( - otlp_metrics_layer - .with_filter(otlp_layer::create_otlp_metrics_filter()) - .boxed(), - ); - layers.push( - otlp_logs_layer - .with_filter(otlp_layer::create_otlp_logs_filter()) - .boxed(), - ); - } + layers.extend(otlp::init_otlp_layers(goose::config::Config::global())); } if let Some(langfuse) = langfuse_layer::create_langfuse_observer() { diff --git a/crates/goose-cli/src/main.rs b/crates/goose-cli/src/main.rs index 0278f3d6..97baae1e 100644 --- a/crates/goose-cli/src/main.rs +++ b/crates/goose-cli/src/main.rs @@ -9,28 +9,9 @@ async fn main() -> Result<()> { let result = cli().await; - // Only wait for telemetry flush if OTLP is configured - let should_wait = goose::config::Config::global() - .get_param::("otel_exporter_otlp_endpoint") - .is_ok(); - - if should_wait { - // Use a shorter, dynamic wait with max timeout - let max_wait = tokio::time::Duration::from_millis(500); - let start = tokio::time::Instant::now(); - - // Give telemetry a chance to flush, but don't wait too long - while start.elapsed() < max_wait { - tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; - - // In future, we could check if there are pending spans/metrics here - // For now, we just do a quick wait to allow batch exports to complete - if start.elapsed() >= tokio::time::Duration::from_millis(200) { - break; // Most exports should complete within 200ms - } - } - - goose::tracing::shutdown_otlp(); + if goose::otel::otlp::is_otlp_initialized() { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + goose::otel::otlp::shutdown_otlp(); } result diff --git a/crates/goose-server/src/commands/agent.rs b/crates/goose-server/src/commands/agent.rs index a68d56ee..b9799156 100644 --- a/crates/goose-server/src/commands/agent.rs +++ b/crates/goose-server/src/commands/agent.rs @@ -58,6 +58,12 @@ pub async fn run() -> Result<()> { axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) .await?; + + if goose::otel::otlp::is_otlp_initialized() { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + goose::otel::otlp::shutdown_otlp(); + } + info!("server shutdown complete"); Ok(()) } diff --git a/crates/goose-server/src/logging.rs b/crates/goose-server/src/logging.rs index 75e7c0bb..0ea1f5d0 100644 --- a/crates/goose-server/src/logging.rs +++ b/crates/goose-server/src/logging.rs @@ -5,7 +5,8 @@ use tracing_subscriber::{ Registry, }; -use goose::tracing::{langfuse_layer, otlp_layer}; +use goose::otel::otlp; +use goose::tracing::langfuse_layer; /// Sets up the logging infrastructure for the application. /// This includes: @@ -54,23 +55,7 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> { console_layer.with_filter(base_env_filter).boxed(), ]; - if let Ok((otlp_tracing_layer, otlp_metrics_layer, otlp_logs_layer)) = otlp_layer::init_otlp() { - layers.push( - otlp_tracing_layer - .with_filter(otlp_layer::create_otlp_tracing_filter()) - .boxed(), - ); - layers.push( - otlp_metrics_layer - .with_filter(otlp_layer::create_otlp_metrics_filter()) - .boxed(), - ); - layers.push( - otlp_logs_layer - .with_filter(otlp_layer::create_otlp_logs_filter()) - .boxed(), - ); - } + layers.extend(otlp::init_otlp_layers(goose::config::Config::global())); if let Some(langfuse) = langfuse_layer::create_langfuse_observer() { layers.push(langfuse.with_filter(LevelFilter::DEBUG).boxed()); diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index f386272b..6daab50a 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -52,14 +52,12 @@ webbrowser = { workspace = true } lazy_static = "1.5.0" tracing = { workspace = true } tracing-subscriber = { workspace = true } -tracing-opentelemetry = "0.28" -opentelemetry = "0.27" -opentelemetry-appender-tracing = "0.27" -opentelemetry_sdk = { version = "0.27", features = ["rt-tokio", "metrics"] } -opentelemetry-otlp = { version = "0.27", features = [ - "http-proto", - "reqwest-client", -] } +tracing-opentelemetry = { workspace = true } +opentelemetry = { workspace = true } +opentelemetry-appender-tracing = { workspace = true } +opentelemetry_sdk = { workspace = true } +opentelemetry-otlp = { workspace = true } +opentelemetry-stdout = { workspace = true } keyring = { version = "3.6.2", features = [ "apple-native", "windows-native", diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index 6c83c141..579534ab 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -12,6 +12,7 @@ pub mod logging; pub mod mcp_utils; pub mod model; pub mod oauth; +pub mod otel; pub mod permission; pub mod posthog; pub mod prompt_template; diff --git a/crates/goose/src/otel/mod.rs b/crates/goose/src/otel/mod.rs new file mode 100644 index 00000000..95eb89aa --- /dev/null +++ b/crates/goose/src/otel/mod.rs @@ -0,0 +1 @@ +pub mod otlp; diff --git a/crates/goose/src/otel/otlp.rs b/crates/goose/src/otel/otlp.rs new file mode 100644 index 00000000..dc1cb8d2 --- /dev/null +++ b/crates/goose/src/otel/otlp.rs @@ -0,0 +1,551 @@ +use opentelemetry::trace::TracerProvider; +use opentelemetry::{global, KeyValue}; +use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; +use opentelemetry_sdk::logs::{SdkLogger, SdkLoggerProvider}; +use opentelemetry_sdk::metrics::SdkMeterProvider; +use opentelemetry_sdk::propagation::TraceContextPropagator; +use opentelemetry_sdk::resource::{EnvResourceDetector, TelemetryResourceDetector}; +use opentelemetry_sdk::trace::SdkTracerProvider; +use opentelemetry_sdk::Resource; +use std::env; +use std::sync::Mutex; +use tracing::{Level, Metadata}; +use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer}; +use tracing_subscriber::filter::FilterFn; +use tracing_subscriber::Layer as _; + +pub type OtlpTracingLayer = + OpenTelemetryLayer; +pub type OtlpMetricsLayer = MetricsLayer; +pub type OtlpLogsLayer = OpenTelemetryTracingBridge; +pub type OtlpResult = Result>; + +static TRACER_PROVIDER: Mutex> = Mutex::new(None); +static METER_PROVIDER: Mutex> = Mutex::new(None); +static LOGGER_PROVIDER: Mutex> = Mutex::new(None); + +#[derive(Debug, Clone, PartialEq)] +pub enum ExporterType { + Otlp, + Console, + None, +} + +impl ExporterType { + pub fn from_env_value(value: &str) -> Self { + match value.to_lowercase().as_str() { + "" | "otlp" => ExporterType::Otlp, + "console" | "stdout" => ExporterType::Console, + _ => ExporterType::None, + } + } +} + +/// Returns the exporter type for a signal, or None if disabled. +/// +/// Checks in order: +/// 1. OTEL_SDK_DISABLED — disables everything +/// 2. OTEL_{SIGNAL}_EXPORTER — explicit exporter selection ("none" disables) +/// 3. OTEL_EXPORTER_OTLP_{SIGNAL}_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT — enables OTLP +pub fn signal_exporter(signal: &str) -> Option { + if env::var("OTEL_SDK_DISABLED") + .ok() + .is_some_and(|v| v.eq_ignore_ascii_case("true")) + { + return None; + } + + let exporter_var = format!("OTEL_{}_EXPORTER", signal.to_uppercase()); + if let Ok(val) = env::var(&exporter_var) { + let typ = ExporterType::from_env_value(&val); + return if matches!(typ, ExporterType::None) { + None + } else { + Some(typ) + }; + } + + let signal_endpoint = format!("OTEL_EXPORTER_OTLP_{}_ENDPOINT", signal.to_uppercase()); + let has_endpoint = env::var(&signal_endpoint) + .ok() + .is_some_and(|v| !v.is_empty()) + || env::var("OTEL_EXPORTER_OTLP_ENDPOINT") + .ok() + .is_some_and(|v| !v.is_empty()); + + if has_endpoint { + Some(ExporterType::Otlp) + } else { + None + } +} + +/// Promotes goose config-file OTel settings to env vars before exporter build. +pub fn promote_config_to_env(config: &crate::config::Config) { + if env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_err() { + if let Ok(endpoint) = config.get_param::("otel_exporter_otlp_endpoint") { + env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint); + } + } + if env::var("OTEL_EXPORTER_OTLP_TIMEOUT").is_err() { + if let Ok(timeout) = config.get_param::("otel_exporter_otlp_timeout") { + env::set_var("OTEL_EXPORTER_OTLP_TIMEOUT", timeout.to_string()); + } + } +} + +fn create_resource() -> Resource { + let mut builder = Resource::builder_empty() + .with_attributes([ + KeyValue::new("service.name", "goose"), + KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), + KeyValue::new("service.namespace", "goose"), + ]) + .with_detector(Box::new(EnvResourceDetector::new())) + .with_detector(Box::new(TelemetryResourceDetector)); + + // OTEL_SERVICE_NAME takes highest priority (skip SdkProvidedResourceDetector + // which would fall back to "unknown_service" when unset) + if let Ok(name) = std::env::var("OTEL_SERVICE_NAME") { + if !name.is_empty() { + builder = builder.with_service_name(name); + } + } + builder.build() +} + +/// Initializes all OTLP signal layers (traces, metrics, logs) and propagation. +/// Returns boxed layers ready to add to a subscriber. +pub fn init_otlp_layers( + config: &crate::config::Config, +) -> Vec + Send + Sync>> { + promote_config_to_env(config); + + let mut layers: Vec< + Box + Send + Sync>, + > = Vec::new(); + + if let Ok(layer) = create_otlp_tracing_layer() { + layers.push(layer.with_filter(create_otlp_tracing_filter()).boxed()); + } + if let Ok(layer) = create_otlp_metrics_layer() { + layers.push(layer.with_filter(create_otlp_metrics_filter()).boxed()); + } + if let Ok(layer) = create_otlp_logs_layer() { + layers.push(layer.with_filter(create_otlp_logs_filter()).boxed()); + } + + if !layers.is_empty() { + global::set_text_map_propagator(TraceContextPropagator::new()); + } + + layers +} + +fn create_otlp_tracing_layer() -> OtlpResult { + let exporter = signal_exporter("traces").ok_or("Traces not enabled")?; + let resource = create_resource(); + + let tracer_provider = match exporter { + ExporterType::Otlp => { + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_http() + .build()?; + SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .with_resource(resource) + .build() + } + ExporterType::Console => { + let exporter = opentelemetry_stdout::SpanExporter::default(); + SdkTracerProvider::builder() + .with_simple_exporter(exporter) + .with_resource(resource) + .build() + } + ExporterType::None => return Err("Traces exporter set to none".into()), + }; + + global::set_tracer_provider(tracer_provider.clone()); + let tracer = tracer_provider.tracer("goose"); + *TRACER_PROVIDER.lock().unwrap_or_else(|e| e.into_inner()) = Some(tracer_provider); + + Ok(tracing_opentelemetry::layer().with_tracer(tracer)) +} + +fn create_otlp_metrics_layer() -> OtlpResult { + let exporter = signal_exporter("metrics").ok_or("Metrics not enabled")?; + let resource = create_resource(); + + let meter_provider = match exporter { + ExporterType::Otlp => { + let exporter = opentelemetry_otlp::MetricExporter::builder() + .with_http() + .build()?; + SdkMeterProvider::builder() + .with_resource(resource) + .with_periodic_exporter(exporter) + .build() + } + ExporterType::Console => { + let exporter = opentelemetry_stdout::MetricExporter::default(); + SdkMeterProvider::builder() + .with_resource(resource) + .with_periodic_exporter(exporter) + .build() + } + ExporterType::None => return Err("Metrics exporter set to none".into()), + }; + + global::set_meter_provider(meter_provider.clone()); + *METER_PROVIDER.lock().unwrap_or_else(|e| e.into_inner()) = Some(meter_provider.clone()); + + Ok(MetricsLayer::new(meter_provider)) +} + +fn create_otlp_logs_layer() -> OtlpResult { + let exporter = signal_exporter("logs").ok_or("Logs not enabled")?; + let resource = create_resource(); + + let logger_provider = match exporter { + ExporterType::Otlp => { + let exporter = opentelemetry_otlp::LogExporter::builder() + .with_http() + .build()?; + SdkLoggerProvider::builder() + .with_batch_exporter(exporter) + .with_resource(resource) + .build() + } + ExporterType::Console => { + let exporter = opentelemetry_stdout::LogExporter::default(); + SdkLoggerProvider::builder() + .with_simple_exporter(exporter) + .with_resource(resource) + .build() + } + ExporterType::None => return Err("Logs exporter set to none".into()), + }; + + let bridge = OpenTelemetryTracingBridge::new(&logger_provider); + *LOGGER_PROVIDER.lock().unwrap_or_else(|e| e.into_inner()) = Some(logger_provider); + + Ok(bridge) +} + +pub fn is_otlp_initialized() -> bool { + TRACER_PROVIDER + .lock() + .unwrap_or_else(|e| e.into_inner()) + .is_some() + || METER_PROVIDER + .lock() + .unwrap_or_else(|e| e.into_inner()) + .is_some() + || LOGGER_PROVIDER + .lock() + .unwrap_or_else(|e| e.into_inner()) + .is_some() +} + +/// Creates a custom filter for OTLP tracing that captures: +/// - All spans at INFO level and above +/// - Specific spans marked with "otel.trace" field +/// - Events from specific modules related to telemetry +fn create_otlp_tracing_filter() -> FilterFn) -> bool> { + FilterFn::new(|metadata: &Metadata<'_>| { + if metadata.level() <= &Level::INFO { + return true; + } + + if metadata.level() == &Level::DEBUG { + let target = metadata.target(); + if target.starts_with("goose::") + || target.starts_with("opentelemetry") + || target.starts_with("tracing_opentelemetry") + { + return true; + } + } + + false + }) +} + +/// Creates a custom filter for OTLP metrics that captures: +/// - All events at INFO level and above +/// - Specific events marked with "otel.metric" field +/// - Events that should be converted to metrics +fn create_otlp_metrics_filter() -> FilterFn) -> bool> { + FilterFn::new(|metadata: &Metadata<'_>| { + if metadata.level() <= &Level::INFO { + return true; + } + + if metadata.level() == &Level::DEBUG { + let target = metadata.target(); + if target.starts_with("goose::telemetry") + || target.starts_with("goose::metrics") + || target.contains("metric") + { + return true; + } + } + + false + }) +} + +/// Creates a custom filter for OTLP logs that captures: +/// - All events at WARN level and above +fn create_otlp_logs_filter() -> FilterFn) -> bool> { + FilterFn::new(|metadata: &Metadata<'_>| metadata.level() <= &Level::WARN) +} + +/// Shutdown OTLP providers gracefully +pub fn shutdown_otlp() { + if let Some(provider) = TRACER_PROVIDER + .lock() + .unwrap_or_else(|e| e.into_inner()) + .take() + { + let _ = provider.shutdown(); + } + if let Some(provider) = METER_PROVIDER + .lock() + .unwrap_or_else(|e| e.into_inner()) + .take() + { + let _ = provider.shutdown(); + } + if let Some(provider) = LOGGER_PROVIDER + .lock() + .unwrap_or_else(|e| e.into_inner()) + .take() + { + let _ = provider.shutdown(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use opentelemetry::metrics::{Meter, MeterProvider}; + use opentelemetry::InstrumentationScope; + use std::sync::Arc; + use test_case::test_case; + + // set_meter_provider requires P: MeterProvider, not Arc + struct SavedMeterProvider(Arc); + + impl MeterProvider for SavedMeterProvider { + fn meter_with_scope(&self, scope: InstrumentationScope) -> Meter { + self.0.meter_with_scope(scope) + } + } + + struct OtelTestGuard { + _env: env_lock::EnvGuard<'static>, + prev_tracer: global::GlobalTracerProvider, + prev_meter: Arc, + } + + impl Drop for OtelTestGuard { + fn drop(&mut self) { + global::set_tracer_provider(self.prev_tracer.clone()); + global::set_meter_provider(SavedMeterProvider(self.prev_meter.clone())); + } + } + + fn clear_otel_env(overrides: &[(&str, &str)]) -> OtelTestGuard { + let prev_tracer = global::tracer_provider(); + let prev_meter = global::meter_provider(); + let guard = env_lock::lock_env([ + ("OTEL_SDK_DISABLED", None::<&str>), + ("OTEL_TRACES_EXPORTER", None), + ("OTEL_METRICS_EXPORTER", None), + ("OTEL_LOGS_EXPORTER", None), + ("OTEL_EXPORTER_OTLP_ENDPOINT", None), + ("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", None), + ("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", None), + ("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", None), + ("OTEL_EXPORTER_OTLP_TIMEOUT", None), + ("OTEL_SERVICE_NAME", None), + ("OTEL_RESOURCE_ATTRIBUTES", None), + ]); + for &(k, v) in overrides { + env::set_var(k, v); + } + OtelTestGuard { + _env: guard, + prev_tracer, + prev_meter, + } + } + + #[test] + fn exporter_type_from_env_value() { + assert_eq!(ExporterType::from_env_value("otlp"), ExporterType::Otlp); + assert_eq!(ExporterType::from_env_value("OTLP"), ExporterType::Otlp); + assert_eq!(ExporterType::from_env_value(""), ExporterType::Otlp); + assert_eq!( + ExporterType::from_env_value("console"), + ExporterType::Console + ); + assert_eq!( + ExporterType::from_env_value("stdout"), + ExporterType::Console + ); + assert_eq!(ExporterType::from_env_value("none"), ExporterType::None); + assert_eq!(ExporterType::from_env_value("NONE"), ExporterType::None); + assert_eq!(ExporterType::from_env_value("unknown"), ExporterType::None); + } + + #[test_case(&[("OTEL_SDK_DISABLED", "true")]; "OTEL_SDK_DISABLED disables all signals")] + #[test_case(&[]; "no env vars returns None")] + fn signal_exporter_disabled(env: &[(&str, &str)]) { + let _guard = clear_otel_env(env); + assert!(signal_exporter("traces").is_none()); + assert!(signal_exporter("metrics").is_none()); + assert!(signal_exporter("logs").is_none()); + } + + #[test_case("traces", &[("OTEL_TRACES_EXPORTER", "console")], Some(ExporterType::Console); "OTEL_TRACES_EXPORTER=console")] + #[test_case("traces", &[("OTEL_TRACES_EXPORTER", "none")], None; "OTEL_TRACES_EXPORTER=none")] + #[test_case("traces", &[("OTEL_TRACES_EXPORTER", "otlp")], Some(ExporterType::Otlp); "OTEL_TRACES_EXPORTER=otlp")] + #[test_case("metrics", &[("OTEL_METRICS_EXPORTER", "console")], Some(ExporterType::Console); "OTEL_METRICS_EXPORTER=console")] + #[test_case("logs", &[("OTEL_LOGS_EXPORTER", "none")], None; "OTEL_LOGS_EXPORTER=none")] + fn signal_exporter_by_var(signal: &str, env: &[(&str, &str)], expected: Option) { + let _guard = clear_otel_env(env); + assert_eq!(signal_exporter(signal), expected); + } + + #[test_case("traces", &[("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")], Some(ExporterType::Otlp); "generic endpoint enables traces")] + #[test_case("traces", &[("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://localhost:4318")], Some(ExporterType::Otlp); "signal-specific endpoint enables traces")] + #[test_case("metrics", &[("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://localhost:4318")], Some(ExporterType::Otlp); "signal-specific endpoint enables metrics")] + #[test_case("traces", &[("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://localhost:4318")], None; "metrics endpoint does not enable traces")] + #[test_case("traces", &[("OTEL_TRACES_EXPORTER", "none"), ("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")], None; "OTEL_TRACES_EXPORTER=none overrides endpoint")] + #[test_case("traces", &[("OTEL_EXPORTER_OTLP_ENDPOINT", "")], None; "empty endpoint returns None")] + fn signal_exporter_endpoints( + signal: &str, + env: &[(&str, &str)], + expected: Option, + ) { + let _guard = clear_otel_env(env); + assert_eq!(signal_exporter(signal), expected); + } + + #[test_case("console"; "console")] + #[test_case("otlp"; "otlp")] + fn test_all_layers_ok(exporter: &str) { + let rt = tokio::runtime::Runtime::new().unwrap(); + let _guard = rt.enter(); + let _env = clear_otel_env(&[ + ("OTEL_TRACES_EXPORTER", exporter), + ("OTEL_METRICS_EXPORTER", exporter), + ("OTEL_LOGS_EXPORTER", exporter), + ("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"), + ]); + assert!(create_otlp_tracing_layer().is_ok()); + assert!(create_otlp_metrics_layer().is_ok()); + assert!(create_otlp_logs_layer().is_ok()); + shutdown_otlp(); + } + + #[test_case( + &[], + Resource::builder_empty() + .with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")]) + .with_detector(Box::new(TelemetryResourceDetector)) + .build(); + "no env vars uses goose defaults" + )] + #[test_case( + &[("OTEL_SERVICE_NAME", "custom")], + Resource::builder_empty() + .with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")]) + .with_detector(Box::new(TelemetryResourceDetector)) + .with_service_name("custom") + .build(); + "OTEL_SERVICE_NAME overrides service.name" + )] + #[test_case( + &[("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=prod")], + Resource::builder_empty() + .with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")]) + .with_detector(Box::new(TelemetryResourceDetector)) + .with_attribute(KeyValue::new("deployment.environment", "prod")) + .build(); + "OTEL_RESOURCE_ATTRIBUTES adds custom attributes" + )] + #[test_case( + &[("OTEL_SERVICE_NAME", "custom"), ("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=prod")], + Resource::builder_empty() + .with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")]) + .with_detector(Box::new(TelemetryResourceDetector)) + .with_service_name("custom") + .with_attribute(KeyValue::new("deployment.environment", "prod")) + .build(); + "OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES combine" + )] + fn test_create_resource(env: &[(&str, &str)], expected: Resource) { + let _guard = clear_otel_env(env); + assert_eq!(create_resource(), expected); + } + + fn test_config( + params: &[(&str, &str)], + ) -> ( + crate::config::Config, + tempfile::NamedTempFile, + tempfile::NamedTempFile, + ) { + let config_file = tempfile::NamedTempFile::new().unwrap(); + let secrets_file = tempfile::NamedTempFile::new().unwrap(); + let yaml: String = params.iter().map(|(k, v)| format!("{k}: {v}\n")).collect(); + std::fs::write(config_file.path(), yaml).unwrap(); + let config = + crate::config::Config::new_with_file_secrets(config_file.path(), secrets_file.path()) + .unwrap(); + (config, config_file, secrets_file) + } + + #[test_case( + &[], + &[("otel_exporter_otlp_endpoint", "http://config:4318"), ("otel_exporter_otlp_timeout", "5000")], + Some("http://config:4318"), Some("5000"); + "config promotes to env when unset" + )] + #[test_case( + &[("OTEL_EXPORTER_OTLP_ENDPOINT", "http://env:4318"), ("OTEL_EXPORTER_OTLP_TIMEOUT", "3000")], + &[("otel_exporter_otlp_endpoint", "http://config:4318"), ("otel_exporter_otlp_timeout", "5000")], + Some("http://env:4318"), Some("3000"); + "env var takes precedence over config" + )] + #[test_case( + &[], + &[], + None, None; + "no config leaves env unset" + )] + fn test_promote_config_to_env( + env_overrides: &[(&str, &str)], + cfg: &[(&str, &str)], + expect_endpoint: Option<&str>, + expect_timeout: Option<&str>, + ) { + let _guard = clear_otel_env(env_overrides); + let (config, _cf, _sf) = test_config(cfg); + + promote_config_to_env(&config); + + assert_eq!( + env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok().as_deref(), + expect_endpoint + ); + assert_eq!( + env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok().as_deref(), + expect_timeout + ); + } +} diff --git a/crates/goose/src/tracing/mod.rs b/crates/goose/src/tracing/mod.rs index 8acd9203..94bff730 100644 --- a/crates/goose/src/tracing/mod.rs +++ b/crates/goose/src/tracing/mod.rs @@ -1,16 +1,11 @@ pub mod langfuse_layer; mod observation_layer; -pub mod otlp_layer; pub mod rate_limiter; pub use langfuse_layer::{create_langfuse_observer, LangfuseBatchManager}; pub use observation_layer::{ flatten_metadata, map_level, BatchManager, ObservationLayer, SpanData, SpanTracker, }; -pub use otlp_layer::{ - create_otlp_metrics_filter, create_otlp_tracing_filter, create_otlp_tracing_layer, - init_otlp_metrics, init_otlp_tracing, init_otlp_tracing_only, shutdown_otlp, OtlpConfig, -}; pub use rate_limiter::{ MetricData, RateLimitedTelemetrySender, SpanData as RateLimitedSpanData, TelemetryEvent, }; diff --git a/crates/goose/src/tracing/otlp_layer.rs b/crates/goose/src/tracing/otlp_layer.rs deleted file mode 100644 index 5a357634..00000000 --- a/crates/goose/src/tracing/otlp_layer.rs +++ /dev/null @@ -1,337 +0,0 @@ -use opentelemetry::trace::TracerProvider; -use opentelemetry::{global, KeyValue}; -use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; -use opentelemetry_otlp::WithExportConfig; -use opentelemetry_sdk::logs::{Logger, LoggerProvider}; -use opentelemetry_sdk::trace::{self, RandomIdGenerator, Sampler}; -use opentelemetry_sdk::{runtime, Resource}; -use std::time::Duration; -use tracing::{Level, Metadata}; -use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer}; -use tracing_subscriber::filter::FilterFn; - -pub type OtlpTracingLayer = - OpenTelemetryLayer; -pub type OtlpMetricsLayer = MetricsLayer; -pub type OtlpLogsLayer = OpenTelemetryTracingBridge; -pub type OtlpLayers = (OtlpTracingLayer, OtlpMetricsLayer, OtlpLogsLayer); -pub type OtlpResult = Result>; - -#[derive(Debug, Clone)] -pub struct OtlpConfig { - pub endpoint: String, - pub timeout: Duration, -} - -impl Default for OtlpConfig { - fn default() -> Self { - Self { - endpoint: "http://localhost:4318".to_string(), - timeout: Duration::from_secs(10), - } - } -} - -impl OtlpConfig { - pub fn from_config() -> Option { - let config = crate::config::Config::global(); - - // Try to get the endpoint from config (checks OTEL_EXPORTER_OTLP_ENDPOINT env var first) - let endpoint = config - .get_param::("otel_exporter_otlp_endpoint") - .ok()?; - - let mut otlp_config = Self { - endpoint, - timeout: Duration::from_secs(10), - }; - - // Try to get timeout from config (checks OTEL_EXPORTER_OTLP_TIMEOUT env var first) - if let Ok(timeout_ms) = config.get_param::("otel_exporter_otlp_timeout") { - otlp_config.timeout = Duration::from_millis(timeout_ms); - } - - Some(otlp_config) - } -} - -pub fn init_otlp_tracing(config: &OtlpConfig) -> OtlpResult<()> { - let resource = Resource::new(vec![ - KeyValue::new("service.name", "goose"), - KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), - KeyValue::new("service.namespace", "goose"), - ]); - - let exporter = opentelemetry_otlp::SpanExporter::builder() - .with_http() - .with_endpoint(&config.endpoint) - .with_timeout(config.timeout) - .build()?; - - let tracer_provider = trace::TracerProvider::builder() - .with_batch_exporter(exporter, runtime::Tokio) - .with_resource(resource.clone()) - .with_id_generator(RandomIdGenerator::default()) - .with_sampler(Sampler::AlwaysOn) - .build(); - - global::set_tracer_provider(tracer_provider); - - Ok(()) -} - -pub fn init_otlp_metrics(config: &OtlpConfig) -> OtlpResult<()> { - let resource = Resource::new(vec![ - KeyValue::new("service.name", "goose"), - KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), - KeyValue::new("service.namespace", "goose"), - ]); - - let exporter = opentelemetry_otlp::MetricExporter::builder() - .with_http() - .with_endpoint(&config.endpoint) - .with_timeout(config.timeout) - .build()?; - - let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder() - .with_resource(resource) - .with_reader( - opentelemetry_sdk::metrics::PeriodicReader::builder(exporter, runtime::Tokio) - .with_interval(Duration::from_secs(3)) - .build(), - ) - .build(); - - global::set_meter_provider(meter_provider); - - Ok(()) -} - -pub fn create_otlp_tracing_layer() -> OtlpResult { - let config = OtlpConfig::from_config().ok_or("OTEL_EXPORTER_OTLP_ENDPOINT not configured")?; - - let resource = Resource::new(vec![ - KeyValue::new("service.name", "goose"), - KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), - KeyValue::new("service.namespace", "goose"), - ]); - - let exporter = opentelemetry_otlp::SpanExporter::builder() - .with_http() - .with_endpoint(&config.endpoint) - .with_timeout(config.timeout) - .build()?; - - let tracer_provider = trace::TracerProvider::builder() - .with_batch_exporter(exporter, runtime::Tokio) - .with_max_events_per_span(2048) - .with_max_attributes_per_span(512) - .with_max_links_per_span(512) - .with_resource(resource) - .with_id_generator(RandomIdGenerator::default()) - .with_sampler(Sampler::TraceIdRatioBased(0.1)) - .build(); - - let tracer = tracer_provider.tracer("goose"); - Ok(tracing_opentelemetry::layer().with_tracer(tracer)) -} - -pub fn create_otlp_metrics_layer() -> OtlpResult { - let config = OtlpConfig::from_config().ok_or("OTEL_EXPORTER_OTLP_ENDPOINT not configured")?; - - let resource = Resource::new(vec![ - KeyValue::new("service.name", "goose"), - KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), - KeyValue::new("service.namespace", "goose"), - ]); - - let exporter = opentelemetry_otlp::MetricExporter::builder() - .with_http() - .with_endpoint(&config.endpoint) - .with_timeout(config.timeout) - .build()?; - - let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder() - .with_resource(resource) - .with_reader( - opentelemetry_sdk::metrics::PeriodicReader::builder(exporter, runtime::Tokio) - .with_interval(Duration::from_millis(2000)) - .build(), - ) - .build(); - - global::set_meter_provider(meter_provider.clone()); - - Ok(tracing_opentelemetry::MetricsLayer::new(meter_provider)) -} - -pub fn create_otlp_logs_layer() -> OtlpResult> { - let config = OtlpConfig::from_config().ok_or("OTEL_EXPORTER_OTLP_ENDPOINT not configured")?; - - let resource = Resource::new(vec![ - KeyValue::new("service.name", "goose"), - KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), - KeyValue::new("service.namespace", "goose"), - ]); - - let exporter = opentelemetry_otlp::LogExporter::builder() - .with_http() - .with_endpoint(&config.endpoint) - .with_timeout(config.timeout) - .build()?; - - let logger_provider = LoggerProvider::builder() - .with_batch_exporter(exporter, runtime::Tokio) - .with_resource(resource) - .build(); - - Ok(OpenTelemetryTracingBridge::new(&logger_provider)) -} - -pub fn init_otlp() -> OtlpResult { - let tracing_layer = create_otlp_tracing_layer()?; - let metrics_layer = create_otlp_metrics_layer()?; - let logs_layer = create_otlp_logs_layer()?; - Ok((tracing_layer, metrics_layer, logs_layer)) -} - -pub fn init_otlp_tracing_only() -> OtlpResult { - create_otlp_tracing_layer() -} - -/// Creates a custom filter for OTLP tracing that captures: -/// - All spans at INFO level and above -/// - Specific spans marked with "otel.trace" field -/// - Events from specific modules related to telemetry -pub fn create_otlp_tracing_filter() -> FilterFn) -> bool> { - FilterFn::new(|metadata: &Metadata<'_>| { - if metadata.level() <= &Level::INFO { - return true; - } - - if metadata.level() == &Level::DEBUG { - let target = metadata.target(); - if target.starts_with("goose::") - || target.starts_with("opentelemetry") - || target.starts_with("tracing_opentelemetry") - { - return true; - } - } - - false - }) -} - -/// Creates a custom filter for OTLP metrics that captures: -/// - All events at INFO level and above -/// - Specific events marked with "otel.metric" field -/// - Events that should be converted to metrics -pub fn create_otlp_metrics_filter() -> FilterFn) -> bool> { - FilterFn::new(|metadata: &Metadata<'_>| { - if metadata.level() <= &Level::INFO { - return true; - } - - if metadata.level() == &Level::DEBUG { - let target = metadata.target(); - if target.starts_with("goose::telemetry") - || target.starts_with("goose::metrics") - || target.contains("metric") - { - return true; - } - } - - false - }) -} - -/// Creates a custom filter for OTLP metrics that captures: -/// - All events at WARN level and above -pub fn create_otlp_logs_filter() -> FilterFn) -> bool> { - FilterFn::new(|metadata: &Metadata<'_>| { - if metadata.level() <= &Level::WARN { - return true; - } - - false - }) -} - -/// Shutdown OTLP providers gracefully -pub fn shutdown_otlp() { - // Shutdown the tracer provider and flush any pending spans - global::shutdown_tracer_provider(); - - // Force flush of metrics by waiting a bit - // The meter provider doesn't have a direct shutdown method in the current SDK, - // but we can give it time to export any pending metrics - std::thread::sleep(std::time::Duration::from_millis(500)); -} - -#[cfg(test)] -mod tests { - use super::*; - use std::env; - - #[test] - fn test_otlp_config_default() { - let config = OtlpConfig::default(); - assert_eq!(config.endpoint, "http://localhost:4318"); - assert_eq!(config.timeout, Duration::from_secs(10)); - } - - #[test] - fn test_otlp_config_from_config() { - use tempfile::NamedTempFile; - - // Save original env vars - let original_endpoint = env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok(); - let original_timeout = env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok(); - - // Clear env vars to ensure we're testing config file - env::remove_var("OTEL_EXPORTER_OTLP_ENDPOINT"); - env::remove_var("OTEL_EXPORTER_OTLP_TIMEOUT"); - - // Create a test config file - let temp_file = NamedTempFile::new().unwrap(); - let test_config = crate::config::Config::new(temp_file.path(), "test-otlp").unwrap(); - - // Set values in config - test_config - .set_param("otel_exporter_otlp_endpoint", "http://config:4318") - .unwrap(); - test_config - .set_param("otel_exporter_otlp_timeout", 3000) - .unwrap(); - - // Test that from_config reads from the config file - // Note: We can't easily test from_config() directly since it uses Config::global() - // But we can test that the config system works with our keys - let endpoint: String = test_config - .get_param("otel_exporter_otlp_endpoint") - .unwrap(); - assert_eq!(endpoint, "http://config:4318"); - - let timeout: u64 = test_config.get_param("otel_exporter_otlp_timeout").unwrap(); - assert_eq!(timeout, 3000); - - // Test env var override still works - env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", "http://env:4317"); - let endpoint: String = test_config - .get_param("otel_exporter_otlp_endpoint") - .unwrap(); - assert_eq!(endpoint, "http://env:4317"); - - // Restore original env vars - match original_endpoint { - Some(val) => env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", val), - None => env::remove_var("OTEL_EXPORTER_OTLP_ENDPOINT"), - } - match original_timeout { - Some(val) => env::set_var("OTEL_EXPORTER_OTLP_TIMEOUT", val), - None => env::remove_var("OTEL_EXPORTER_OTLP_TIMEOUT"), - } - } -}