1use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use uuid::Uuid;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct TracedEvent {
18 pub id: Uuid,
20 pub timestamp: DateTime<Utc>,
22 pub correlation_id: Uuid,
25 pub parent_id: Option<Uuid>,
27 pub category: EventCategory,
29 pub action: String,
31 pub fields: HashMap<String, serde_json::Value>,
33 pub node_id: Option<u64>,
35 pub duration_ms: Option<u64>,
37 pub level: EventLevel,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43#[serde(rename_all = "snake_case")]
44pub enum EventCategory {
45 Job,
47 Allocation,
49 Task,
51 Node,
53 Scheduling,
55 Network,
57 Health,
59 Security,
61 Build,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
67#[serde(rename_all = "snake_case")]
68pub enum EventLevel {
69 #[default]
70 Info,
71 Warning,
72 Error,
73 Debug,
74}
75
76impl TracedEvent {
77 pub fn new(correlation_id: Uuid, category: EventCategory, action: impl Into<String>) -> Self {
79 Self {
80 id: Uuid::new_v4(),
81 timestamp: Utc::now(),
82 correlation_id,
83 parent_id: None,
84 category,
85 action: action.into(),
86 fields: HashMap::new(),
87 node_id: None,
88 duration_ms: None,
89 level: EventLevel::Info,
90 }
91 }
92
93 pub fn with_parent(mut self, parent_id: Uuid) -> Self {
95 self.parent_id = Some(parent_id);
96 self
97 }
98
99 pub fn with_node(mut self, node_id: u64) -> Self {
101 self.node_id = Some(node_id);
102 self
103 }
104
105 pub fn with_duration(mut self, ms: u64) -> Self {
107 self.duration_ms = Some(ms);
108 self
109 }
110
111 pub fn with_level(mut self, level: EventLevel) -> Self {
113 self.level = level;
114 self
115 }
116
117 pub fn field(mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
119 self.fields.insert(key.into(), value.into());
120 self
121 }
122
123 pub fn allocation_phase(
125 correlation_id: Uuid,
126 alloc_id: Uuid,
127 from_phase: &str,
128 to_phase: &str,
129 ) -> Self {
130 Self::new(
131 correlation_id,
132 EventCategory::Allocation,
133 "phase_transition",
134 )
135 .field("alloc_id", alloc_id.to_string())
136 .field("from_phase", from_phase)
137 .field("to_phase", to_phase)
138 }
139
140 pub fn scheduling_decision(
142 correlation_id: Uuid,
143 job_id: &str,
144 node_id: u64,
145 driver: &str,
146 ) -> Self {
147 Self::new(
148 correlation_id,
149 EventCategory::Scheduling,
150 "allocation_placed",
151 )
152 .field("job_id", job_id)
153 .field("node_id", serde_json::Value::Number(node_id.into()))
154 .field("driver", driver)
155 }
156
157 pub fn health_check(
159 correlation_id: Uuid,
160 service_name: &str,
161 healthy: bool,
162 latency_ms: u64,
163 ) -> Self {
164 Self::new(correlation_id, EventCategory::Health, "probe_result")
165 .field("service_name", service_name)
166 .field("healthy", healthy)
167 .with_duration(latency_ms)
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct CorrelationContext {
179 pub correlation_id: Uuid,
181 pub job_id: String,
183 pub event_count: u64,
185 pub last_event_id: Option<Uuid>,
187}
188
189impl CorrelationContext {
190 pub fn new(job_id: &str) -> Self {
191 Self {
192 correlation_id: Uuid::new_v4(),
193 job_id: job_id.to_string(),
194 event_count: 0,
195 last_event_id: None,
196 }
197 }
198
199 pub fn emit(&mut self, category: EventCategory, action: impl Into<String>) -> TracedEvent {
201 let event = TracedEvent::new(self.correlation_id, category, action);
202 let event = if let Some(parent) = self.last_event_id {
203 event.with_parent(parent)
204 } else {
205 event
206 };
207 self.last_event_id = Some(event.id);
208 self.event_count += 1;
209 event
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_traced_event_creation() {
219 let corr_id = Uuid::new_v4();
220 let event = TracedEvent::new(corr_id, EventCategory::Job, "submitted")
221 .field("job_id", "web-service")
222 .with_node(42);
223
224 assert_eq!(event.correlation_id, corr_id);
225 assert_eq!(event.category, EventCategory::Job);
226 assert_eq!(event.action, "submitted");
227 assert_eq!(event.node_id, Some(42));
228 assert_eq!(event.fields["job_id"], "web-service");
229 }
230
231 #[test]
232 fn test_correlation_context() {
233 let mut ctx = CorrelationContext::new("web-service");
234
235 let e1 = ctx.emit(EventCategory::Job, "submitted");
236 assert!(e1.parent_id.is_none());
237 assert_eq!(ctx.event_count, 1);
238
239 let e2 = ctx.emit(EventCategory::Scheduling, "placed");
240 assert_eq!(e2.parent_id, Some(e1.id));
241 assert_eq!(ctx.event_count, 2);
242
243 let e3 = ctx.emit(EventCategory::Allocation, "warming");
244 assert_eq!(e3.parent_id, Some(e2.id));
245 assert_eq!(e3.correlation_id, e1.correlation_id);
246 }
247
248 #[test]
249 fn test_convenience_constructors() {
250 let corr = Uuid::new_v4();
251 let alloc = Uuid::new_v4();
252
253 let phase = TracedEvent::allocation_phase(corr, alloc, "warming", "executing");
254 assert_eq!(phase.fields["from_phase"], "warming");
255 assert_eq!(phase.fields["to_phase"], "executing");
256
257 let sched = TracedEvent::scheduling_decision(corr, "web", 42, "wasi");
258 assert_eq!(sched.fields["driver"], "wasi");
259
260 let health = TracedEvent::health_check(corr, "web", true, 5);
261 assert_eq!(health.duration_ms, Some(5));
262 }
263
264 #[test]
265 fn test_serde_roundtrip() {
266 let event = TracedEvent::new(Uuid::new_v4(), EventCategory::Network, "flow_detected")
267 .field("bytes", 1024)
268 .with_level(EventLevel::Debug);
269
270 let json = serde_json::to_string(&event).unwrap();
271 let back: TracedEvent = serde_json::from_str(&json).unwrap();
272 assert_eq!(back.category, EventCategory::Network);
273 assert_eq!(back.level, EventLevel::Debug);
274 }
275}