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}