Skip to main content

apm_core/
logger.rs

1use std::fs::OpenOptions;
2use std::io::Write;
3use std::path::Path;
4use std::sync::{Mutex, OnceLock};
5
6static LOGGER: OnceLock<Option<Mutex<std::io::BufWriter<std::fs::File>>>> = OnceLock::new();
7static AGENT: OnceLock<String> = OnceLock::new();
8
9pub fn default_log_path(project_name: &str) -> std::path::PathBuf {
10    #[cfg(target_os = "macos")]
11    {
12        let home = std::env::var("HOME").unwrap_or_default();
13        std::path::PathBuf::from(home)
14            .join("Library/Logs/apm")
15            .join(format!("{project_name}.log"))
16    }
17    #[cfg(not(target_os = "macos"))]
18    {
19        let base = std::env::var("XDG_STATE_HOME")
20            .map(std::path::PathBuf::from)
21            .unwrap_or_else(|_| {
22                let home = std::env::var("HOME").unwrap_or_default();
23                std::path::PathBuf::from(home).join(".local/state")
24            });
25        base.join("apm").join(format!("{project_name}.log"))
26    }
27}
28
29pub fn resolve_log_path(project_name: &str, override_path: Option<&std::path::Path>) -> std::path::PathBuf {
30    if let Some(p) = override_path {
31        expand_tilde(p)
32    } else {
33        default_log_path(project_name)
34    }
35}
36
37fn expand_tilde(path: &std::path::Path) -> std::path::PathBuf {
38    let s = path.to_string_lossy();
39    if let Some(rest) = s.strip_prefix("~/") {
40        let home = std::env::var("HOME").unwrap_or_default();
41        std::path::PathBuf::from(home).join(rest)
42    } else {
43        path.to_path_buf()
44    }
45}
46
47pub fn init(root: &Path, log_file: &Path, agent: &str) {
48    AGENT.get_or_init(|| agent.to_string());
49    let path = if log_file.is_absolute() {
50        log_file.to_path_buf()
51    } else {
52        root.join(log_file)
53    };
54    let file = OpenOptions::new().create(true).append(true).open(&path).ok();
55    LOGGER.get_or_init(|| file.map(|f| Mutex::new(std::io::BufWriter::new(f))));
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    #[test]
62    fn default_log_path_contains_apm_and_ends_log() {
63        let p = default_log_path("myproject");
64        let s = p.to_string_lossy();
65        assert!(s.contains("apm"), "path should contain 'apm': {s}");
66        assert!(s.ends_with(".log"), "path should end with .log: {s}");
67        assert!(s.contains("myproject"), "path should contain project name: {s}");
68    }
69    #[test]
70    fn tilde_expansion() {
71        let home = std::env::var("HOME").unwrap();
72        let result = expand_tilde(std::path::Path::new("~/foo.log"));
73        assert_eq!(result, std::path::PathBuf::from(&home).join("foo.log"));
74    }
75}
76
77pub fn log(action: &str, detail: &str) {
78    let Some(Some(mutex)) = LOGGER.get() else { return };
79    let agent = AGENT.get().map(|s| s.as_str()).unwrap_or("apm");
80    let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ");
81    let line = format!("{now} [{agent}] {action} {detail}\n");
82    if let Ok(mut writer) = mutex.lock() {
83        let _ = writer.write_all(line.as_bytes());
84        let _ = writer.flush();
85    }
86}