Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 20 additions & 42 deletions crates/goose-cli/src/logging.rs
Original file line number Diff line number Diff line change
@@ -1,53 +1,31 @@
use anyhow::Result;
use std::sync::Once;
use goose::providers::utils::init_goose_request_log;
use std::sync::OnceLock;

// Used to ensure we only set up tracing once
static INIT: Once = Once::new();
static INIT: OnceLock<Result<()>> = OnceLock::new();

/// Sets up the logging infrastructure for the CLI.
/// Logs go to a JSON file only (no console output).
pub fn setup_logging(name: Option<&str>) -> Result<()> {
setup_logging_internal(name, false)
}

fn setup_logging_internal(name: Option<&str>, force: bool) -> Result<()> {
let mut result = Ok(());

let mut setup = || {
result = (|| {
use tracing_subscriber::util::SubscriberInitExt;

let config = goose::logging::LoggingConfig {
component: "cli",
name,
extra_directives: &["goose_cli=info"],
console: false,
json: true,
};
let subscriber = goose::logging::build_logging_subscriber(&config)?;

if force {
let _guard = subscriber.set_default();
tracing::warn!("Test log entry from setup");
tracing::info!("Another test log entry from setup");
std::thread::sleep(std::time::Duration::from_millis(100));
Ok(())
} else {
subscriber
.try_init()
.map_err(|e| anyhow::anyhow!("Failed to set global subscriber: {}", e))?;
Ok(())
}
})();
};
pub fn setup_logging(name: Option<&str>) -> &'static Result<()> {
INIT.get_or_init(|| {
use tracing_subscriber::util::SubscriberInitExt;

if force {
setup();
} else {
INIT.call_once(setup);
}
init_goose_request_log()?;
let config = goose::logging::LoggingConfig {
component: "cli",
name,
extra_directives: &["goose_cli=info"],
console: false,
json: true,
};
let subscriber = goose::logging::build_logging_subscriber(&config)?;

result
subscriber
.try_init()
.map_err(|e| anyhow::anyhow!("Failed to set global subscriber: {}", e))?;
Ok(())
})
}

#[cfg(test)]
Expand Down
5 changes: 5 additions & 0 deletions crates/goose-providers/src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ pub async fn collect_stream(
}
}

pub fn stream_from_single_message(message: Message, usage: ProviderUsage) -> MessageStream {
let stream = futures::stream::once(async move { Ok((Some(message), Some(usage))) });
Box::pin(stream)
}

/// Base trait for AI providers (OpenAI, Anthropic, etc)
#[async_trait]
pub trait Provider: Send + Sync {
Expand Down
8 changes: 8 additions & 0 deletions crates/goose-providers/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use reqwest::StatusCode;
use std::time::Duration;
use thiserror::Error;

use crate::request_log::LogError;

#[derive(Error, Debug, Clone, PartialEq)]
pub enum ProviderError {
#[error("Authentication error: {0}")]
Expand Down Expand Up @@ -142,6 +144,12 @@ impl From<reqwest::Error> for ProviderError {
}
}

impl From<LogError> for ProviderError {
fn from(value: LogError) -> Self {
ProviderError::ExecutionError(value.to_string())
}
}

#[derive(Debug)]
pub enum GoogleErrorCode {
BadRequest = 400,
Expand Down
1 change: 1 addition & 0 deletions crates/goose-providers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod json;
pub(crate) mod mcp_utils;
pub mod model;
pub mod permission;
pub mod request_log;
pub mod retry;
pub mod thinking;
pub mod utils;
134 changes: 134 additions & 0 deletions crates/goose-providers/src/request_log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use std::{
error::Error,
fmt::Display,
sync::{Arc, OnceLock},
};

use serde::Serialize;
use serde_json::json;

use crate::conversation::token_usage::Usage;

type RequestLogError = Box<dyn Error + Send + Sync>;

static LOGGER: OnceLock<Arc<dyn RequestLogger>> = OnceLock::new();

#[derive(Debug)]
pub struct LoggerAlreadyInstalled;

impl Display for LoggerAlreadyInstalled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "request logger is already installed")
}
}

impl Error for LoggerAlreadyInstalled {}

