Mnovich/otlp v2 (#3772)

This commit is contained in:
Max Novich
2025-08-01 12:15:12 -07:00
committed by GitHub
parent 1a8b3b1a42
commit 3b6a92b715
8 changed files with 602 additions and 35 deletions
+27
View File
@@ -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(());
+16 -2
View File
@@ -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());
}
+11
View File
@@ -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
+26 -15
View File
@@ -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(())
}
+5
View File
@@ -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"
+5
View File
@@ -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,
};
+267
View File
@@ -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<tracing_subscriber::Registry, opentelemetry_sdk::trace::Tracer>;
pub type OtlpMetricsLayer = MetricsLayer<tracing_subscriber::Registry>;
pub type OtlpLayers = (OtlpTracingLayer, OtlpMetricsLayer);
pub type OtlpResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
#[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<Self> {
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::<u64>() {
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<OtlpTracingLayer> {
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<OtlpMetricsLayer> {
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<OtlpLayers> {
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<OtlpTracingLayer> {
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<impl Fn(&Metadata<'_>) -> 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<impl Fn(&Metadata<'_>) -> 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"),
}
}
}