1use std::fs::OpenOptions;
15use std::io::Write;
16use std::path::Path;
17use std::sync::{Mutex, OnceLock};
18
19use crate::error::PawError;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
23pub enum Level {
24 Error,
26 Warn,
28 Info,
30 Debug,
32 Trace,
34}
35
36impl Level {
37 fn label(self) -> &'static str {
38 match self {
39 Self::Error => "ERROR",
40 Self::Warn => "WARN",
41 Self::Info => "INFO",
42 Self::Debug => "DEBUG",
43 Self::Trace => "TRACE",
44 }
45 }
46}
47
48#[must_use]
52pub fn parse_level(rust_log: Option<&str>) -> Level {
53 let Some(value) = rust_log else {
54 return Level::Warn;
55 };
56 let v = value.to_ascii_lowercase();
57 if v.contains("trace") {
59 Level::Trace
60 } else if v.contains("debug") {
61 Level::Debug
62 } else if v.contains("info") {
63 Level::Info
64 } else if v.contains("error") && !v.contains("warn") {
65 Level::Error
66 } else {
67 Level::Warn
68 }
69}
70
71struct Logger {
72 threshold: Level,
73 file: Option<Mutex<std::fs::File>>,
74}
75
76static LOGGER: OnceLock<Logger> = OnceLock::new();
77
78pub fn init(log_file: Option<&Path>) -> Result<(), PawError> {
82 let threshold = parse_level(std::env::var("RUST_LOG").ok().as_deref());
83 let file = match log_file {
84 Some(path) => {
85 let f = OpenOptions::new()
86 .create(true)
87 .append(true)
88 .open(path)
89 .map_err(|e| {
90 PawError::McpError(format!("could not open --log-file {}: {e}", path.display()))
91 })?;
92 Some(Mutex::new(f))
93 }
94 None => None,
95 };
96 let _ = LOGGER.set(Logger { threshold, file });
98 Ok(())
99}
100
101pub fn log(level: Level, message: &str) {
105 match LOGGER.get() {
106 Some(logger) => {
107 if level > logger.threshold {
108 return;
109 }
110 let line = format!("[git-paw mcp] {}: {message}\n", level.label());
111 let _ = std::io::stderr().write_all(line.as_bytes());
113 if let Some(file) = logger.file.as_ref()
114 && let Ok(mut f) = file.lock()
115 {
116 let _ = f.write_all(line.as_bytes());
117 }
118 }
119 None => {
120 if level <= Level::Warn {
121 let _ = std::io::stderr()
122 .write_all(format!("[git-paw mcp] {}: {message}\n", level.label()).as_bytes());
123 }
124 }
125 }
126}
127
128pub fn info(message: &str) {
130 log(Level::Info, message);
131}
132
133pub fn warn(message: &str) {
135 log(Level::Warn, message);
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn parse_level_defaults_to_warn() {
144 assert_eq!(parse_level(None), Level::Warn);
145 assert_eq!(parse_level(Some("")), Level::Warn);
146 }
147
148 #[test]
149 fn parse_level_recognises_keywords() {
150 assert_eq!(parse_level(Some("debug")), Level::Debug);
151 assert_eq!(parse_level(Some("info")), Level::Info);
152 assert_eq!(parse_level(Some("trace")), Level::Trace);
153 assert_eq!(parse_level(Some("git_paw=debug,hyper=warn")), Level::Debug);
154 }
155
156 #[test]
157 fn level_ordering_is_least_to_most_verbose() {
158 assert!(Level::Error < Level::Warn);
159 assert!(Level::Warn < Level::Debug);
160 assert!(Level::Debug < Level::Trace);
161 }
162}