Skip to main content

claudectl_core/
logger.rs

1use std::fs::{File, OpenOptions};
2use std::io::Write;
3use std::sync::Mutex;
4
5static LOGGER: Mutex<Option<File>> = Mutex::new(None);
6
7/// Initialize the logger with a file path. Call once at startup.
8pub fn init(path: &str) -> std::io::Result<()> {
9    let file = OpenOptions::new().create(true).append(true).open(path)?;
10    *LOGGER.lock().unwrap() = Some(file);
11    log("INFO", "Logger initialized");
12    Ok(())
13}
14
15/// Write a log line with timestamp and level.
16pub fn log(level: &str, message: &str) {
17    let Ok(mut guard) = LOGGER.lock() else {
18        return;
19    };
20    let Some(ref mut file) = *guard else {
21        return;
22    };
23    let now = chrono_now();
24    let _ = writeln!(file, "{now} [{level}] {message}");
25}
26
27/// Format current time as ISO 8601 without external crate.
28/// Get the current timestamp as ISO 8601 string.
29pub fn timestamp_now() -> String {
30    chrono_now()
31}
32
33fn chrono_now() -> String {
34    let d = std::time::SystemTime::now()
35        .duration_since(std::time::UNIX_EPOCH)
36        .unwrap_or_default();
37    let total_secs = d.as_secs();
38    let secs_in_day = total_secs % 86400;
39    let h = secs_in_day / 3600;
40    let m = (secs_in_day % 3600) / 60;
41    let s = secs_in_day % 60;
42
43    // Approximate date (good enough for logs, not worth a crate)
44    let days = total_secs / 86400;
45    let (year, month, day) = days_to_date(days);
46
47    format!("{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}Z")
48}
49
50/// Convert days since epoch to (year, month, day). Simplified civil calendar.
51pub fn days_to_date(days: u64) -> (u64, u64, u64) {
52    // Algorithm from http://howardhinnant.github.io/date_algorithms.html
53    let z = days + 719468;
54    let era = z / 146097;
55    let doe = z - era * 146097;
56    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
57    let y = yoe + era * 400;
58    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
59    let mp = (5 * doy + 2) / 153;
60    let d = doy - (153 * mp + 2) / 5 + 1;
61    let m = if mp < 10 { mp + 3 } else { mp - 9 };
62    let y = if m <= 2 { y + 1 } else { y };
63    (y, m, d)
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn test_days_to_date_epoch() {
72        assert_eq!(days_to_date(0), (1970, 1, 1));
73    }
74
75    #[test]
76    fn test_days_to_date_known() {
77        // 2026-04-11 = 20554 days since epoch
78        assert_eq!(days_to_date(20554), (2026, 4, 11));
79    }
80
81    #[test]
82    fn test_log_without_init() {
83        // Should not panic even without init
84        log("DEBUG", "test message");
85    }
86
87    #[test]
88    fn test_init_and_log() {
89        let file = tempfile::NamedTempFile::new().unwrap();
90        let path = file.path().to_str().unwrap();
91        init(path).unwrap();
92        log("INFO", "hello world");
93        log("WARN", "something happened");
94
95        // Read back
96        let content = std::fs::read_to_string(path).unwrap();
97        assert!(content.contains("[INFO] Logger initialized"));
98        assert!(content.contains("[INFO] hello world"));
99        assert!(content.contains("[WARN] something happened"));
100
101        // Clean up global state
102        *LOGGER.lock().unwrap() = None;
103    }
104}