mockforge_chaos/
scenario_recorder.rs

1//! Scenario recording and replay system
2
3use 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/// A recorded chaos event
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ChaosEvent {
16    /// Event timestamp
17    pub timestamp: DateTime<Utc>,
18    /// Event type
19    pub event_type: ChaosEventType,
20    /// Event metadata
21    pub metadata: HashMap<String, String>,
22}
23
24/// Types of chaos events
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(tag = "type")]
27pub enum ChaosEventType {
28    /// Latency injection event
29    LatencyInjection {
30        delay_ms: u64,
31        endpoint: Option<String>,
32    },
33    /// Fault injection event
34    FaultInjection {
35        fault_type: String,
36        endpoint: Option<String>,
37    },
38    /// Rate limit event
39    RateLimitExceeded {
40        client_ip: Option<String>,
41        endpoint: Option<String>,
42    },
43    /// Traffic shaping event
44    TrafficShaping { action: String, bytes: usize },
45    /// Protocol-specific event
46    ProtocolEvent {
47        protocol: String,
48        event: String,
49        details: HashMap<String, String>,
50    },
51    /// Scenario transition
52    ScenarioTransition {
53        from_scenario: Option<String>,
54        to_scenario: String,
55    },
56}
57
58/// Recorded scenario with events
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct RecordedScenario {
61    /// Scenario metadata
62    pub scenario: ChaosScenario,
63    /// Recorded events
64    pub events: Vec<ChaosEvent>,
65    /// Recording start time
66    pub recording_started: DateTime<Utc>,
67    /// Recording end time
68    pub recording_ended: Option<DateTime<Utc>>,
69    /// Total duration in milliseconds
70    pub total_duration_ms: u64,
71}
72
73impl RecordedScenario {
74    /// Create a new recorded scenario
75    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    /// Add an event to the recording
86    pub fn add_event(&mut self, event: ChaosEvent) {
87        self.events.push(event);
88    }
89
90    /// Finish recording
91    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    /// Get events within a time range
101    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    /// Export to JSON
109    pub fn to_json(&self) -> Result<String, serde_json::Error> {
110        serde_json::to_string_pretty(self)
111    }
112
113    /// Export to YAML
114    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
115        serde_yaml::to_string(self)
116    }
117
118    /// Import from JSON
119    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
120        serde_json::from_str(json)
121    }
122
123    /// Import from YAML
124    pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
125        serde_yaml::from_str(yaml)
126    }
127
128    /// Save to file
129    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    /// Load from file
146    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
163/// Scenario recorder
164pub struct ScenarioRecorder {
165    /// Current recording
166    current_recording: Arc<RwLock<Option<RecordedScenario>>>,
167    /// Completed recordings
168    recordings: Arc<RwLock<Vec<RecordedScenario>>>,
169    /// Maximum events to record (0 = unlimited)
170    max_events: usize,
171}
172
173impl ScenarioRecorder {
174    /// Create a new scenario recorder
175    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    /// Set maximum events to record
184    pub fn with_max_events(mut self, max: usize) -> Self {
185        self.max_events = max;
186        self
187    }
188
189    /// Start recording a scenario
190    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    /// Stop recording
203    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            // Store in completed recordings
216            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    /// Record an event
226    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            // Check max events limit
231            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    /// Check if recording is in progress
242    pub fn is_recording(&self) -> bool {
243        self.current_recording.read().is_some()
244    }
245
246    /// Get current recording (read-only)
247    pub fn get_current_recording(&self) -> Option<RecordedScenario> {
248        self.current_recording.read().clone()
249    }
250
251    /// Get all completed recordings
252    pub fn get_recordings(&self) -> Vec<RecordedScenario> {
253        self.recordings.read().clone()
254    }
255
256    /// Get recording by scenario name
257    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    /// Clear all recordings
262    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}