use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::SystemTime;
use tracing::Level;
use crate::config::LogDestination;
use crate::wire::iso_millis;
#[derive(Debug)]
pub(crate) struct Logger {
destination: LogDestination,
path: Option<PathBuf>,
}
impl Logger {
pub(crate) fn new(destination: LogDestination, home: Option<PathBuf>) -> Self {
Self {
destination,
path: home.map(|dir| dir.join("agentcat.log")),
}
}
pub(crate) fn write(&self, message: &str) {
match self.destination {
LogDestination::Tracing => emit(message),
LogDestination::File => self.write_file(message),
}
}
fn write_file(&self, message: &str) {
let Some(path) = &self.path else { return };
let line = format!(
"[{}] {} {message}\n",
iso_millis(SystemTime::now()),
prefix()
);
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
let _ = file.write_all(line.as_bytes());
}
}
}
fn emit(message: &str) {
match level(message) {
Level::ERROR => tracing::error!(target: "agentcat", "{message}"),
Level::WARN => tracing::warn!(target: "agentcat", "{message}"),
_ => tracing::info!(target: "agentcat", "{message}"),
}
}
static GLOBAL: OnceLock<Logger> = OnceLock::new();
pub(crate) fn configure(destination: LogDestination, home: Option<PathBuf>) {
let _ = GLOBAL.set(Logger::new(destination, home));
}
pub(crate) fn write(message: &str) {
if let Some(logger) = GLOBAL.get() {
logger.write(message);
}
}
pub(crate) fn prefix() -> &'static str {
static PREFIX: OnceLock<String> = OnceLock::new();
PREFIX.get_or_init(|| {
format!(
"[sdk={} rust={} rmcp={}]",
env!("CARGO_PKG_VERSION"),
env!("AGENTCAT_RUSTC_VERSION"),
rmcp::model::Implementation::default().version
)
})
}
pub(crate) fn level(message: &str) -> Level {
let lower = message.to_lowercase();
if lower.contains("fail") || lower.contains("error") {
Level::ERROR
} else if message.contains("Warning:") {
Level::WARN
} else {
Level::INFO
}
}
#[cfg(test)]
mod tests;