diff --git a/Cargo.lock b/Cargo.lock index b434baef..a53e2a9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -992,13 +992,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 1.0.2", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "axum" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" dependencies = [ - "axum-core", + "axum-core 0.5.0", "axum-macros", "base64 0.22.1", "bytes", @@ -1010,7 +1037,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "itoa", - "matchit", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", @@ -1030,6 +1057,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-core" version = "0.5.0" @@ -2695,7 +2742,7 @@ dependencies = [ "futures", "hyper 0.14.32", "hyper-rustls 0.24.2", - "hyper-timeout", + "hyper-timeout 0.4.1", "log", "pin-project", "rand 0.8.5", @@ -3246,7 +3293,7 @@ dependencies = [ "aws-sdk-bedrockruntime", "aws-sdk-sagemakerruntime", "aws-smithy-types", - "axum", + "axum 0.8.1", "base64 0.21.7", "blake3", "chrono", @@ -3271,6 +3318,9 @@ dependencies = [ "mockall", "nanoid", "once_cell", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "rand 0.8.5", "regex", "reqwest 0.12.12", @@ -3289,7 +3339,9 @@ dependencies = [ "tokio-cron-scheduler", "tokio-stream", "tokio-util", + "tonic", "tracing", + "tracing-opentelemetry", "tracing-subscriber", "url", "urlencoding", @@ -3329,7 +3381,7 @@ version = "1.1.0" dependencies = [ "anyhow", "async-trait", - "axum", + "axum 0.8.1", "base64 0.22.1", "bat", "chrono", @@ -3429,7 +3481,7 @@ version = "1.1.0" dependencies = [ "anyhow", "async-trait", - "axum", + "axum 0.8.1", "base64 0.21.7", "bytes", "chrono", @@ -3799,6 +3851,19 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.6.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.10" @@ -4391,7 +4456,7 @@ dependencies = [ "object_store", "permutation", "pin-project", - "prost", + "prost 0.12.6", "prost-build", "rand 0.8.5", "roaring", @@ -4450,7 +4515,7 @@ dependencies = [ "num_cpus", "object_store", "pin-project", - "prost", + "prost 0.12.6", "rand 0.8.5", "roaring", "serde_json", @@ -4484,7 +4549,7 @@ dependencies = [ "lance-core", "lazy_static", "log", - "prost", + "prost 0.12.6", "snafu", "tokio", ] @@ -4518,7 +4583,7 @@ dependencies = [ "log", "num-traits", "paste", - "prost", + "prost 0.12.6", "prost-build", "prost-types", "rand 0.8.5", @@ -4555,7 +4620,7 @@ dependencies = [ "log", "num-traits", "object_store", - "prost", + "prost 0.12.6", "prost-build", "prost-types", "roaring", @@ -4603,7 +4668,7 @@ dependencies = [ "moka", "num-traits", "object_store", - "prost", + "prost 0.12.6", "prost-build", "rand 0.8.5", "rayon", @@ -4649,7 +4714,7 @@ dependencies = [ "object_store", "path_abs", "pin-project", - "prost", + "prost 0.12.6", "prost-build", "rand 0.8.5", "shellexpand", @@ -4710,7 +4775,7 @@ dependencies = [ "lazy_static", "log", "object_store", - "prost", + "prost 0.12.6", "prost-build", "prost-types", "rand 0.8.5", @@ -5065,6 +5130,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "matchit" version = "0.8.4" @@ -5087,7 +5158,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "axum", + "axum 0.8.1", "base64 0.22.1", "eventsource-client", "futures", @@ -5754,6 +5825,87 @@ 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-http" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a8a7f5f6ba7c1b286c2fbca0454eaba116f63bbe69ed250b642d36fbb04d80" +dependencies = [ + "async-trait", + "bytes", + "http 1.2.0", + "opentelemetry", + "reqwest 0.12.12", +] + +[[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.2.0", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost 0.13.5", + "reqwest 0.12.12", + "thiserror 1.0.69", + "tokio", + "tonic", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost 0.13.5", + "tonic", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231e9d6ceef9b0b2546ddf52335785ce41252bc7474ee8ba05bfad277be13ab8" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry", + "percent-encoding", + "rand 0.8.5", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tracing", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -6192,7 +6344,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.12.6", +] + +[[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]] @@ -6209,7 +6371,7 @@ dependencies = [ "once_cell", "petgraph", "prettyplease", - "prost", + "prost 0.12.6", "prost-types", "regex", "syn 2.0.99", @@ -6229,13 +6391,26 @@ dependencies = [ "syn 2.0.99", ] +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "prost-types" version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ - "prost", + "prost 0.12.6", ] [[package]] @@ -8335,6 +8510,36 @@ dependencies = [ "winnow", ] +[[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.8", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-timeout 0.5.2", + "hyper-util", + "percent-encoding", + "pin-project", + "prost 0.13.5", + "socket2 0.5.8", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.4.13" @@ -8343,9 +8548,13 @@ 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", @@ -8460,6 +8669,24 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a971f6058498b5c0f1affa23e7ea202057a7301dbff68e968b2d578bcbd053" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + [[package]] name = "tracing-serde" version = "0.2.0" diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 072613a7..346715d0 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -701,6 +701,28 @@ pub async fn cli() -> Result<()> { eprintln!("Warning: Failed to update project tracker: {}", e); } + let command_name = match &cli.command { + Some(Command::Configure {}) => "configure", + Some(Command::Info { .. }) => "info", + Some(Command::Mcp { .. }) => "mcp", + Some(Command::Session { .. }) => "session", + Some(Command::Project {}) => "project", + Some(Command::Projects) => "projects", + Some(Command::Run { .. }) => "run", + Some(Command::Schedule { .. }) => "schedule", + Some(Command::Update { .. }) => "update", + Some(Command::Bench { .. }) => "bench", + Some(Command::Recipe { .. }) => "recipe", + Some(Command::Web { .. }) => "web", + None => "default_session", + }; + + tracing::info!( + monotonic_counter.goose.cli_commands = 1, + command = command_name, + "CLI command executed" + ); + match cli.command { Some(Command::Configure {}) => { let _ = handle_configure().await; @@ -875,6 +897,11 @@ pub async fn cli() -> Result<()> { (input_config, None) } (_, _, Some(recipe_name)) => { + tracing::info!(monotonic_counter.goose.recipe_runs = 1, + recipe_name = %recipe_name, + "Recipe execution started" + ); + if explain { explain_recipe(&recipe_name, params)?; return Ok(()); diff --git a/crates/goose-cli/src/logging.rs b/crates/goose-cli/src/logging.rs index 630919d5..97ae3f1c 100644 --- a/crates/goose-cli/src/logging.rs +++ b/crates/goose-cli/src/logging.rs @@ -11,7 +11,7 @@ use tracing_subscriber::{ Registry, }; -use goose::tracing::langfuse_layer; +use goose::tracing::{langfuse_layer, otlp_layer}; use goose_bench::bench_session::BenchAgentError; use goose_bench::error_capture::ErrorCaptureLayer; @@ -140,7 +140,21 @@ fn setup_logging_internal( layers.push(ErrorCaptureLayer::new().boxed()); } - // Add Langfuse layer if available + if !force { + if let Ok((otlp_tracing_layer, otlp_metrics_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(), + ); + } + } + if let Some(langfuse) = langfuse_layer::create_langfuse_observer() { layers.push(langfuse.with_filter(LevelFilter::DEBUG).boxed()); } diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 61579085..106bc0a0 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -1009,6 +1009,17 @@ impl Session { } // otherwise we have a model/tool to render else { + for content in &message.content { + if let MessageContent::ToolRequest(tool_request) = content { + if let Ok(tool_call) = &tool_request.tool_call { + tracing::info!(monotonic_counter.goose.tool_calls = 1, + tool_name = %tool_call.name, + "Tool call executed" + ); + } + } + } + push_message(&mut self.messages, message.clone()); // No need to update description on assistant messages diff --git a/crates/goose-server/src/logging.rs b/crates/goose-server/src/logging.rs index 90db8f8a..0af49603 100644 --- a/crates/goose-server/src/logging.rs +++ b/crates/goose-server/src/logging.rs @@ -9,7 +9,7 @@ use tracing_subscriber::{ }; use goose::config::APP_STRATEGY; -use goose::tracing::langfuse_layer; +use goose::tracing::{langfuse_layer, otlp_layer}; /// Returns the directory where log files should be stored. /// Creates the directory structure if it doesn't exist. @@ -91,22 +91,33 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> { .add_directive(LevelFilter::WARN.into()) }); - // Build the subscriber with required layers - let subscriber = Registry::default() - .with(file_layer.with_filter(env_filter)) - .with(console_layer.with_filter(LevelFilter::INFO)); + let mut layers = vec![ + file_layer.with_filter(env_filter).boxed(), + console_layer.with_filter(LevelFilter::INFO).boxed(), + ]; - // Initialize with Langfuse if available - if let Some(langfuse) = langfuse_layer::create_langfuse_observer() { - subscriber - .with(langfuse.with_filter(LevelFilter::DEBUG)) - .try_init() - .context("Failed to set global subscriber")?; - } else { - subscriber - .try_init() - .context("Failed to set global subscriber")?; + if let Ok((otlp_tracing_layer, otlp_metrics_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(), + ); } + if let Some(langfuse) = langfuse_layer::create_langfuse_observer() { + layers.push(langfuse.with_filter(LevelFilter::DEBUG).boxed()); + } + + let subscriber = Registry::default().with(layers); + + subscriber + .try_init() + .context("Failed to set global subscriber")?; + Ok(()) } diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index ceb05db3..bfccccf5 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -58,6 +58,11 @@ webbrowser = "0.8" lazy_static = "1.5.0" tracing = "0.1" tracing-subscriber = "0.3" +tracing-opentelemetry = "0.28" +opentelemetry = "0.27" +opentelemetry_sdk = { version = "0.27", features = ["rt-tokio", "metrics"] } +opentelemetry-otlp = { version = "0.27", features = ["grpc-tonic", "http-proto", "reqwest-client"] } +tonic = "0.12" keyring = { version = "3.6.2", features = ["apple-native", "windows-native", "sync-secret-service", "vendored"] } serde_yaml = "0.9.34" once_cell = "1.20.2" diff --git a/crates/goose/src/tracing/mod.rs b/crates/goose/src/tracing/mod.rs index caa6bcd9..3d5f34c9 100644 --- a/crates/goose/src/tracing/mod.rs +++ b/crates/goose/src/tracing/mod.rs @@ -1,7 +1,12 @@ pub mod langfuse_layer; mod observation_layer; +pub mod otlp_layer; 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, +}; diff --git a/crates/goose/src/tracing/otlp_layer.rs b/crates/goose/src/tracing/otlp_layer.rs new file mode 100644 index 00000000..b2ce425b --- /dev/null +++ b/crates/goose/src/tracing/otlp_layer.rs @@ -0,0 +1,267 @@ +use opentelemetry::trace::TracerProvider; +use opentelemetry::{global, KeyValue}; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::trace::{self, RandomIdGenerator, Sampler}; +use opentelemetry_sdk::{runtime, Resource}; +use std::env; +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 OtlpLayers = (OtlpTracingLayer, OtlpMetricsLayer); +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_env() -> Option { + if let Ok(endpoint) = env::var("OTEL_EXPORTER_OTLP_ENDPOINT") { + let mut config = Self { + endpoint, + timeout: Duration::from_secs(10), + }; + + if let Ok(timeout_str) = env::var("OTEL_EXPORTER_OTLP_TIMEOUT") { + if let Ok(timeout_ms) = timeout_str.parse::() { + config.timeout = Duration::from_millis(timeout_ms); + } + } + + Some(config) + } else { + None + } + } +} + +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_env().ok_or("OTEL_EXPORTER_OTLP_ENDPOINT environment variable not set")?; + + 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) + .with_id_generator(RandomIdGenerator::default()) + .with_sampler(Sampler::AlwaysOn) + .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_env().ok_or("OTEL_EXPORTER_OTLP_ENDPOINT environment variable not set")?; + + 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(5)) // Reduced from 30s to 5s for faster metrics + .build(), + ) + .build(); + + global::set_meter_provider(meter_provider.clone()); + + Ok(tracing_opentelemetry::MetricsLayer::new(meter_provider)) +} + +pub fn init_otlp() -> OtlpResult { + let tracing_layer = create_otlp_tracing_layer()?; + let metrics_layer = create_otlp_metrics_layer()?; + Ok((tracing_layer, metrics_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 + }) +} + +/// Shutdown OTLP providers gracefully +pub fn shutdown_otlp() { + global::shutdown_tracer_provider(); + + // Note: There's currently no clean way to shutdown the global meter provider + // in the OpenTelemetry Rust SDK. The meter provider will be cleaned up when + // the process exits. Individual meter providers can be shut down if you have + // a direct reference to them. +} + +#[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_env() { + let original_endpoint = env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok(); + let original_timeout = env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok(); + + env::remove_var("OTEL_EXPORTER_OTLP_ENDPOINT"); + assert!(OtlpConfig::from_env().is_none()); + + env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", "http://test:4317"); + env::set_var("OTEL_EXPORTER_OTLP_TIMEOUT", "5000"); + + let config = OtlpConfig::from_env().unwrap(); + assert_eq!(config.endpoint, "http://test:4317"); + assert_eq!(config.timeout, Duration::from_millis(5000)); + + 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"), + } + } +}