batuta/playbook/
eventlog.rs1use super::types::{PipelineEvent, TimestampedEvent};
7use anyhow::{Context, Result};
8use std::io::Write;
9use std::path::{Path, PathBuf};
10
11pub fn event_log_path(playbook_path: &Path) -> PathBuf {
13 let stem = playbook_path.file_stem().unwrap_or_default().to_string_lossy();
14 playbook_path.with_file_name(format!("{}.events.jsonl", stem))
15}
16
17pub fn generate_run_id() -> String {
19 use std::time::{SystemTime, UNIX_EPOCH};
20
21 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
22
23 let seed = now.as_nanos() ^ (std::process::id() as u128);
25 format!("r-{:012x}", seed & 0xFFFF_FFFF_FFFF)
26}
27
28pub fn now_iso8601() -> String {
30 chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
31}
32
33pub fn append_event(playbook_path: &Path, event: PipelineEvent) -> Result<()> {
35 let path = event_log_path(playbook_path);
36 let timestamped = TimestampedEvent { ts: now_iso8601(), event };
37
38 let json = serde_json::to_string(×tamped).context("failed to serialize event")?;
39
40 let mut file = std::fs::OpenOptions::new()
41 .create(true)
42 .append(true)
43 .open(&path)
44 .with_context(|| format!("failed to open event log: {}", path.display()))?;
45
46 writeln!(file, "{}", json).context("failed to write event")?;
47
48 Ok(())
49}
50
51#[cfg(test)]
52#[allow(non_snake_case)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn test_PB005_event_log_path() {
58 let path = event_log_path(Path::new("/tmp/pipeline.yaml"));
59 assert_eq!(path, PathBuf::from("/tmp/pipeline.events.jsonl"));
60 }
61
62 #[test]
63 fn test_PB005_generate_run_id_format() {
64 let id = generate_run_id();
65 assert!(id.starts_with("r-"));
66 assert!(id.len() > 2);
67 }
68
69 #[test]
70 fn test_PB005_generate_run_id_unique() {
71 let id1 = generate_run_id();
72 std::thread::sleep(std::time::Duration::from_millis(1));
74 let id2 = generate_run_id();
75 assert_ne!(id1, id2);
76 }
77
78 #[test]
79 fn test_PB005_append_event() {
80 let dir = tempfile::tempdir().expect("tempdir creation failed");
81 let playbook_path = dir.path().join("test.yaml");
82
83 append_event(
84 &playbook_path,
85 PipelineEvent::RunStarted {
86 playbook: "test".to_string(),
87 run_id: "r-abc123".to_string(),
88 batuta_version: "0.6.5".to_string(),
89 },
90 )
91 .expect("unexpected failure");
92
93 append_event(
94 &playbook_path,
95 PipelineEvent::StageCached {
96 stage: "hello".to_string(),
97 cache_key: "blake3:key".to_string(),
98 reason: "cache_key matches".to_string(),
99 },
100 )
101 .expect("unexpected failure");
102
103 let log_path = event_log_path(&playbook_path);
104 let content = std::fs::read_to_string(&log_path).expect("fs read failed");
105 let lines: Vec<&str> = content.lines().collect();
106 assert_eq!(lines.len(), 2);
107
108 let event1: TimestampedEvent =
110 serde_json::from_str(lines[0]).expect("json deserialize failed");
111 assert!(matches!(event1.event, PipelineEvent::RunStarted { .. }));
112
113 let event2: TimestampedEvent =
114 serde_json::from_str(lines[1]).expect("json deserialize failed");
115 assert!(matches!(event2.event, PipelineEvent::StageCached { .. }));
116 }
117
118 #[test]
119 fn test_PB005_now_iso8601_format() {
120 let ts = now_iso8601();
121 assert!(ts.contains('T'));
123 assert!(ts.ends_with('Z'));
124 }
125}