1#![allow(dead_code)]
8
9use serde::{Deserialize, Serialize};
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum WorkflowEventType {
17 Submitted,
19 TaskStarted,
21 TaskCompleted,
23 TaskFailed,
25 WorkflowCompleted,
27 WorkflowCancelled,
29 Warning,
31 Error,
33}
34
35impl WorkflowEventType {
36 #[must_use]
38 pub fn is_error(self) -> bool {
39 matches!(self, Self::TaskFailed | Self::Error)
40 }
41
42 #[must_use]
44 pub fn is_success(self) -> bool {
45 matches!(self, Self::TaskCompleted | Self::WorkflowCompleted)
46 }
47
48 #[must_use]
50 pub fn label(self) -> &'static str {
51 match self {
52 Self::Submitted => "SUBMITTED",
53 Self::TaskStarted => "TASK_START",
54 Self::TaskCompleted => "TASK_OK",
55 Self::TaskFailed => "TASK_FAIL",
56 Self::WorkflowCompleted => "WF_OK",
57 Self::WorkflowCancelled => "WF_CANCEL",
58 Self::Warning => "WARN",
59 Self::Error => "ERROR",
60 }
61 }
62}
63
64impl std::fmt::Display for WorkflowEventType {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "{}", self.label())
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74pub struct WorkflowEvent {
75 pub seq: u64,
77 pub timestamp_secs: u64,
79 pub event_type: WorkflowEventType,
81 pub workflow_id: String,
83 pub task_id: Option<String>,
85 pub message: String,
87}
88
89impl WorkflowEvent {
90 #[must_use]
92 pub fn workflow_event(
93 seq: u64,
94 event_type: WorkflowEventType,
95 workflow_id: impl Into<String>,
96 message: impl Into<String>,
97 ) -> Self {
98 Self {
99 seq,
100 timestamp_secs: now_secs(),
101 event_type,
102 workflow_id: workflow_id.into(),
103 task_id: None,
104 message: message.into(),
105 }
106 }
107
108 #[must_use]
110 pub fn task_event(
111 seq: u64,
112 event_type: WorkflowEventType,
113 workflow_id: impl Into<String>,
114 task_id: impl Into<String>,
115 message: impl Into<String>,
116 ) -> Self {
117 Self {
118 seq,
119 timestamp_secs: now_secs(),
120 event_type,
121 workflow_id: workflow_id.into(),
122 task_id: Some(task_id.into()),
123 message: message.into(),
124 }
125 }
126
127 #[must_use]
129 pub fn is_error(&self) -> bool {
130 self.event_type.is_error()
131 }
132}
133
134fn now_secs() -> u64 {
136 SystemTime::now()
137 .duration_since(UNIX_EPOCH)
138 .unwrap_or(Duration::ZERO)
139 .as_secs()
140}
141
142#[derive(Debug, Default, Clone)]
147pub struct WorkflowLog {
148 events: Vec<WorkflowEvent>,
149 next_seq: u64,
150}
151
152impl WorkflowLog {
153 #[must_use]
155 pub fn new() -> Self {
156 Self::default()
157 }
158
159 pub fn append(
161 &mut self,
162 event_type: WorkflowEventType,
163 workflow_id: impl Into<String>,
164 message: impl Into<String>,
165 ) {
166 let ev = WorkflowEvent::workflow_event(self.next_seq, event_type, workflow_id, message);
167 self.next_seq += 1;
168 self.events.push(ev);
169 }
170
171 pub fn append_task(
173 &mut self,
174 event_type: WorkflowEventType,
175 workflow_id: impl Into<String>,
176 task_id: impl Into<String>,
177 message: impl Into<String>,
178 ) {
179 let ev =
180 WorkflowEvent::task_event(self.next_seq, event_type, workflow_id, task_id, message);
181 self.next_seq += 1;
182 self.events.push(ev);
183 }
184
185 #[must_use]
187 pub fn len(&self) -> usize {
188 self.events.len()
189 }
190
191 #[must_use]
193 pub fn is_empty(&self) -> bool {
194 self.events.is_empty()
195 }
196
197 #[must_use]
199 pub fn all(&self) -> &[WorkflowEvent] {
200 &self.events
201 }
202
203 #[must_use]
206 pub fn recent_errors(&self, n: usize) -> Vec<&WorkflowEvent> {
207 self.events
208 .iter()
209 .rev()
210 .filter(|e| e.is_error())
211 .take(n)
212 .collect()
213 }
214
215 #[must_use]
217 pub fn events_for_workflow<'a>(&'a self, workflow_id: &str) -> Vec<&'a WorkflowEvent> {
218 self.events
219 .iter()
220 .filter(|e| e.workflow_id == workflow_id)
221 .collect()
222 }
223
224 #[must_use]
226 pub fn events_of_type(&self, event_type: WorkflowEventType) -> Vec<&WorkflowEvent> {
227 self.events
228 .iter()
229 .filter(|e| e.event_type == event_type)
230 .collect()
231 }
232
233 #[must_use]
235 pub fn error_count(&self) -> usize {
236 self.events.iter().filter(|e| e.is_error()).count()
237 }
238
239 pub fn clear(&mut self) {
241 self.events.clear();
242 self.next_seq = 0;
243 }
244}
245
246#[cfg(test)]
249mod tests {
250 use super::*;
251
252 fn make_log() -> WorkflowLog {
253 let mut log = WorkflowLog::new();
254 log.append(WorkflowEventType::Submitted, "wf-1", "Workflow submitted");
255 log.append_task(
256 WorkflowEventType::TaskStarted,
257 "wf-1",
258 "task-a",
259 "Started task-a",
260 );
261 log.append_task(
262 WorkflowEventType::TaskFailed,
263 "wf-1",
264 "task-a",
265 "task-a failed: timeout",
266 );
267 log.append(WorkflowEventType::Error, "wf-1", "Unrecoverable error");
268 log.append_task(
269 WorkflowEventType::TaskCompleted,
270 "wf-2",
271 "task-b",
272 "task-b done",
273 );
274 log
275 }
276
277 #[test]
278 fn test_new_log_empty() {
279 let log = WorkflowLog::new();
280 assert!(log.is_empty());
281 assert_eq!(log.len(), 0);
282 }
283
284 #[test]
285 fn test_append_increments_len() {
286 let mut log = WorkflowLog::new();
287 log.append(WorkflowEventType::Submitted, "wf-x", "msg");
288 assert_eq!(log.len(), 1);
289 }
290
291 #[test]
292 fn test_sequence_numbers_monotonic() {
293 let log = make_log();
294 for (i, ev) in log.all().iter().enumerate() {
295 assert_eq!(ev.seq, i as u64);
296 }
297 }
298
299 #[test]
300 fn test_recent_errors_count() {
301 let log = make_log();
302 let errors = log.recent_errors(10);
304 assert_eq!(errors.len(), 2);
305 }
306
307 #[test]
308 fn test_recent_errors_newest_first() {
309 let log = make_log();
310 let errors = log.recent_errors(10);
311 assert!(errors[0].seq > errors[1].seq);
313 }
314
315 #[test]
316 fn test_recent_errors_limit() {
317 let log = make_log();
318 let errors = log.recent_errors(1);
319 assert_eq!(errors.len(), 1);
320 }
321
322 #[test]
323 fn test_events_for_workflow() {
324 let log = make_log();
325 let wf1 = log.events_for_workflow("wf-1");
326 assert_eq!(wf1.len(), 4);
327 let wf2 = log.events_for_workflow("wf-2");
328 assert_eq!(wf2.len(), 1);
329 }
330
331 #[test]
332 fn test_events_of_type() {
333 let log = make_log();
334 let starts = log.events_of_type(WorkflowEventType::TaskStarted);
335 assert_eq!(starts.len(), 1);
336 }
337
338 #[test]
339 fn test_error_count() {
340 let log = make_log();
341 assert_eq!(log.error_count(), 2);
342 }
343
344 #[test]
345 fn test_clear_resets_log() {
346 let mut log = make_log();
347 log.clear();
348 assert!(log.is_empty());
349 assert_eq!(log.error_count(), 0);
350 }
351
352 #[test]
353 fn test_clear_resets_sequence() {
354 let mut log = make_log();
355 log.clear();
356 log.append(WorkflowEventType::Submitted, "wf-new", "msg");
357 assert_eq!(log.all()[0].seq, 0);
358 }
359
360 #[test]
361 fn test_event_type_is_error() {
362 assert!(WorkflowEventType::TaskFailed.is_error());
363 assert!(WorkflowEventType::Error.is_error());
364 assert!(!WorkflowEventType::TaskCompleted.is_error());
365 assert!(!WorkflowEventType::Submitted.is_error());
366 }
367
368 #[test]
369 fn test_event_type_is_success() {
370 assert!(WorkflowEventType::TaskCompleted.is_success());
371 assert!(WorkflowEventType::WorkflowCompleted.is_success());
372 assert!(!WorkflowEventType::TaskFailed.is_success());
373 }
374
375 #[test]
376 fn test_event_type_label() {
377 assert_eq!(WorkflowEventType::Error.label(), "ERROR");
378 assert_eq!(WorkflowEventType::Submitted.label(), "SUBMITTED");
379 }
380
381 #[test]
382 fn test_event_type_display() {
383 assert_eq!(format!("{}", WorkflowEventType::Warning), "WARN");
384 }
385
386 #[test]
387 fn test_task_event_has_task_id() {
388 let log = make_log();
389 let task_ev = log
390 .events_of_type(WorkflowEventType::TaskStarted)
391 .into_iter()
392 .next()
393 .expect("should succeed in test");
394 assert_eq!(task_ev.task_id.as_deref(), Some("task-a"));
395 }
396
397 #[test]
398 fn test_workflow_event_no_task_id() {
399 let log = make_log();
400 let submitted = log
401 .events_of_type(WorkflowEventType::Submitted)
402 .into_iter()
403 .next()
404 .expect("should succeed in test");
405 assert!(submitted.task_id.is_none());
406 }
407}