use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use std::sync::{Mutex, OnceLock};
use crate::error::PawError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Error,
Warn,
Info,
Debug,
Trace,
}
impl Level {
fn label(self) -> &'static str {
match self {
Self::Error => "ERROR",
Self::Warn => "WARN",
Self::Info => "INFO",
Self::Debug => "DEBUG",
Self::Trace => "TRACE",
}
}
}
#[must_use]
pub fn parse_level(rust_log: Option<&str>) -> Level {
let Some(value) = rust_log else {
return Level::Warn;
};
let v = value.to_ascii_lowercase();
if v.contains("trace") {
Level::Trace
} else if v.contains("debug") {
Level::Debug
} else if v.contains("info") {
Level::Info
} else if v.contains("error") && !v.contains("warn") {
Level::Error
} else {
Level::Warn
}
}
struct Logger {
threshold: Level,
file: Option<Mutex<std::fs::File>>,
}
static LOGGER: OnceLock<Logger> = OnceLock::new();
pub fn init(log_file: Option<&Path>) -> Result<(), PawError> {
let threshold = parse_level(std::env::var("RUST_LOG").ok().as_deref());
let file = match log_file {
Some(path) => {
let f = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| {
PawError::McpError(format!("could not open --log-file {}: {e}", path.display()))
})?;
Some(Mutex::new(f))
}
None => None,
};
let _ = LOGGER.set(Logger { threshold, file });
Ok(())
}
pub fn log(level: Level, message: &str) {
match LOGGER.get() {
Some(logger) => {
if level > logger.threshold {
return;
}
let line = format!("[git-paw mcp] {}: {message}\n", level.label());
let _ = std::io::stderr().write_all(line.as_bytes());
if let Some(file) = logger.file.as_ref()
&& let Ok(mut f) = file.lock()
{
let _ = f.write_all(line.as_bytes());
}
}
None => {
if level <= Level::Warn {
let _ = std::io::stderr()
.write_all(format!("[git-paw mcp] {}: {message}\n", level.label()).as_bytes());
}
}
}
}
pub fn info(message: &str) {
log(Level::Info, message);
}
pub fn warn(message: &str) {
log(Level::Warn, message);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_level_defaults_to_warn() {
assert_eq!(parse_level(None), Level::Warn);
assert_eq!(parse_level(Some("")), Level::Warn);
}
#[test]
fn parse_level_recognises_keywords() {
assert_eq!(parse_level(Some("debug")), Level::Debug);
assert_eq!(parse_level(Some("info")), Level::Info);
assert_eq!(parse_level(Some("trace")), Level::Trace);
assert_eq!(parse_level(Some("git_paw=debug,hyper=warn")), Level::Debug);
}
#[test]
fn level_ordering_is_least_to_most_verbose() {
assert!(Level::Error < Level::Warn);
assert!(Level::Warn < Level::Debug);
assert!(Level::Debug < Level::Trace);
}
}