From ce9d87318b49c2eb4a2c3f3f708a4727cae9593d Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sun, 15 Feb 2026 10:14:21 -0500 Subject: [PATCH] fix(otel): use monotonic_counter prefix and support temporality env var Signed-off-by: Adrian Cole --- crates/goose-cli/src/cli.rs | 14 +++++------ crates/goose-cli/src/session/mod.rs | 4 +-- crates/goose-server/src/routes/reply.rs | 16 ++++++------ crates/goose-server/src/routes/schedule.rs | 2 +- crates/goose/src/agents/tool_execution.rs | 2 +- crates/goose/src/otel/otlp.rs | 29 +++++++++++++++++++++- crates/goose/src/security/mod.rs | 10 ++++---- 7 files changed, 52 insertions(+), 25 deletions(-) diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 3b1afd547a68..a6cda3cc6a0d 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -1059,7 +1059,7 @@ async fn handle_interactive_session( }; tracing::info!( - counter.goose.session_starts = 1, + monotonic_counter.goose.session_starts = 1, session_type, interactive = true, "Session started" @@ -1136,7 +1136,7 @@ async fn log_session_completion( .unwrap_or((0, 0)); tracing::info!( - counter.goose.session_completions = 1, + monotonic_counter.goose.session_completions = 1, session_type, exit_type, duration_ms = session_duration.as_millis() as u64, @@ -1146,14 +1146,14 @@ async fn log_session_completion( ); tracing::info!( - counter.goose.session_duration_ms = session_duration.as_millis() as u64, + monotonic_counter.goose.session_duration_ms = session_duration.as_millis() as u64, session_type, "Session duration" ); if total_tokens > 0 { tracing::info!( - counter.goose.session_tokens = total_tokens, + monotonic_counter.goose.session_tokens = total_tokens, session_type, "Session tokens" ); @@ -1236,7 +1236,7 @@ fn parse_run_input( } tracing::info!( - counter.goose.recipe_runs = 1, + monotonic_counter.goose.recipe_runs = 1, recipe_name = %recipe_display_name, recipe_version = %recipe_version, session_type = "recipe", @@ -1323,7 +1323,7 @@ async fn handle_run_command( let session_type = if recipe.is_some() { "recipe" } else { "run" }; tracing::info!( - counter.goose.session_starts = 1, + monotonic_counter.goose.session_starts = 1, session_type, interactive = false, "Headless session started" @@ -1437,7 +1437,7 @@ pub async fn cli() -> anyhow::Result<()> { let command_name = get_command_name(&cli.command); tracing::info!( - counter.goose.cli_commands = 1, + monotonic_counter.goose.cli_commands = 1, command = command_name, "CLI command executed" ); diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 1757d85aeb06..8b832d346c28 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -1740,7 +1740,7 @@ fn log_tool_metrics(message: &Message, messages: &Conversation) { if let MessageContent::ToolRequest(tool_request) = content { if let Ok(tool_call) = &tool_request.tool_call { tracing::info!( - counter.goose.tool_calls = 1, + monotonic_counter.goose.tool_calls = 1, tool_name = %tool_call.name, "Tool call started" ); @@ -1771,7 +1771,7 @@ fn log_tool_metrics(message: &Message, messages: &Conversation) { "error" }; tracing::info!( - counter.goose.tool_completions = 1, + monotonic_counter.goose.tool_completions = 1, tool_name = %tool_name, result = %result_status, "Tool call completed" diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index 897702a719a3..df33fd2ea4cd 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -66,7 +66,7 @@ fn track_tool_telemetry(content: &MessageContent, all_messages: &[Message]) { let result_status = if success { "success" } else { "error" }; tracing::info!( - counter.goose.tool_completions = 1, + monotonic_counter.goose.tool_completions = 1, tool_name = %tool_name, result = %result_status, "Tool call completed" @@ -209,7 +209,7 @@ pub async fn reply( let session_start = std::time::Instant::now(); tracing::info!( - counter.goose.session_starts = 1, + monotonic_counter.goose.session_starts = 1, session_type = "app", interface = "ui", "Session started" @@ -225,7 +225,7 @@ pub async fn reply( .unwrap_or_else(|| "unknown".to_string()); tracing::info!( - counter.goose.recipe_runs = 1, + monotonic_counter.goose.recipe_runs = 1, recipe_name = %recipe_name, recipe_version = %recipe_version, session_type = "app", @@ -396,7 +396,7 @@ pub async fn reply( if let Ok(session) = state.session_manager().get_session(&session_id, true).await { let total_tokens = session.total_tokens.unwrap_or(0); tracing::info!( - counter.goose.session_completions = 1, + monotonic_counter.goose.session_completions = 1, session_type = "app", interface = "ui", exit_type = "normal", @@ -407,7 +407,7 @@ pub async fn reply( ); tracing::info!( - counter.goose.session_duration_ms = session_duration.as_millis() as u64, + monotonic_counter.goose.session_duration_ms = session_duration.as_millis() as u64, session_type = "app", interface = "ui", "Session duration" @@ -415,7 +415,7 @@ pub async fn reply( if total_tokens > 0 { tracing::info!( - counter.goose.session_tokens = total_tokens, + monotonic_counter.goose.session_tokens = total_tokens, session_type = "app", interface = "ui", "Session tokens" @@ -423,7 +423,7 @@ pub async fn reply( } } else { tracing::info!( - counter.goose.session_completions = 1, + monotonic_counter.goose.session_completions = 1, session_type = "app", interface = "ui", exit_type = "normal", @@ -434,7 +434,7 @@ pub async fn reply( ); tracing::info!( - counter.goose.session_duration_ms = session_duration.as_millis() as u64, + monotonic_counter.goose.session_duration_ms = session_duration.as_millis() as u64, session_type = "app", interface = "ui", "Session duration" diff --git a/crates/goose-server/src/routes/schedule.rs b/crates/goose-server/src/routes/schedule.rs index 4eeae27ed5b1..e7ca2ebf9005 100644 --- a/crates/goose-server/src/routes/schedule.rs +++ b/crates/goose-server/src/routes/schedule.rs @@ -274,7 +274,7 @@ async fn run_now_handler( let recipe_version_tag = recipe_version_opt.as_deref().unwrap_or(""); tracing::info!( - counter.goose.recipe_runs = 1, + monotonic_counter.goose.recipe_runs = 1, recipe_name = %recipe_display_name, recipe_version = %recipe_version_tag, session_type = "schedule", diff --git a/crates/goose/src/agents/tool_execution.rs b/crates/goose/src/agents/tool_execution.rs index 5ffd6eb72bfb..b8813900b8a4 100644 --- a/crates/goose/src/agents/tool_execution.rs +++ b/crates/goose/src/agents/tool_execution.rs @@ -88,7 +88,7 @@ impl Agent { // Log user decision if this was a security alert if let Some(finding_id) = get_security_finding_id_from_results(&request.id, inspection_results) { tracing::info!( - counter.goose.prompt_injection_user_decisions = 1, + monotonic_counter.goose.prompt_injection_user_decisions = 1, decision = ?confirmation.permission, finding_id = %finding_id, tool_request_id = %request.id, diff --git a/crates/goose/src/otel/otlp.rs b/crates/goose/src/otel/otlp.rs index dc1cb8d29a16..f7f76063058b 100644 --- a/crates/goose/src/otel/otlp.rs +++ b/crates/goose/src/otel/otlp.rs @@ -2,7 +2,7 @@ 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::metrics::{SdkMeterProvider, Temporality}; use opentelemetry_sdk::propagation::TraceContextPropagator; use opentelemetry_sdk::resource::{EnvResourceDetector, TelemetryResourceDetector}; use opentelemetry_sdk::trace::SdkTracerProvider; @@ -173,6 +173,19 @@ fn create_otlp_tracing_layer() -> OtlpResult { Ok(tracing_opentelemetry::layer().with_tracer(tracer)) } +// TODO: remove once https://github.com/open-telemetry/opentelemetry-rust/pull/3351 is released. +fn temporality_preference() -> Temporality { + match env::var("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE") + .unwrap_or_default() + .to_lowercase() + .as_str() + { + "delta" => Temporality::Delta, + "lowmemory" => Temporality::LowMemory, + _ => Temporality::Cumulative, + } +} + fn create_otlp_metrics_layer() -> OtlpResult { let exporter = signal_exporter("metrics").ok_or("Metrics not enabled")?; let resource = create_resource(); @@ -181,6 +194,7 @@ fn create_otlp_metrics_layer() -> OtlpResult { ExporterType::Otlp => { let exporter = opentelemetry_otlp::MetricExporter::builder() .with_http() + .with_temporality(temporality_preference()) .build()?; SdkMeterProvider::builder() .with_resource(resource) @@ -332,6 +346,7 @@ mod tests { use super::*; use opentelemetry::metrics::{Meter, MeterProvider}; use opentelemetry::InstrumentationScope; + use opentelemetry_sdk::metrics::Temporality; use std::sync::Arc; use test_case::test_case; @@ -371,6 +386,7 @@ mod tests { ("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", None), ("OTEL_EXPORTER_OTLP_TIMEOUT", None), ("OTEL_SERVICE_NAME", None), + ("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", None), ("OTEL_RESOURCE_ATTRIBUTES", None), ]); for &(k, v) in overrides { @@ -548,4 +564,15 @@ mod tests { expect_timeout ); } + + #[test_case(&[], Temporality::Cumulative; "default is cumulative")] + #[test_case(&[("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", "delta")], Temporality::Delta; "delta")] + #[test_case(&[("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", "Delta")], Temporality::Delta; "Delta mixed case")] + #[test_case(&[("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", "lowmemory")], Temporality::LowMemory; "lowmemory")] + #[test_case(&[("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", "cumulative")], Temporality::Cumulative; "cumulative")] + #[test_case(&[("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", "bogus")], Temporality::Cumulative; "unknown defaults to cumulative")] + fn temporality_preference_from_env(env: &[(&str, &str)], expected: Temporality) { + let _guard = clear_otel_env(env); + assert_eq!(temporality_preference(), expected); + } } diff --git a/crates/goose/src/security/mod.rs b/crates/goose/src/security/mod.rs index 88b5d21487f1..b1e3cbb98453 100644 --- a/crates/goose/src/security/mod.rs +++ b/crates/goose/src/security/mod.rs @@ -61,7 +61,7 @@ impl SecurityManager { ) -> Result> { if !self.is_prompt_injection_detection_enabled() { tracing::debug!( - counter.goose.prompt_injection_scanner_disabled = 1, + monotonic_counter.goose.prompt_injection_scanner_disabled = 1, "Security scanning disabled" ); return Ok(vec![]); @@ -74,7 +74,7 @@ impl SecurityManager { match PromptInjectionScanner::with_ml_detection() { Ok(s) => { tracing::info!( - counter.goose.prompt_injection_scanner_enabled = 1, + monotonic_counter.goose.prompt_injection_scanner_enabled = 1, "Security scanner initialized with ML-based detection" ); s @@ -90,7 +90,7 @@ impl SecurityManager { } } else { tracing::info!( - counter.goose.prompt_injection_scanner_enabled = 1, + monotonic_counter.goose.prompt_injection_scanner_enabled = 1, "Security scanner initialized with pattern-based detection only" ); PromptInjectionScanner::new() @@ -124,7 +124,7 @@ impl SecurityManager { serde_json::to_string(&tool_call).unwrap_or_else(|_| "{}".to_string()); tracing::warn!( - counter.goose.prompt_injection_finding = 1, + monotonic_counter.goose.prompt_injection_finding = 1, threat_type = "command_injection", above_threshold = above_threshold, tool_name = %tool_call.name, @@ -164,7 +164,7 @@ impl SecurityManager { } tracing::info!( - counter.goose.prompt_injection_analysis_performed = 1, + monotonic_counter.goose.prompt_injection_analysis_performed = 1, security_issues_found = results.len(), "Prompt injection detection: Security analysis complete" );