1use serde::{Deserialize, Serialize};
6use std::io;
7use std::path::Path;
8
9pub const DEFAULT_LOG_RETENTION: usize = 2000;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct FlowLogEntry {
16 pub timestamp: String,
18 pub flow_id: String,
20 pub flow_name: String,
22 pub action: String,
25 pub detail: String,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub run_id: Option<String>,
30}
31
32pub fn append_flow_log(log_path: &Path, entry: &FlowLogEntry) -> io::Result<()> {
37 let mut entries = load_flow_logs(log_path);
38 entries.push(entry.clone());
39 if entries.len() > DEFAULT_LOG_RETENTION {
40 let drop = entries.len() - DEFAULT_LOG_RETENTION;
41 entries.drain(0..drop);
42 }
43 let json = serde_json::to_string_pretty(&entries)
44 .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
45 std::fs::write(log_path, json)
46}
47
48pub fn load_flow_logs(log_path: &Path) -> Vec<FlowLogEntry> {
52 if !log_path.exists() {
53 return vec![];
54 }
55 std::fs::read_to_string(log_path)
56 .ok()
57 .and_then(|s| serde_json::from_str(&s).ok())
58 .unwrap_or_default()
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use tempfile::tempdir;
65
66 fn entry(action: &str) -> FlowLogEntry {
67 FlowLogEntry {
68 timestamp: "2026-01-01T00:00:00Z".into(),
69 flow_id: "f1".into(),
70 flow_name: "F1".into(),
71 action: action.into(),
72 detail: "stuff".into(),
73 run_id: Some("r1".into()),
74 }
75 }
76
77 #[test]
78 fn append_then_load_round_trip() {
79 let dir = tempdir().unwrap();
80 let path = dir.path().join("log.json");
81 append_flow_log(&path, &entry("started")).unwrap();
82 append_flow_log(&path, &entry("finished")).unwrap();
83 let loaded = load_flow_logs(&path);
84 assert_eq!(loaded.len(), 2);
85 assert_eq!(loaded[0].action, "started");
86 assert_eq!(loaded[1].action, "finished");
87 }
88
89 #[test]
90 fn missing_log_returns_empty() {
91 let dir = tempdir().unwrap();
92 assert!(load_flow_logs(&dir.path().join("nope.json")).is_empty());
93 }
94
95 #[test]
96 fn retention_drops_oldest() {
97 let dir = tempdir().unwrap();
98 let path = dir.path().join("log.json");
99 for i in 0..(DEFAULT_LOG_RETENTION + 50) {
100 let mut e = entry("tick");
101 e.detail = format!("{i}");
102 append_flow_log(&path, &e).unwrap();
103 }
104 let loaded = load_flow_logs(&path);
105 assert_eq!(loaded.len(), DEFAULT_LOG_RETENTION);
106 assert_eq!(loaded.first().unwrap().detail, "50");
108 assert_eq!(
109 loaded.last().unwrap().detail,
110 format!("{}", DEFAULT_LOG_RETENTION + 49)
111 );
112 }
113}