1use crate::scenarios::ChaosScenario;
4use chrono::{DateTime, Utc};
5use parking_lot::RwLock;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fs;
9use std::path::Path;
10use std::sync::Arc;
11use tracing::{debug, info, warn};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ChaosEvent {
16 pub timestamp: DateTime<Utc>,
18 pub event_type: ChaosEventType,
20 pub metadata: HashMap<String, String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(tag = "type")]
27pub enum ChaosEventType {
28 LatencyInjection {
30 delay_ms: u64,
31 endpoint: Option<String>,
32 },
33 FaultInjection {
35 fault_type: String,
36 endpoint: Option<String>,
37 },
38 RateLimitExceeded {
40 client_ip: Option<String>,
41 endpoint: Option<String>,
42 },
43 TrafficShaping { action: String, bytes: usize },
45 ProtocolEvent {
47 protocol: String,
48 event: String,
49 details: HashMap<String, String>,
50 },
51 ScenarioTransition {
53 from_scenario: Option<String>,
54 to_scenario: String,
55 },
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct RecordedScenario {
61 pub scenario: ChaosScenario,
63 pub events: Vec<ChaosEvent>,
65 pub recording_started: DateTime<Utc>,
67 pub recording_ended: Option<DateTime<Utc>>,
69 pub total_duration_ms: u64,
71}
72
73impl RecordedScenario {
74 pub fn new(scenario: ChaosScenario) -> Self {
76 Self {
77 scenario,
78 events: Vec::new(),
79 recording_started: Utc::now(),
80 recording_ended: None,
81 total_duration_ms: 0,
82 }
83 }
84
85 pub fn add_event(&mut self, event: ChaosEvent) {
87 self.events.push(event);
88 }
89
90 pub fn finish(&mut self) {
92 self.recording_ended = Some(Utc::now());
93 self.total_duration_ms = self
94 .recording_ended
95 .unwrap()
96 .signed_duration_since(self.recording_started)
97 .num_milliseconds() as u64;
98 }
99
100 pub fn events_in_range(&self, start: DateTime<Utc>, end: DateTime<Utc>) -> Vec<&ChaosEvent> {
102 self.events
103 .iter()
104 .filter(|e| e.timestamp >= start && e.timestamp <= end)
105 .collect()
106 }
107
108 pub fn to_json(&self) -> Result<String, serde_json::Error> {
110 serde_json::to_string_pretty(self)
111 }
112
113 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
115 serde_yaml::to_string(self)
116 }
117
118 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
120 serde_json::from_str(json)
121 }
122
123 pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
125 serde_yaml::from_str(yaml)
126 }
127
128 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
130 let path = path.as_ref();
131 let extension = path.extension().and_then(|s| s.to_str());
132
133 let content = match extension {
134 Some("yaml") | Some("yml") => {
135 self.to_yaml().map_err(|e| std::io::Error::other(e.to_string()))?
136 }
137 _ => self.to_json().map_err(|e| std::io::Error::other(e.to_string()))?,
138 };
139
140 fs::write(path, content)?;
141 info!("Saved recorded scenario to: {}", path.display());
142 Ok(())
143 }
144
145 pub fn load_from_file<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
147 let path = path.as_ref();
148 let content = fs::read_to_string(path)?;
149 let extension = path.extension().and_then(|s| s.to_str());
150
151 let scenario = match extension {
152 Some("yaml") | Some("yml") => {
153 Self::from_yaml(&content).map_err(|e| std::io::Error::other(e.to_string()))?
154 }
155 _ => Self::from_json(&content).map_err(|e| std::io::Error::other(e.to_string()))?,
156 };
157
158 info!("Loaded recorded scenario from: {}", path.display());
159 Ok(scenario)
160 }
161}
162
163pub struct ScenarioRecorder {
165 current_recording: Arc<RwLock<Option<RecordedScenario>>>,
167 recordings: Arc<RwLock<Vec<RecordedScenario>>>,
169 max_events: usize,
171}
172
173impl ScenarioRecorder {
174 pub fn new() -> Self {
176 Self {
177 current_recording: Arc::new(RwLock::new(None)),
178 recordings: Arc::new(RwLock::new(Vec::new())),
179 max_events: 10000,
180 }
181 }
182
183 pub fn with_max_events(mut self, max: usize) -> Self {
185 self.max_events = max;
186 self
187 }
188
189 pub fn start_recording(&self, scenario: ChaosScenario) -> Result<(), String> {
191 let mut current = self.current_recording.write();
192
193 if current.is_some() {
194 return Err("Recording already in progress".to_string());
195 }
196
197 info!("Started recording scenario: {}", scenario.name);
198 *current = Some(RecordedScenario::new(scenario));
199 Ok(())
200 }
201
202 pub fn stop_recording(&self) -> Result<RecordedScenario, String> {
204 let mut current = self.current_recording.write();
205
206 if let Some(mut recording) = current.take() {
207 recording.finish();
208 info!(
209 "Stopped recording scenario: {} ({} events, {}ms)",
210 recording.scenario.name,
211 recording.events.len(),
212 recording.total_duration_ms
213 );
214
215 let mut recordings = self.recordings.write();
217 recordings.push(recording.clone());
218
219 Ok(recording)
220 } else {
221 Err("No recording in progress".to_string())
222 }
223 }
224
225 pub fn record_event(&self, event: ChaosEvent) {
227 let mut current = self.current_recording.write();
228
229 if let Some(recording) = current.as_mut() {
230 if self.max_events > 0 && recording.events.len() >= self.max_events {
232 warn!("Max events limit ({}) reached, stopping recording", self.max_events);
233 return;
234 }
235
236 recording.add_event(event);
237 debug!("Recorded event (total: {})", recording.events.len());
238 }
239 }
240
241 pub fn is_recording(&self) -> bool {
243 self.current_recording.read().is_some()
244 }
245
246 pub fn get_current_recording(&self) -> Option<RecordedScenario> {
248 self.current_recording.read().clone()
249 }
250
251 pub fn get_recordings(&self) -> Vec<RecordedScenario> {
253 self.recordings.read().clone()
254 }
255
256 pub fn get_recording_by_name(&self, name: &str) -> Option<RecordedScenario> {
258 self.recordings.read().iter().find(|r| r.scenario.name == name).cloned()
259 }
260
261 pub fn clear_recordings(&self) {
263 let mut recordings = self.recordings.write();
264 recordings.clear();
265 info!("Cleared all recordings");
266 }
267}
268
269impl Default for ScenarioRecorder {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_recorded_scenario_creation() {
281 let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
282 let recording = RecordedScenario::new(scenario);
283
284 assert_eq!(recording.scenario.name, "test");
285 assert_eq!(recording.events.len(), 0);
286 assert!(recording.recording_ended.is_none());
287 }
288
289 #[test]
290 fn test_add_event() {
291 let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
292 let mut recording = RecordedScenario::new(scenario);
293
294 let event = ChaosEvent {
295 timestamp: Utc::now(),
296 event_type: ChaosEventType::LatencyInjection {
297 delay_ms: 100,
298 endpoint: Some("/api/test".to_string()),
299 },
300 metadata: HashMap::new(),
301 };
302
303 recording.add_event(event);
304 assert_eq!(recording.events.len(), 1);
305 }
306
307 #[test]
308 fn test_finish_recording() {
309 let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
310 let mut recording = RecordedScenario::new(scenario);
311
312 std::thread::sleep(std::time::Duration::from_millis(10));
313 recording.finish();
314
315 assert!(recording.recording_ended.is_some());
316 assert!(recording.total_duration_ms >= 10);
317 }
318
319 #[test]
320 fn test_recorder_start_stop() {
321 let recorder = ScenarioRecorder::new();
322 let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
323
324 assert!(!recorder.is_recording());
325
326 recorder.start_recording(scenario).unwrap();
327 assert!(recorder.is_recording());
328
329 let recording = recorder.stop_recording().unwrap();
330 assert!(!recorder.is_recording());
331 assert_eq!(recording.scenario.name, "test");
332 }
333
334 #[test]
335 fn test_record_event() {
336 let recorder = ScenarioRecorder::new();
337 let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
338
339 recorder.start_recording(scenario).unwrap();
340
341 let event = ChaosEvent {
342 timestamp: Utc::now(),
343 event_type: ChaosEventType::LatencyInjection {
344 delay_ms: 100,
345 endpoint: None,
346 },
347 metadata: HashMap::new(),
348 };
349
350 recorder.record_event(event);
351
352 let current = recorder.get_current_recording().unwrap();
353 assert_eq!(current.events.len(), 1);
354 }
355
356 #[test]
357 fn test_json_export_import() {
358 let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
359 let mut recording = RecordedScenario::new(scenario);
360
361 let event = ChaosEvent {
362 timestamp: Utc::now(),
363 event_type: ChaosEventType::LatencyInjection {
364 delay_ms: 100,
365 endpoint: Some("/test".to_string()),
366 },
367 metadata: HashMap::new(),
368 };
369
370 recording.add_event(event);
371 recording.finish();
372
373 let json = recording.to_json().unwrap();
374 let imported = RecordedScenario::from_json(&json).unwrap();
375
376 assert_eq!(imported.scenario.name, "test");
377 assert_eq!(imported.events.len(), 1);
378 }
379}