diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 74d1cd02c..15d722fdd 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -39,6 +39,8 @@ rust_decimal_macros = "1.36.0" tracing = "0.1" chrono = "0.4" parking_lot = "0.12.3" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json", "time"] } +tracing-appender = "0.2" [dev-dependencies] tempfile = "3" diff --git a/crates/goose-cli/src/logging.rs b/crates/goose-cli/src/logging.rs new file mode 100644 index 000000000..6d5fe6d9d --- /dev/null +++ b/crates/goose-cli/src/logging.rs @@ -0,0 +1,102 @@ +use anyhow::{Context, Result}; +use std::fs; +use std::path::PathBuf; +use tracing_appender::rolling::Rotation; +use tracing_subscriber::{ + fmt, + Layer, + layer::SubscriberExt, + util::SubscriberInitExt, + EnvFilter, + Registry, + filter::LevelFilter, +}; + +use goose::tracing::langfuse_layer; + +/// Returns the directory where log files should be stored. +/// Creates the directory structure if it doesn't exist. +fn get_log_directory() -> Result { + let home = std::env::var("HOME").context("HOME environment variable not set")?; + let base_log_dir = PathBuf::from(home) + .join(".config") + .join("goose") + .join("logs") + .join("cli"); // Add cli-specific subdirectory + + // Create date-based subdirectory + let now = chrono::Local::now(); + let date_dir = base_log_dir.join(now.format("%Y-%m-%d").to_string()); + + // Ensure log directory exists + fs::create_dir_all(&date_dir).context("Failed to create log directory")?; + + Ok(date_dir) +} + +/// Sets up the logging infrastructure for the application. +/// This includes: +/// - File-based logging with JSON formatting (DEBUG level) +/// - Console output for development (INFO level) +/// - Optional Langfuse integration (DEBUG level) +pub fn setup_logging() -> Result<()> { + // Set up file appender for goose module logs + let log_dir = get_log_directory()?; + let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string(); + + // Create non-rolling file appender for detailed logs + let file_appender = tracing_appender::rolling::RollingFileAppender::new( + Rotation::NEVER, + log_dir, + &format!("goose_{}.log", timestamp), + ); + + // 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) + .with_file(true) + .pretty(); + + // Create console logging layer for development - INFO and above only + let console_layer = fmt::layer() + .with_target(true) + .with_level(true) + .with_ansi(true) + .with_file(true) + .with_line_number(true) + .pretty(); + + // Base filter + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { + // Set default levels for different modules + EnvFilter::new("") + // Set goose module to INFO only + .add_directive("goose=debug".parse().unwrap()) + // Set goose-cli to INFO + .add_directive("goose_cli=info".parse().unwrap()) + // Set everything else to WARN + .add_directive(LevelFilter::WARN.into()) + }); + + // Build the subscriber with required layers + let subscriber = Registry::default() + .with(file_layer.with_filter(env_filter)) // Gets all logs + .with(console_layer.with_filter(LevelFilter::INFO)); // Controls log levels + + // 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")?; + } + + Ok(()) +} \ No newline at end of file diff --git a/crates/goose-cli/src/main.rs b/crates/goose-cli/src/main.rs index 42ffa8617..921a0f123 100644 --- a/crates/goose-cli/src/main.rs +++ b/crates/goose-cli/src/main.rs @@ -7,6 +7,7 @@ pub mod agents; mod profile; mod prompt; pub mod session; +mod logging; mod systems; @@ -17,7 +18,7 @@ use commands::session::build_session; use commands::version::print_version; use profile::has_no_profiles; use std::io::{self, Read}; -use goose::logging::setup_logging; +use logging::setup_logging; mod log_usage; @@ -196,7 +197,7 @@ enum CliProviderVariant { #[tokio::main] async fn main() -> Result<()> { - setup_logging()?; + setup_logging(); let cli = Cli::parse(); diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index c410f8e2e..8b00e35dd 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -18,7 +18,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" futures = "0.3" tracing = "0.1" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json", "time"] } +tracing-appender = "0.2" tokio-stream = "0.1" anyhow = "1.0" bytes = "1.5" @@ -34,4 +35,4 @@ path = "src/main.rs" [dev-dependencies] serial_test = "3.2.0" tower = "0.5" -async-trait = "0.1" +async-trait = "0.1" \ No newline at end of file diff --git a/crates/goose-server/src/logging.rs b/crates/goose-server/src/logging.rs new file mode 100644 index 000000000..724c7c414 --- /dev/null +++ b/crates/goose-server/src/logging.rs @@ -0,0 +1,103 @@ +use anyhow::{Context, Result}; +use std::fs; +use std::path::PathBuf; +use tracing_appender::rolling::Rotation; +use tracing_subscriber::{ + fmt, + Layer, + layer::SubscriberExt, + util::SubscriberInitExt, + EnvFilter, + Registry, + filter::LevelFilter, +}; + +use goose::tracing::langfuse_layer; + +/// Returns the directory where log files should be stored. +/// Creates the directory structure if it doesn't exist. +fn get_log_directory() -> Result { + let home = std::env::var("HOME").context("HOME environment variable not set")?; + let base_log_dir = PathBuf::from(home) + .join(".config") + .join("goose") + .join("logs") + .join("server"); // Add server-specific subdirectory + + // Create date-based subdirectory + let now = chrono::Local::now(); + let date_dir = base_log_dir.join(now.format("%Y-%m-%d").to_string()); + + // Ensure log directory exists + fs::create_dir_all(&date_dir).context("Failed to create log directory")?; + + Ok(date_dir) +} + +/// Sets up the logging infrastructure for the application. +/// This includes: +/// - File-based logging with JSON formatting (DEBUG level) +/// - Console output for development (INFO level) +/// - Optional Langfuse integration (DEBUG level) +pub fn setup_logging() -> Result<()> { + // Set up file appender for goose module logs + let log_dir = get_log_directory()?; + let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string(); + + // Create non-rolling file appender for detailed logs + let file_appender = tracing_appender::rolling::RollingFileAppender::new( + Rotation::NEVER, + log_dir, + &format!("goosed_{}.log", timestamp), + ); + + // Create JSON file logging layer + let file_layer = fmt::layer() + .with_target(true) + .with_level(true) + .with_writer(file_appender) + .with_ansi(false) + .with_file(true); + + // Create console logging layer for development - INFO and above only + let console_layer = fmt::layer() + .with_target(true) + .with_level(true) + .with_ansi(true) + .with_file(true) + .with_line_number(true) + .pretty(); + + // Base filter for all logging + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { + // Set default levels for different modules + EnvFilter::new("") + // Set goose module to INFO only + .add_directive("goose=info".parse().unwrap()) + // Set goose-server to INFO + .add_directive("goose_server=info".parse().unwrap()) + // Set tower-http to INFO for request logging + .add_directive("tower_http=info".parse().unwrap()) + // Set everything else to WARN + .add_directive(LevelFilter::WARN.into()) + }); + + // Build the subscriber with required layers + let subscriber = Registry::default() + .with(file_layer) + .with(console_layer.with_filter(env_filter)); + + // 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")?; + } + + Ok(()) +} \ No newline at end of file diff --git a/crates/goose-server/src/main.rs b/crates/goose-server/src/main.rs index d575596a7..703d7ea92 100644 --- a/crates/goose-server/src/main.rs +++ b/crates/goose-server/src/main.rs @@ -1,5 +1,6 @@ mod configuration; mod error; +mod logging; mod routes; mod state; @@ -9,7 +10,7 @@ use tracing::info; #[tokio::main] async fn main() -> anyhow::Result<()> { // Initialize tracing for logging - tracing_subscriber::fmt::init(); + logging::setup_logging()?; // Load configuration let settings = configuration::Settings::new()?; @@ -34,4 +35,4 @@ async fn main() -> anyhow::Result<()> { info!("listening on {}", listener.local_addr()?); axum::serve(listener, app).await?; Ok(()) -} +} \ No newline at end of file diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index b901bebad..a33727a07 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -48,8 +48,8 @@ libc = "=0.2.167" lazy_static = "1.5" kill_tree = "0.2.4" tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json", "time"] } -tracing-appender = "0.2" +tracing-subscriber = "0.3" + keyring = { version = "3.6.1", features = [ "apple-native", diff --git a/crates/goose/src/agent.rs b/crates/goose/src/agent.rs index 327f7f74e..860640804 100644 --- a/crates/goose/src/agent.rs +++ b/crates/goose/src/agent.rs @@ -127,8 +127,8 @@ impl Agent { let system_tool_call = ToolCall::new(tool_name, call.arguments); let result = system.call(system_tool_call.clone()).await; - tracing::debug!("input"=serde_json::to_string(&system_tool_call).unwrap(), - "output"=serde_json::to_string(&result).unwrap(), + debug!("input"=serde_json::to_string(&system_tool_call).unwrap(), + "output"=serde_json::to_string(&result).unwrap(), ); result @@ -350,7 +350,7 @@ impl Agent { .and_then(|msg| msg.content.first()) .and_then(|c| c.as_text()) { - reply_span.record("user_message", &content); + debug!("user_message"=&content); } let system_prompt = self.get_system_prompt()?; diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index 46b5a95af..811fbcbf5 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -10,4 +10,3 @@ pub mod providers; pub mod systems; pub mod token_counter; pub mod tracing; -pub mod logging; \ No newline at end of file diff --git a/crates/goose/src/logging.rs b/crates/goose/src/logging.rs deleted file mode 100644 index 3c6189a90..000000000 --- a/crates/goose/src/logging.rs +++ /dev/null @@ -1,100 +0,0 @@ -use anyhow::{Context, Result}; -use serde_json::Value; -use std::fs; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::Mutex; -use tracing_appender::rolling::Rotation; -use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter, Registry}; -use tracing::dispatcher::set_global_default; - -use crate::tracing::{langfuse_layer, observation_layer::{BatchManager, ObservationLayer, SpanTracker}}; - -struct ConsoleLogger { - batch: Vec, -} - -impl ConsoleLogger { - fn new() -> Self { - Self { - batch: Vec::new(), - } - } -} - -impl BatchManager for ConsoleLogger { - fn add_event(&mut self, _event_type: &str, body: Value) { - self.batch.push(body); - } - - fn send(&mut self) -> Result<(), Box> { - self.batch.clear(); - Ok(()) - } -} - -fn get_log_directory() -> Result { - let home = std::env::var("HOME").context("HOME environment variable not set")?; - let base_log_dir = PathBuf::from(home).join(".config").join("goose").join("logs"); - - // Create date-based subdirectory - let now = chrono::Local::now(); - let date_dir = base_log_dir.join(now.format("%Y-%m-%d").to_string()); - - // Ensure log directory exists - fs::create_dir_all(&date_dir).context("Failed to create log directory")?; - - Ok(date_dir) -} - -fn create_observation_layer() -> ObservationLayer { - let batch_manager = Arc::new(Mutex::new(ConsoleLogger::new())); - ObservationLayer { - batch_manager, - span_tracker: Arc::new(Mutex::new(SpanTracker::new())), - } -} - -pub fn setup_logging() -> Result<()> { - // Set up file appender for goose module logs - let log_dir = get_log_directory()?; - let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S%.3f").to_string(); - - // Create non-rolling file appender - let file_appender = tracing_appender::rolling::RollingFileAppender::new( - Rotation::NEVER, - log_dir, - &format!("{}.log", timestamp), - ); - - // Create JSON file logging layer - let file_layer = fmt::layer() - .with_target(true) - .with_level(true) - .with_writer(file_appender) - .with_ansi(false) - .with_file(true); - - // Update filter to include debug level - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("goose=debug")); - - // Build the base subscriber - let subscriber = Registry::default() - .with(file_layer) - .with(filter) - .with(create_observation_layer()); - - // Set up the dispatcher - let dispatcher = if let Some(langfuse) = langfuse_layer::create_langfuse_observer() { - subscriber.with(langfuse).into() - } else { - subscriber.into() - }; - - // Set the subscriber as the default - set_global_default(dispatcher) - .map_err(|e| anyhow::anyhow!("Failed to set global subscriber: {}", e))?; - - Ok(()) -} diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index 862684051..c2d896885 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -14,6 +14,8 @@ use super::utils::{ }; use crate::message::Message; use mcp_core::tool::Tool; +use tracing::{debug}; + pub struct DatabricksProvider { client: Client, @@ -165,7 +167,7 @@ impl Provider for DatabricksProvider { let usage = Self::get_usage(&response)?; let model = get_model(&response); let cost = cost(&usage, &model_pricing_for(&model)); - tracing::debug!( + debug!( model_config = %serde_json::to_string_pretty(&self.config).unwrap_or_default(), input = %serde_json::to_string_pretty(&payload).unwrap_or_default(), output = %serde_json::to_string_pretty(&response).unwrap_or_default(),