use crate::scenarios::ChaosScenario;
use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChaosEvent {
pub timestamp: DateTime<Utc>,
pub event_type: ChaosEventType,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ChaosEventType {
LatencyInjection {
delay_ms: u64,
endpoint: Option<String>,
},
FaultInjection {
fault_type: String,
endpoint: Option<String>,
},
RateLimitExceeded {
client_ip: Option<String>,
endpoint: Option<String>,
},
TrafficShaping { action: String, bytes: usize },
ProtocolEvent {
protocol: String,
event: String,
details: HashMap<String, String>,
},
ScenarioTransition {
from_scenario: Option<String>,
to_scenario: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordedScenario {
pub scenario: ChaosScenario,
pub events: Vec<ChaosEvent>,
pub recording_started: DateTime<Utc>,
pub recording_ended: Option<DateTime<Utc>>,
pub total_duration_ms: u64,
}
impl RecordedScenario {
pub fn new(scenario: ChaosScenario) -> Self {
Self {
scenario,
events: Vec::new(),
recording_started: Utc::now(),
recording_ended: None,
total_duration_ms: 0,
}
}
pub fn add_event(&mut self, event: ChaosEvent) {
self.events.push(event);
}
pub fn finish(&mut self) {
self.recording_ended = Some(Utc::now());
self.total_duration_ms = self
.recording_ended
.unwrap()
.signed_duration_since(self.recording_started)
.num_milliseconds() as u64;
}
pub fn events_in_range(&self, start: DateTime<Utc>, end: DateTime<Utc>) -> Vec<&ChaosEvent> {
self.events
.iter()
.filter(|e| e.timestamp >= start && e.timestamp <= end)
.collect()
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
serde_yaml::from_str(yaml)
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
let path = path.as_ref();
let extension = path.extension().and_then(|s| s.to_str());
let content = match extension {
Some("yaml") | Some("yml") => {
self.to_yaml().map_err(|e| std::io::Error::other(e.to_string()))?
}
_ => self.to_json().map_err(|e| std::io::Error::other(e.to_string()))?,
};
fs::write(path, content)?;
info!("Saved recorded scenario to: {}", path.display());
Ok(())
}
pub fn load_from_file<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
let path = path.as_ref();
let content = fs::read_to_string(path)?;
let extension = path.extension().and_then(|s| s.to_str());
let scenario = match extension {
Some("yaml") | Some("yml") => {
Self::from_yaml(&content).map_err(|e| std::io::Error::other(e.to_string()))?
}
_ => Self::from_json(&content).map_err(|e| std::io::Error::other(e.to_string()))?,
};
info!("Loaded recorded scenario from: {}", path.display());
Ok(scenario)
}
}
pub struct ScenarioRecorder {
current_recording: Arc<RwLock<Option<RecordedScenario>>>,
recordings: Arc<RwLock<Vec<RecordedScenario>>>,
max_events: usize,
}
impl ScenarioRecorder {
pub fn new() -> Self {
Self {
current_recording: Arc::new(RwLock::new(None)),
recordings: Arc::new(RwLock::new(Vec::new())),
max_events: 10000,
}
}
pub fn with_max_events(mut self, max: usize) -> Self {
self.max_events = max;
self
}
pub fn start_recording(&self, scenario: ChaosScenario) -> Result<(), String> {
let mut current = self.current_recording.write();
if current.is_some() {
return Err("Recording already in progress".to_string());
}
info!("Started recording scenario: {}", scenario.name);
*current = Some(RecordedScenario::new(scenario));
Ok(())
}
pub fn stop_recording(&self) -> Result<RecordedScenario, String> {
let mut current = self.current_recording.write();
if let Some(mut recording) = current.take() {
recording.finish();
info!(
"Stopped recording scenario: {} ({} events, {}ms)",
recording.scenario.name,
recording.events.len(),
recording.total_duration_ms
);
let mut recordings = self.recordings.write();
recordings.push(recording.clone());
Ok(recording)
} else {
Err("No recording in progress".to_string())
}
}
pub fn record_event(&self, event: ChaosEvent) {
let mut current = self.current_recording.write();
if let Some(recording) = current.as_mut() {
if self.max_events > 0 && recording.events.len() >= self.max_events {
warn!("Max events limit ({}) reached, stopping recording", self.max_events);
return;
}
recording.add_event(event);
debug!("Recorded event (total: {})", recording.events.len());
}
}
pub fn is_recording(&self) -> bool {
self.current_recording.read().is_some()
}
pub fn get_current_recording(&self) -> Option<RecordedScenario> {
self.current_recording.read().clone()
}
pub fn get_recordings(&self) -> Vec<RecordedScenario> {
self.recordings.read().clone()
}
pub fn get_recording_by_name(&self, name: &str) -> Option<RecordedScenario> {
self.recordings.read().iter().find(|r| r.scenario.name == name).cloned()
}
pub fn clear_recordings(&self) {
let mut recordings = self.recordings.write();
recordings.clear();
info!("Cleared all recordings");
}
}
impl Default for ScenarioRecorder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recorded_scenario_creation() {
let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
let recording = RecordedScenario::new(scenario);
assert_eq!(recording.scenario.name, "test");
assert_eq!(recording.events.len(), 0);
assert!(recording.recording_ended.is_none());
}
#[test]
fn test_add_event() {
let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
let mut recording = RecordedScenario::new(scenario);
let event = ChaosEvent {
timestamp: Utc::now(),
event_type: ChaosEventType::LatencyInjection {
delay_ms: 100,
endpoint: Some("/api/test".to_string()),
},
metadata: HashMap::new(),
};
recording.add_event(event);
assert_eq!(recording.events.len(), 1);
}
#[test]
fn test_finish_recording() {
let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
let mut recording = RecordedScenario::new(scenario);
std::thread::sleep(std::time::Duration::from_millis(10));
recording.finish();
assert!(recording.recording_ended.is_some());
assert!(recording.total_duration_ms >= 10);
}
#[test]
fn test_recorder_start_stop() {
let recorder = ScenarioRecorder::new();
let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
assert!(!recorder.is_recording());
recorder.start_recording(scenario).unwrap();
assert!(recorder.is_recording());
let recording = recorder.stop_recording().unwrap();
assert!(!recorder.is_recording());
assert_eq!(recording.scenario.name, "test");
}
#[test]
fn test_record_event() {
let recorder = ScenarioRecorder::new();
let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
recorder.start_recording(scenario).unwrap();
let event = ChaosEvent {
timestamp: Utc::now(),
event_type: ChaosEventType::LatencyInjection {
delay_ms: 100,
endpoint: None,
},
metadata: HashMap::new(),
};
recorder.record_event(event);
let current = recorder.get_current_recording().unwrap();
assert_eq!(current.events.len(), 1);
}
#[test]
fn test_json_export_import() {
let scenario = ChaosScenario::new("test", crate::ChaosConfig::default());
let mut recording = RecordedScenario::new(scenario);
let event = ChaosEvent {
timestamp: Utc::now(),
event_type: ChaosEventType::LatencyInjection {
delay_ms: 100,
endpoint: Some("/test".to_string()),
},
metadata: HashMap::new(),
};
recording.add_event(event);
recording.finish();
let json = recording.to_json().unwrap();
let imported = RecordedScenario::from_json(&json).unwrap();
assert_eq!(imported.scenario.name, "test");
assert_eq!(imported.events.len(), 1);
}
}