1use std::fs;
2use std::io::{BufRead, BufReader, Seek, SeekFrom};
3use std::path::{Path, PathBuf};
4
5use chrono::Utc;
6
7use crate::daemon::LogConfig;
8use crate::error::Result;
9use crate::paths;
10
11pub struct LogManager {
12 data_dir: PathBuf,
13}
14
15impl LogManager {
16 pub fn new(data_dir: &Path) -> Self {
17 Self {
18 data_dir: data_dir.to_path_buf(),
19 }
20 }
21
22 pub fn with_defaults() -> Self {
23 Self {
24 data_dir: paths::data_dir(),
25 }
26 }
27
28 fn log_dir(&self, daemon_name: &str) -> PathBuf {
29 self.data_dir.join("logs").join(daemon_name)
30 }
31
32 pub fn setup_log_files(&self, daemon_name: &str) -> Result<(PathBuf, PathBuf)> {
34 let dir = self.log_dir(daemon_name);
35 fs::create_dir_all(&dir)?;
36
37 let stdout_path = dir.join("stdout.log");
38 let stderr_path = dir.join("stderr.log");
39
40 Ok((stdout_path, stderr_path))
41 }
42
43 pub fn rotate_logs(&self, daemon_name: &str, config: &LogConfig) -> Result<()> {
46 let dir = self.log_dir(daemon_name);
47
48 for log_name in &["stdout.log", "stderr.log"] {
49 let log_path = dir.join(log_name);
50 if !log_path.exists() {
51 continue;
52 }
53
54 let metadata = fs::metadata(&log_path)?;
55 if metadata.len() < config.max_size_bytes {
56 continue;
57 }
58
59 let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
61 let base = log_name.trim_end_matches(".log");
62 let rotated_name = format!("{}_{}.log", base, timestamp);
63 let rotated_path = dir.join(&rotated_name);
64
65 fs::rename(&log_path, &rotated_path)?;
66 tracing::info!(
67 daemon = daemon_name,
68 file = %log_name,
69 rotated_to = %rotated_name,
70 "Rotated log file"
71 );
72
73 self.prune_rotated(&dir, base, config.retain_count)?;
75 }
76
77 Ok(())
78 }
79
80 fn prune_rotated(&self, dir: &Path, base_name: &str, retain_count: u32) -> Result<()> {
82 let prefix = format!("{}_", base_name);
83
84 let mut rotated: Vec<PathBuf> = fs::read_dir(dir)?
85 .filter_map(|entry| entry.ok())
86 .filter(|entry| {
87 let name = entry.file_name().to_string_lossy().to_string();
88 name.starts_with(&prefix) && name.ends_with(".log")
89 })
90 .map(|entry| entry.path())
91 .collect();
92
93 rotated.sort();
95 rotated.reverse();
96
97 for path in rotated.iter().skip(retain_count as usize) {
98 tracing::debug!(path = %path.display(), "Pruning old rotated log");
99 if let Err(e) = fs::remove_file(path) {
100 tracing::warn!(path = %path.display(), error = %e, "Failed to prune log file");
101 }
102 }
103
104 Ok(())
105 }
106
107 pub fn read_logs(&self, daemon_name: &str, lines: usize, stderr: bool) -> Result<Vec<String>> {
110 let dir = self.log_dir(daemon_name);
111 let filename = if stderr { "stderr.log" } else { "stdout.log" };
112 let log_path = dir.join(filename);
113
114 if !log_path.exists() {
115 return Ok(Vec::new());
116 }
117
118 tail_file(&log_path, lines)
119 }
120}
121
122fn tail_file(path: &Path, n: usize) -> Result<Vec<String>> {
124 if n == 0 {
125 return Ok(Vec::new());
126 }
127
128 let file = fs::File::open(path)?;
129 let metadata = file.metadata()?;
130 let file_size = metadata.len();
131
132 if file_size == 0 {
133 return Ok(Vec::new());
134 }
135
136 if file_size < 64 * 1024 {
138 let reader = BufReader::new(file);
139 let all_lines: Vec<String> = reader.lines().filter_map(|l| l.ok()).collect();
140 let start = all_lines.len().saturating_sub(n);
141 return Ok(all_lines[start..].to_vec());
142 }
143
144 let mut file = file;
146 let chunk_size: u64 = 8192;
147 let mut remaining = file_size;
148 let mut trailing_data = Vec::new();
149
150 loop {
151 let read_size = chunk_size.min(remaining);
152 remaining -= read_size;
153
154 file.seek(SeekFrom::Start(remaining))?;
155 let mut buf = vec![0u8; read_size as usize];
156 std::io::Read::read_exact(&mut file, &mut buf)?;
157
158 buf.extend_from_slice(&trailing_data);
160 trailing_data = buf;
161
162 let text = String::from_utf8_lossy(&trailing_data);
163 let lines_in_buf: Vec<&str> = text.lines().collect();
164
165 if lines_in_buf.len() > n || remaining == 0 {
166 let start = lines_in_buf.len().saturating_sub(n);
167 return Ok(lines_in_buf[start..]
168 .iter()
169 .map(|s| s.to_string())
170 .collect());
171 }
172 }
173}