diff --git a/Cargo.lock b/Cargo.lock index 18578f0b1640..bc412fd7e273 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2646,6 +2646,7 @@ dependencies = [ "oauth2", "once_cell", "opentelemetry", + "opentelemetry-appender-tracing", "opentelemetry-otlp", "opentelemetry_sdk", "rand 0.8.5", @@ -4559,6 +4560,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry-appender-tracing" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5feffc321035ad94088a7e5333abb4d84a8726e54a802e736ce9dd7237e85b" +dependencies = [ + "opentelemetry", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "opentelemetry-http" version = "0.27.0" diff --git a/crates/goose-cli/src/logging.rs b/crates/goose-cli/src/logging.rs index 9d3887246047..f65fe5a4df6a 100644 --- a/crates/goose-cli/src/logging.rs +++ b/crates/goose-cli/src/logging.rs @@ -102,7 +102,9 @@ fn setup_logging_internal( } if !force { - if let Ok((otlp_tracing_layer, otlp_metrics_layer)) = otlp_layer::init_otlp() { + 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()) @@ -113,6 +115,11 @@ fn setup_logging_internal( .with_filter(otlp_layer::create_otlp_metrics_filter()) .boxed(), ); + layers.push( + otlp_logs_layer + .with_filter(otlp_layer::create_otlp_logs_filter()) + .boxed(), + ); } } diff --git a/crates/goose-server/src/logging.rs b/crates/goose-server/src/logging.rs index c4ffd92870e5..7179e3cf7da3 100644 --- a/crates/goose-server/src/logging.rs +++ b/crates/goose-server/src/logging.rs @@ -74,7 +74,7 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> { console_layer.with_filter(LevelFilter::INFO).boxed(), ]; - if let Ok((otlp_tracing_layer, otlp_metrics_layer)) = otlp_layer::init_otlp() { + 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()) @@ -85,6 +85,11 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> { .with_filter(otlp_layer::create_otlp_metrics_filter()) .boxed(), ); + layers.push( + otlp_logs_layer + .with_filter(otlp_layer::create_otlp_logs_filter()) + .boxed(), + ); } if let Some(langfuse) = langfuse_layer::create_langfuse_observer() { diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index e70471c8909f..dd5303779b7f 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -67,6 +67,7 @@ tracing = "0.1" tracing-subscriber = "0.3" 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 = ["grpc-tonic", "http-proto", "reqwest-client"] } tonic = "0.12" diff --git a/crates/goose/src/tracing/otlp_layer.rs b/crates/goose/src/tracing/otlp_layer.rs index 09d88956b648..958e188f31a1 100644 --- a/crates/goose/src/tracing/otlp_layer.rs +++ b/crates/goose/src/tracing/otlp_layer.rs @@ -1,6 +1,8 @@ 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; @@ -11,7 +13,8 @@ use tracing_subscriber::filter::FilterFn; pub type OtlpTracingLayer = OpenTelemetryLayer; pub type OtlpMetricsLayer = MetricsLayer; -pub type OtlpLayers = (OtlpTracingLayer, OtlpMetricsLayer); +pub type OtlpLogsLayer = OpenTelemetryTracingBridge; +pub type OtlpLayers = (OtlpTracingLayer, OtlpMetricsLayer, OtlpLogsLayer); pub type OtlpResult = Result>; #[derive(Debug, Clone)] @@ -163,10 +166,34 @@ pub fn create_otlp_metrics_layer() -> OtlpResult { 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()?; - Ok((tracing_layer, metrics_layer)) + let logs_layer = create_otlp_logs_layer()?; + Ok((tracing_layer, metrics_layer, logs_layer)) } pub fn init_otlp_tracing_only() -> OtlpResult { @@ -221,6 +248,18 @@ pub fn create_otlp_metrics_filter() -> FilterFn) -> bool> }) } +/// 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