Files
tkmind_go/crates/goose-cli/src/logging.rs
T
Rodolfo Olivieri adc0e221fc feat: gate telemetry behind Cargo feature flags (#8119)
Signed-off-by: Rodolfo Olivieri <rolivier@redhat.com>
Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
2026-03-26 16:33:43 +00:00

203 lines
7.1 KiB
Rust

use anyhow::{Context, Result};
use std::sync::Once;
use tracing_appender::rolling::Rotation;
use tracing_subscriber::{
filter::LevelFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
Registry,
};
#[cfg(feature = "otel")]
use goose::otel::otlp;
use goose::tracing::langfuse_layer;
// Used to ensure we only set up tracing once
static INIT: Once = Once::new();
fn default_env_filter() -> EnvFilter {
EnvFilter::new("")
// Keep goose and MCP logs visible without verbose debug payloads.
.add_directive("mcp_client=info".parse().unwrap())
.add_directive("goose=info".parse().unwrap())
.add_directive("goose_cli=info".parse().unwrap())
.add_directive(LevelFilter::WARN.into())
}
/// Sets up the logging infrastructure for the application.
/// This includes:
/// - File-based logging with JSON formatting (DEBUG level)
/// - No console output (all logs go to files only)
/// - Optional Langfuse integration (DEBUG level)
pub fn setup_logging(name: Option<&str>) -> Result<()> {
setup_logging_internal(name, false)
}
/// Internal function that allows bypassing the Once check for testing
fn setup_logging_internal(name: Option<&str>, force: bool) -> Result<()> {
let mut result = Ok(());
let mut setup = || {
result = (|| {
let log_dir = goose::logging::prepare_log_directory("cli", true)?;
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string();
let log_filename = if let Some(n) = name {
format!("{}-{}.log", timestamp, n)
} else {
format!("{}.log", timestamp)
};
let file_appender = tracing_appender::rolling::RollingFileAppender::new(
Rotation::NEVER, // we do manual rotation via file naming and cleanup_old_logs
log_dir,
log_filename,
);
// Create JSON file logging layer with all logs (DEBUG and above)
let file_layer = fmt::layer()
.with_target(true)
.with_level(true)
.with_writer(file_appender)
.with_ansi(false)
.json();
// Base filter
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| default_env_filter());
// Start building the subscriber
let mut layers = vec![
file_layer.with_filter(env_filter).boxed(),
// Console logging disabled for CLI - all logs go to files only
];
#[cfg(feature = "otel")]
if !force {
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());
}
// Build the subscriber
let subscriber = Registry::default().with(layers);
if force {
// For testing, just create and use the subscriber without setting it globally
// Write a test log to ensure the file is created
let _guard = subscriber.set_default();
tracing::warn!("Test log entry from setup");
tracing::info!("Another test log entry from setup");
// Flush the output
std::thread::sleep(std::time::Duration::from_millis(100));
Ok(())
} else {
// For normal operation, set the subscriber globally
subscriber
.try_init()
.context("Failed to set global subscriber")?;
Ok(())
}
})();
};
if force {
setup();
} else {
INIT.call_once(setup);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use tempfile::TempDir;
fn setup_temp_home() -> TempDir {
let temp_dir = TempDir::new().unwrap();
if cfg!(windows) {
env::set_var("USERPROFILE", temp_dir.path());
} else {
env::set_var("HOME", temp_dir.path());
}
temp_dir
}
#[test]
fn test_log_directory_creation() {
let _temp_dir = setup_temp_home();
let log_dir = goose::logging::prepare_log_directory("cli", true).unwrap();
assert!(log_dir.exists());
assert!(log_dir.is_dir());
// Verify directory structure
let path_components: Vec<_> = log_dir.components().collect();
assert!(path_components.iter().any(|c| c.as_os_str() == "goose"));
assert!(path_components.iter().any(|c| c.as_os_str() == "logs"));
assert!(path_components.iter().any(|c| c.as_os_str() == "cli"));
}
#[tokio::test]
async fn test_langfuse_layer_creation() {
let _temp_dir = setup_temp_home();
// Store original environment variables (both sets)
let original_vars = [
("LANGFUSE_PUBLIC_KEY", env::var("LANGFUSE_PUBLIC_KEY").ok()),
("LANGFUSE_SECRET_KEY", env::var("LANGFUSE_SECRET_KEY").ok()),
("LANGFUSE_URL", env::var("LANGFUSE_URL").ok()),
(
"LANGFUSE_INIT_PROJECT_PUBLIC_KEY",
env::var("LANGFUSE_INIT_PROJECT_PUBLIC_KEY").ok(),
),
(
"LANGFUSE_INIT_PROJECT_SECRET_KEY",
env::var("LANGFUSE_INIT_PROJECT_SECRET_KEY").ok(),
),
];
// Clear all Langfuse environment variables
for (var, _) in &original_vars {
env::remove_var(var);
}
// Test without any environment variables
assert!(langfuse_layer::create_langfuse_observer().is_none());
// Test with standard Langfuse variables
env::set_var("LANGFUSE_PUBLIC_KEY", "test_public_key");
env::set_var("LANGFUSE_SECRET_KEY", "test_secret_key");
assert!(langfuse_layer::create_langfuse_observer().is_some());
// Clear and test with init project variables
env::remove_var("LANGFUSE_PUBLIC_KEY");
env::remove_var("LANGFUSE_SECRET_KEY");
env::set_var("LANGFUSE_INIT_PROJECT_PUBLIC_KEY", "test_public_key");
env::set_var("LANGFUSE_INIT_PROJECT_SECRET_KEY", "test_secret_key");
assert!(langfuse_layer::create_langfuse_observer().is_some());
// Test fallback behavior
env::remove_var("LANGFUSE_INIT_PROJECT_PUBLIC_KEY");
assert!(langfuse_layer::create_langfuse_observer().is_none());
// Restore original environment variables
for (var, value) in original_vars {
match value {
Some(val) => env::set_var(var, val),
None => env::remove_var(var),
}
}
}
#[test]
fn test_default_filter_avoids_debug_by_default() {
let filter = super::default_env_filter().to_string();
assert!(!filter.contains("mcp_client=debug"));
assert!(!filter.contains("goose=debug"));
assert!(filter.contains("mcp_client=info"));
assert!(filter.contains("goose=info"));
assert!(filter.contains("goose_cli=info"));
}
}