1use std::fs::{self, OpenOptions};
2use std::io::Write;
3use std::path::PathBuf;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6#[derive(Clone)]
7pub struct LogStore {
8 dir: PathBuf,
9}
10
11impl LogStore {
12 pub fn new(dir: PathBuf) -> Self {
13 Self { dir }
14 }
15
16 pub fn process_log_path(&self, process: &str) -> PathBuf {
17 self.dir.join(format!("{process}.log"))
18 }
19
20 pub fn append_line(&self, process: &str, stream: &str, line: &str) -> Result<(), String> {
21 fs::create_dir_all(&self.dir)
22 .map_err(|err| format!("failed to create log dir {}: {}", self.dir.display(), err))?;
23 let path = self.process_log_path(process);
24 let mut file = OpenOptions::new()
25 .create(true)
26 .append(true)
27 .open(&path)
28 .map_err(|err| format!("failed to open process log {}: {}", path.display(), err))?;
29 writeln!(file, "{} [{}] {}", unix_timestamp_secs(), stream, line)
30 .map_err(|err| format!("failed to append process log {}: {}", path.display(), err))
31 }
32}
33
34fn unix_timestamp_secs() -> u64 {
35 SystemTime::now()
36 .duration_since(UNIX_EPOCH)
37 .unwrap_or_default()
38 .as_secs()
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 #[test]
46 fn log_store_appends_process_output() {
47 let dir = std::env::temp_dir().join(format!("rns-server-logs-{}", std::process::id()));
48 let store = LogStore::new(dir.clone());
49
50 store.append_line("rnsd", "stdout", "started").unwrap();
51 store.append_line("rnsd", "stderr", "warning").unwrap();
52
53 let body = std::fs::read_to_string(store.process_log_path("rnsd")).unwrap();
54 assert!(body.contains("[stdout] started"));
55 assert!(body.contains("[stderr] warning"));
56
57 let _ = std::fs::remove_dir_all(dir);
58 }
59
60 #[test]
61 fn process_log_path_is_per_process() {
62 let store = LogStore::new(PathBuf::from("/tmp/rns/logs"));
63 assert_eq!(
64 store.process_log_path("rns-statsd"),
65 std::path::Path::new("/tmp/rns/logs/rns-statsd.log")
66 );
67 }
68}