mati_core/mcp/
daemon_log.rs1use std::fs::{File, OpenOptions};
19use std::io::{self, Write};
20use std::path::{Path, PathBuf};
21use std::sync::{Mutex, OnceLock};
22
23use tracing_subscriber::fmt::MakeWriter;
24
25pub const DAEMON_LOG_FILENAME: &str = "daemon.log";
27
28const MAX_BYTES: u64 = 1 << 20;
31
32pub const DAEMON_DIRECTIVES: &str = "warn,mati=info,mati_core=info";
37
38struct Sink {
39 path: PathBuf,
40 file: File,
41 written: u64,
42}
43
44impl Sink {
45 fn write_line(&mut self, buf: &[u8]) -> io::Result<usize> {
46 if self.written + buf.len() as u64 > MAX_BYTES {
47 self.rotate()?;
48 }
49 let n = self.file.write(buf)?;
50 self.written += n as u64;
51 Ok(n)
52 }
53
54 fn rotate(&mut self) -> io::Result<()> {
55 let previous = self.path.with_extension("log.1");
56 std::fs::rename(&self.path, &previous)?;
57 self.file = open_append(&self.path)?;
58 self.written = 0;
59 Ok(())
60 }
61}
62
63static SINK: OnceLock<Mutex<Sink>> = OnceLock::new();
64
65fn open_append(path: &Path) -> io::Result<File> {
66 OpenOptions::new().create(true).append(true).open(path)
67}
68
69pub fn log_path(root: &Path) -> PathBuf {
71 root.join(DAEMON_LOG_FILENAME)
72}
73
74pub fn install(root: &Path) -> bool {
80 let path = log_path(root);
81 let Ok(file) = open_append(&path) else {
82 return false;
83 };
84 let written = file.metadata().map(|m| m.len()).unwrap_or(0);
85 SINK.set(Mutex::new(Sink {
86 path,
87 file,
88 written,
89 }))
90 .is_ok()
91}
92
93#[derive(Clone, Copy, Default)]
98pub struct DaemonLogWriter;
99
100pub enum Target {
102 Rotating,
103 Stderr(io::Stderr),
104}
105
106impl Write for Target {
107 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
108 match self {
109 Self::Rotating => match SINK.get().and_then(|s| s.lock().ok()) {
112 Some(mut sink) => sink.write_line(buf),
113 None => Ok(buf.len()),
114 },
115 Self::Stderr(w) => w.write(buf),
116 }
117 }
118
119 fn flush(&mut self) -> io::Result<()> {
120 match self {
121 Self::Rotating => Ok(()),
123 Self::Stderr(w) => w.flush(),
124 }
125 }
126}
127
128impl<'a> MakeWriter<'a> for DaemonLogWriter {
129 type Writer = Target;
130
131 fn make_writer(&'a self) -> Self::Writer {
132 if SINK.get().is_some() {
133 Target::Rotating
134 } else {
135 Target::Stderr(io::stderr())
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn sink_at(path: &Path) -> Sink {
147 Sink {
148 path: path.to_path_buf(),
149 file: open_append(path).unwrap(),
150 written: 0,
151 }
152 }
153
154 #[test]
155 fn growth_is_bounded_by_two_generations() {
156 let dir = tempfile::TempDir::new().unwrap();
157 let path = dir.path().join(DAEMON_LOG_FILENAME);
158 let mut sink = sink_at(&path);
159
160 let line = vec![b'x'; 64 * 1024];
161 for _ in 0..64 {
162 sink.write_line(&line).unwrap();
163 }
164
165 let total: u64 = std::fs::read_dir(dir.path())
166 .unwrap()
167 .flatten()
168 .map(|e| e.metadata().unwrap().len())
169 .sum();
170 assert!(
171 total <= 2 * MAX_BYTES,
172 "daemon log grew past two generations: {total} bytes"
173 );
174 assert!(
175 path.with_extension("log.1").exists(),
176 "rotation must keep one previous generation"
177 );
178 }
179
180 #[test]
181 fn rotation_preserves_the_most_recent_lines() {
182 let dir = tempfile::TempDir::new().unwrap();
183 let path = dir.path().join(DAEMON_LOG_FILENAME);
184 let mut sink = sink_at(&path);
185
186 sink.write_line(&vec![b'o'; MAX_BYTES as usize]).unwrap();
187 sink.write_line(b"newest\n").unwrap();
188
189 assert_eq!(std::fs::read_to_string(&path).unwrap(), "newest\n");
190 }
191
192 #[test]
196 fn writer_falls_back_to_stderr_until_installed() {
197 if SINK.get().is_some() {
198 return;
199 }
200 assert!(matches!(DaemonLogWriter.make_writer(), Target::Stderr(_)));
201 }
202}