pub fn install_logger<R: RequestLogger + 'static>(r: R) -> Result<(), LoggerAlreadyInstalled> {
LOGGER.set(Arc::new(r)).map_err(|_| LoggerAlreadyInstalled)
}

pub trait RequestLogger: Send + Sync {
fn start(&self) -> Result<Box<dyn RequestLogHandle>, RequestLogError>;
}

pub trait RequestLogHandle: Send {
fn write(&mut self, s: &str) -> Result<(), RequestLogError>;
}

#[derive(Debug)]
pub enum LogError {
LoggerError(String),
SerializeError(serde_json::Error),
}

impl Display for LogError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogError::LoggerError(msg) => write!(f, "{}", msg),
LogError::SerializeError(error) => write!(f, "serialize error: {}", error),
}
}
}

impl Error for LogError {}

impl From<RequestLogError> for LogError {
fn from(value: RequestLogError) -> Self {
Self::LoggerError(value.to_string())
}
}

fn serialize(v: &serde_json::Value) -> Result<String, LogError> {
serde_json::to_string(v).map_err(LogError::SerializeError)
}

pub fn start_log<M, P>(
model_config: M,
payload: P,
) -> Result<Option<Box<dyn RequestLogHandle>>, LogError>
where
M: Serialize,
P: Serialize,
{
let logger = if let Some(logger) = LOGGER.get() {
logger
} else {
return Ok(None);
};

let mut handle = logger.start()?;
let payload = json!({
"model_config": model_config,
"input": payload,
});

handle.write(serialize(&payload)?.as_str())?;
Ok(Some(handle))
}

pub trait LoggerHandleExt {
fn write<Payload>(&mut self, data: &Payload, usage: Option<&Usage>) -> Result<(), LogError>
where
Payload: Serialize;
fn error<E>(&mut self, error: E) -> Result<(), LogError>
where
E: Display;
}

impl LoggerHandleExt for Option<Box<dyn RequestLogHandle>> {
fn write<Payload>(&mut self, data: &Payload, usage: Option<&Usage>) -> Result<(), LogError>
where
Payload: Serialize,
{
let log = if let Some(log) = self {
log
} else {
return Ok(());
};

let line = serialize(&json!({
"data": data,
"usage": usage,
}))?;

Ok(log.write(line.as_str())?)
}

fn error<E>(&mut self, error: E) -> Result<(), LogError>
where
E: Display,
{
let log = if let Some(log) = self {
log
} else {
return Ok(());
};

let line = serialize(&json!({
"error": format!("{}", error),
}))?;

Ok(log.write(line.as_str())?)
}
}
2 changes: 2 additions & 0 deletions crates/goose-server/src/logging.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use anyhow::Result;
use goose::providers::utils::init_goose_request_log;
use tracing_subscriber::util::SubscriberInitExt;

