Skip to main content

aster/
logging.rs

1use crate::config::paths::Paths;
2use anyhow::{Context, Result};
3use std::fs;
4use std::path::PathBuf;
5use std::time::{Duration, SystemTime};
6
7/// Returns the directory where log files should be stored for a specific component.
8/// Creates the directory structure if it doesn't exist.
9///
10/// # Arguments
11///
12/// * `component` - The component name (e.g., "cli", "server", "debug", "llm")
13/// * `use_date_subdir` - Whether to create a date-based subdirectory
14pub fn prepare_log_directory(component: &str, use_date_subdir: bool) -> Result<PathBuf> {
15    let base_log_dir = Paths::in_state_dir("logs");
16
17    let _ = cleanup_old_logs(component);
18
19    let component_dir = base_log_dir.join(component);
20
21    let log_dir = if use_date_subdir {
22        component_dir.join(chrono::Local::now().format("%Y-%m-%d").to_string())
23    } else {
24        component_dir
25    };
26
27    fs::create_dir_all(&log_dir)
28        .with_context(|| format!("Failed to create log directory: {:?}", log_dir))?;
29
30    Ok(log_dir)
31}
32
33pub fn cleanup_old_logs(component: &str) -> Result<()> {
34    let base_log_dir = Paths::in_state_dir("logs");
35    let component_dir = base_log_dir.join(component);
36
37    if !component_dir.exists() {
38        return Ok(());
39    }
40
41    let two_weeks = SystemTime::now() - Duration::from_secs(14 * 24 * 60 * 60);
42    let entries = fs::read_dir(&component_dir)?;
43
44    for entry in entries.flatten() {
45        let path = entry.path();
46
47        if let Ok(metadata) = entry.metadata() {
48            if let Ok(modified) = metadata.modified() {
49                if modified < two_weeks && path.is_dir() {
50                    let _ = fs::remove_dir_all(&path);
51                }
52            }
53        }
54    }
55
56    Ok(())
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use std::fs;
63
64    #[test]
65    fn test_get_log_directory_basic_functionality() {
66        // Test basic directory creation without date subdirectory
67        let result = prepare_log_directory("cli", false);
68        assert!(result.is_ok());
69
70        let log_dir = result.unwrap();
71
72        // Verify the directory was created and has correct structure
73        assert!(log_dir.exists());
74        assert!(log_dir.is_dir());
75
76        let path_str = log_dir.to_string_lossy();
77        assert!(path_str.contains("cli"));
78        assert!(path_str.contains("logs"));
79
80        // Verify we can write to the directory
81        let test_file = log_dir.join("test.log");
82        assert!(fs::write(&test_file, "test log content").is_ok());
83        let _ = fs::remove_file(&test_file);
84    }
85
86    #[test]
87    fn test_get_log_directory_with_date_subdir() {
88        // Test date-based subdirectory creation
89        let result = prepare_log_directory("server", true);
90        assert!(result.is_ok());
91
92        let log_dir = result.unwrap();
93
94        // Verify the directory was created
95        assert!(log_dir.exists());
96        assert!(log_dir.is_dir());
97
98        let path_str = log_dir.to_string_lossy();
99        assert!(path_str.contains("server"));
100        assert!(path_str.contains("logs"));
101
102        // Verify date format (YYYY-MM-DD) is present
103        let now = chrono::Local::now();
104        let date_str = now.format("%Y-%m-%d").to_string();
105        assert!(path_str.contains(&date_str));
106
107        // Verify path structure: logs -> component -> date
108        let logs_pos = path_str.find("logs").unwrap();
109        let component_pos = path_str.find("server").unwrap();
110        let date_pos = path_str.find(&date_str).unwrap();
111        assert!(logs_pos < component_pos);
112        assert!(component_pos < date_pos);
113    }
114
115    #[test]
116    fn test_get_log_directory_idempotent() {
117        // Test that multiple calls return the same result and don't fail
118        let component = "debug";
119
120        let result1 = prepare_log_directory(component, false);
121        assert!(result1.is_ok());
122        let log_dir1 = result1.unwrap();
123
124        let result2 = prepare_log_directory(component, false);
125        assert!(result2.is_ok());
126        let log_dir2 = result2.unwrap();
127
128        // Both calls should return the same path and directory should exist
129        assert_eq!(log_dir1, log_dir2);
130        assert!(log_dir1.exists());
131        assert!(log_dir2.exists());
132
133        // Test same behavior with date subdirectories
134        let result3 = prepare_log_directory(component, true);
135        assert!(result3.is_ok());
136        let log_dir3 = result3.unwrap();
137
138        let result4 = prepare_log_directory(component, true);
139        assert!(result4.is_ok());
140        let log_dir4 = result4.unwrap();
141
142        assert_eq!(log_dir3, log_dir4);
143        assert!(log_dir3.exists());
144    }
145
146    #[test]
147    fn test_get_log_directory_different_components() {
148        // Test that different components create different directories
149        let components = ["cli", "server", "debug"];
150        let mut created_dirs = Vec::new();
151
152        for component in &components {
153            let result = prepare_log_directory(component, false);
154            assert!(result.is_ok(), "Failed for component: {}", component);
155
156            let log_dir = result.unwrap();
157            assert!(log_dir.exists());
158            assert!(log_dir.to_string_lossy().contains(component));
159
160            created_dirs.push(log_dir);
161        }
162
163        // Verify all directories are different
164        for i in 0..created_dirs.len() {
165            for j in i + 1..created_dirs.len() {
166                assert_ne!(created_dirs[i], created_dirs[j]);
167            }
168        }
169    }
170}