Skip to main content

ubermind_core/
logs.rs

1use crate::protocol::state_dir;
2use std::path::PathBuf;
3
4pub fn log_dir() -> PathBuf {
5	state_dir().join("logs")
6}
7
8pub fn service_log_dir(service: &str) -> PathBuf {
9	log_dir().join(service)
10}
11
12pub fn current_log_name(process: &str) -> String {
13	let now = now_ymd();
14	format!("{} {}.log", process, now)
15}
16
17pub fn rotated_log_name(process: &str) -> String {
18	let now = now_ymdhm();
19	let (date, hour, minute) = now;
20	let candidate = format!("{} {} {}.log", process, date, hour);
21	let candidate_path = log_dir().join(&candidate);
22	if candidate_path.exists() {
23		format!("{} {} {}.{}.log", process, date, hour, minute)
24	} else {
25		candidate
26	}
27}
28
29pub fn parse_log_date(filename: &str) -> Option<(u32, u32, u32)> {
30	// Extract "YY-MMDD" from filename like "web 26-0214.log"
31	let parts: Vec<&str> = filename.splitn(2, ' ').collect();
32	if parts.len() < 2 {
33		return None;
34	}
35	let rest = parts[1];
36	// Extract date portion - everything before first space or .log
37	let date_str = rest
38		.split(' ')
39		.next()
40		.unwrap_or(rest)
41		.trim_end_matches(".log");
42
43	// Parse "YY-MMDD"
44	let parts: Vec<&str> = date_str.splitn(2, '-').collect();
45	if parts.len() != 2 {
46		return None;
47	}
48	let year: u32 = parts[0].parse().ok()?;
49	let mmdd = parts[1];
50	if mmdd.len() != 4 {
51		return None;
52	}
53	let month: u32 = mmdd[..2].parse().ok()?;
54	let day: u32 = mmdd[2..].parse().ok()?;
55	Some((year, month, day))
56}
57
58fn now_ymd() -> String {
59	// Format: "YY-MMDD"
60	use std::time::SystemTime;
61	let now = SystemTime::now()
62		.duration_since(SystemTime::UNIX_EPOCH)
63		.unwrap()
64		.as_secs();
65	let (year, month, day, _, _) = secs_to_datetime(now);
66	format!("{:02}-{:02}{:02}", year % 100, month, day)
67}
68
69fn now_ymdhm() -> (String, String, String) {
70	use std::time::SystemTime;
71	let now = SystemTime::now()
72		.duration_since(SystemTime::UNIX_EPOCH)
73		.unwrap()
74		.as_secs();
75	let (year, month, day, hour, minute) = secs_to_datetime(now);
76	(
77		format!("{:02}-{:02}{:02}", year % 100, month, day),
78		format!("{:02}", hour),
79		format!("{:02}", minute),
80	)
81}
82
83fn secs_to_datetime(secs: u64) -> (u32, u32, u32, u32, u32) {
84	// Simple UTC datetime from unix timestamp
85	let days = (secs / 86400) as i64;
86	let time_of_day = secs % 86400;
87	let hour = (time_of_day / 3600) as u32;
88	let minute = ((time_of_day % 3600) / 60) as u32;
89
90	// Days since epoch to date (civil_from_days algorithm)
91	let z = days + 719468;
92	let era = if z >= 0 { z } else { z - 146096 } / 146097;
93	let doe = (z - era * 146097) as u32;
94	let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
95	let y = yoe as i64 + era * 400;
96	let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
97	let mp = (5 * doy + 2) / 153;
98	let d = doy - (153 * mp + 2) / 5 + 1;
99	let m = if mp < 10 { mp + 3 } else { mp - 9 };
100	let y = if m <= 2 { y + 1 } else { y };
101
102	(y as u32, m, d, hour, minute)
103}
104
105#[cfg(test)]
106mod tests {
107	use super::*;
108
109	#[test]
110	fn test_parse_log_date() {
111		assert_eq!(parse_log_date("web 26-0214.log"), Some((26, 2, 14)));
112		assert_eq!(parse_log_date("web 26-0214 09.log"), Some((26, 2, 14)));
113		assert_eq!(parse_log_date("web 26-0214 09.47.log"), Some((26, 2, 14)));
114		assert_eq!(parse_log_date("invalid"), None);
115	}
116
117	#[test]
118	fn test_secs_to_datetime() {
119		// 2026-02-14 00:00:00 UTC = 1771027200
120		let (y, m, d, h, min) = secs_to_datetime(1771027200);
121		assert_eq!((y, m, d, h, min), (2026, 2, 14, 0, 0));
122	}
123}