use anyhow::{Context, Result};
use std::sync::Arc;
use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoggingMode {
Cli,
Acp,
WebSocket,
Silent,
}
static LOGGING_MODE: std::sync::OnceLock<LoggingMode> = std::sync::OnceLock::new();
pub fn set_logging_mode(mode: LoggingMode) {
let _ = LOGGING_MODE.set(mode);
}
#[inline]
pub fn get_logging_mode() -> Option<LoggingMode> {
LOGGING_MODE.get().copied()
}
#[inline]
pub fn is_structured_output_mode() -> bool {
matches!(
get_logging_mode(),
Some(LoggingMode::Acp | LoggingMode::WebSocket)
)
}
#[inline]
pub fn is_tracing_initialized() -> bool {
tracing::dispatcher::has_been_set()
}
pub fn init_tracing(mode: LoggingMode, log_level: &str) -> Result<()> {
set_logging_mode(mode);
if tracing::dispatcher::has_been_set() {
return Ok(());
}
match mode {
LoggingMode::Cli => init_cli_logging(log_level),
LoggingMode::Acp => init_acp_logging(log_level),
LoggingMode::WebSocket => init_websocket_logging(log_level),
LoggingMode::Silent => init_silent_logging(),
}
}
fn init_cli_logging(_log_level: &str) -> Result<()> {
if std::env::var("RUST_LOG").is_ok() {
let filter = EnvFilter::from_env("RUST_LOG");
let subscriber = tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_target(false)
.with_thread_ids(false)
.with_file(false)
.with_line_number(false)
.with_span_events(FmtSpan::CLOSE)
.with_env_filter(filter)
.finish();
tracing::subscriber::set_global_default(subscriber)
.with_context(|| "Failed to set tracing subscriber")?;
}
Ok(())
}
fn init_acp_logging(log_level: &str) -> Result<()> {
let logs_dir = crate::directories::get_logs_dir()?;
let log_file = logs_dir.join("acp-debug.log");
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_file)
.with_context(|| format!("Failed to open log file: {:?}", log_file))?;
let filter = create_env_filter(log_level)?;
let subscriber = tracing_subscriber::fmt()
.with_writer(Arc::new(file))
.with_target(true)
.with_thread_ids(true)
.with_file(true)
.with_line_number(true)
.with_span_events(FmtSpan::CLOSE)
.with_env_filter(filter)
.finish();
tracing::subscriber::set_global_default(subscriber)
.with_context(|| "Failed to set tracing subscriber")?;
Ok(())
}
fn init_websocket_logging(log_level: &str) -> Result<()> {
let logs_dir = crate::directories::get_logs_dir()?;
let log_file = logs_dir.join("websocket-debug.log");
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_file)
.with_context(|| format!("Failed to open log file: {:?}", log_file))?;
let filter = create_env_filter(log_level)?;
let subscriber = tracing_subscriber::fmt()
.with_writer(Arc::new(file))
.with_target(true)
.with_thread_ids(true)
.with_file(true)
.with_line_number(true)
.with_span_events(FmtSpan::CLOSE)
.with_env_filter(filter)
.finish();
tracing::subscriber::set_global_default(subscriber)
.with_context(|| "Failed to set tracing subscriber")?;
Ok(())
}
fn init_silent_logging() -> Result<()> {
let subscriber = tracing_subscriber::fmt()
.with_writer(std::io::sink)
.with_env_filter(EnvFilter::new("off"))
.finish();
tracing::subscriber::set_global_default(subscriber)
.with_context(|| "Failed to set tracing subscriber")?;
Ok(())
}
fn create_env_filter(level: &str) -> Result<EnvFilter> {
let filter_str = match level.to_lowercase().as_str() {
"debug" => "debug",
"info" => "info",
"warn" => "warn",
"error" => "error",
"trace" => "trace",
"off" => "off",
_ => "info",
};
let filter = if std::env::var("RUST_LOG").is_ok() {
EnvFilter::from_env("RUST_LOG")
} else {
EnvFilter::new(filter_str)
};
Ok(filter)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_env_filter() {
assert!(create_env_filter("debug").is_ok());
assert!(create_env_filter("info").is_ok());
assert!(create_env_filter("warn").is_ok());
assert!(create_env_filter("error").is_ok());
assert!(create_env_filter("trace").is_ok());
assert!(create_env_filter("off").is_ok());
assert!(create_env_filter("unknown").is_ok());
}
#[test]
fn test_logging_mode_tracking() {
let mode = get_logging_mode();
assert!(
mode.is_none()
|| matches!(
mode,
Some(
LoggingMode::Cli
| LoggingMode::Acp | LoggingMode::WebSocket
| LoggingMode::Silent
)
)
);
}
#[test]
fn test_is_structured_output_mode() {
let _ = is_structured_output_mode();
}
#[test]
fn test_is_tracing_initialized() {
let _ = is_tracing_initialized();
}
}