/// Sets up the logging infrastructure for the server.
/// Logs go to a JSON file and a pretty console layer on stderr.
pub fn setup_logging(name: Option<&str>) -> Result<()> {
init_goose_request_log()?;
let config = goose::logging::LoggingConfig {
component: "server",
name,
Expand Down
5 changes: 3 additions & 2 deletions crates/goose/src/providers/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use async_stream::try_stream;
use async_trait::async_trait;
use futures::TryStreamExt;
use goose_providers::errors::ProviderError;
use goose_providers::request_log::{start_log, LoggerHandleExt};
use reqwest::StatusCode;
use serde_json::Value;
use std::io;
Expand All @@ -20,7 +21,6 @@ use super::openai_compatible::map_http_error_to_provider_error;
use super::retry::ProviderRetry;
use crate::config::declarative_providers::DeclarativeProviderConfig;
use crate::conversation::message::Message;
use crate::providers::utils::RequestLog;
use futures::future::BoxFuture;
use goose_providers::model::ModelConfig;
use rmcp::model::Tool;
Expand Down Expand Up @@ -330,7 +330,7 @@ impl Provider for AnthropicProvider {
.insert("stream".to_string(), Value::Bool(true));

let conditional_headers = self.get_conditional_headers();
let mut log = RequestLog::start(model_config, &payload)?;
let mut log = start_log(model_config, &payload)?;

let response = self
.with_retry(|| async {
Expand Down Expand Up @@ -362,6 +362,7 @@ impl Provider for AnthropicProvider {
}))
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
6 changes: 0 additions & 6 deletions crates/goose/src/providers/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ pub const DEFAULT_PROVIDER_TIMEOUT_SECS: u64 = 600;

use crate::config::base::ConfigValue;
use crate::config::ExtensionConfig;
use goose_providers::conversation::message::Message;
use goose_providers::model::ModelConfig;
use utoipa::ToSchema;

Expand Down Expand Up @@ -264,11 +263,6 @@ pub trait ProviderDef: Send + Sync {
}
}

pub fn stream_from_single_message(message: Message, usage: ProviderUsage) -> MessageStream {
let stream = futures::stream::once(async move { Ok((Some(message), Some(usage))) });
Box::pin(stream)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
8 changes: 4 additions & 4 deletions crates/goose/src/providers/bedrock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use super::formats::openai_responses::create_responses_request;
use super::openai_compatible::{handle_status, stream_responses_compat};
use super::retry::{ProviderRetry, RetryConfig};
use crate::conversation::message::Message;
use crate::providers::utils::RequestLog;
use crate::session_context::SESSION_ID_HEADER;
use anyhow::Result;
use async_stream::try_stream;
Expand All @@ -21,6 +20,7 @@ use goose_providers::conversation::token_usage::{ProviderUsage, Usage};
use goose_providers::errors::ProviderError;
use goose_providers::formats::openai::extract_reasoning_effort;
use goose_providers::model::ModelConfig;
use goose_providers::request_log::{start_log, LoggerHandleExt};
use reqwest::header::{HeaderName, HeaderValue, AUTHORIZATION};
use rmcp::model::{object, CallToolRequestParams, ErrorCode, ErrorData, Tool};
use serde_json::Value;
Expand Down Expand Up @@ -526,7 +526,7 @@ impl BedrockProvider {
"messages": messages,
"tools": tools
});
let mut log = RequestLog::start(&self.model, &debug_payload)?;
let mut log = start_log(&self.model, &debug_payload)?;
log.write(
&serde_json::to_value(&message).unwrap_or_default(),
Some(&usage),
Expand Down Expand Up @@ -775,7 +775,7 @@ impl Provider for BedrockProvider {
create_responses_request(&normalized_config, system, messages, tools)?;
payload["model"] = Value::String(bedrock_model_id.clone());
payload["stream"] = Value::Bool(true);
let mut log = RequestLog::start(model_config, &payload)?;
let mut log = start_log(model_config, &payload).map_err(anyhow::Error::from)?;

let response = self
.with_retry(|| self.post_mantle_streaming(session_id_opt, &payload))
Expand Down Expand Up @@ -810,7 +810,7 @@ impl Provider for BedrockProvider {
"messages": messages,
"tools": tools
});
let mut log = RequestLog::start(&self.model, &debug_payload)?;
let mut log = start_log(&self.model, &debug_payload)?;

let mut event_stream = response.stream;

Expand Down
7 changes: 3 additions & 4 deletions crates/goose/src/providers/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;

use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata};
use super::utils::{filter_extensions_from_system_prompt, RequestLog};
use super::utils::filter_extensions_from_system_prompt;
use crate::config::base::{CodexCommand, CodexSkipGitCheck};
use crate::config::paths::Paths;
use crate::config::search_path::SearchPaths;
Expand All @@ -23,6 +23,7 @@ use crate::conversation::message::{Message, MessageContent};
use crate::subprocess::configure_subprocess;
use goose_providers::errors::ProviderError;
use goose_providers::model::ModelConfig;
use goose_providers::request_log::{start_log, LoggerHandleExt};
use rmcp::model::Role;
use rmcp::model::Tool;

Expand Down Expand Up @@ -717,9 +718,7 @@ impl Provider for CodexProvider {
"messages_count": messages.len()
});

let mut log = RequestLog::start(model_config, &payload).map_err(|e| {
ProviderError::RequestFailed(format!("Failed to start request log: {}", e))
})?;
let mut log = start_log(model_config, &payload)?;

let response = json!({
"lines": lines.len(),
Expand Down
Loading
Loading