use once_cell::sync::Lazy;
use prometheus::{
proto::MetricFamily, register_counter_vec, register_gauge_vec, register_histogram_vec,
CounterVec, GaugeVec, HistogramVec, Registry,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub struct ChaosMetrics {
pub scenarios_total: CounterVec,
pub faults_injected_total: CounterVec,
pub latency_injected: HistogramVec,
pub jitter_applied: HistogramVec,
pub bandwidth_throttle_delay: HistogramVec,
pub rate_limit_violations_total: CounterVec,
pub circuit_breaker_state: GaugeVec,
pub bulkhead_concurrent: GaugeVec,
pub orchestration_step_duration: HistogramVec,
pub orchestration_executions_total: CounterVec,
pub active_orchestrations: GaugeVec,
pub assertion_results_total: CounterVec,
pub hook_executions_total: CounterVec,
pub recommendations_total: GaugeVec,
pub chaos_impact_score: GaugeVec,
}
impl ChaosMetrics {
pub fn new() -> Result<Self, prometheus::Error> {
Ok(Self {
scenarios_total: register_counter_vec!(
"mockforge_chaos_scenarios_total",
"Total number of chaos scenarios executed",
&["scenario_type", "status"]
)?,
faults_injected_total: register_counter_vec!(
"mockforge_chaos_faults_total",
"Total number of faults injected",
&["fault_type", "endpoint"]
)?,
latency_injected: register_histogram_vec!(
"mockforge_chaos_latency_ms",
"Latency injected in milliseconds",
&["endpoint"],
vec![10.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 2500.0, 5000.0, 10000.0]
)?,
jitter_applied: register_histogram_vec!(
"mockforge_chaos_jitter_ms",
"Jitter offset applied on top of base latency, in milliseconds (absolute value)",
&["endpoint"],
vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0]
)?,
bandwidth_throttle_delay: register_histogram_vec!(
"mockforge_chaos_bandwidth_throttle_ms",
"Bandwidth-throttle delay added to a transfer, in milliseconds",
&["endpoint", "direction"],
vec![1.0, 10.0, 50.0, 100.0, 500.0, 1000.0, 5000.0, 10000.0]
)?,
rate_limit_violations_total: register_counter_vec!(
"mockforge_chaos_rate_limit_violations_total",
"Total rate limit violations",
&["endpoint"]
)?,
circuit_breaker_state: register_gauge_vec!(
"mockforge_chaos_circuit_breaker_state",
"Circuit breaker state (0=closed, 1=open, 2=half-open)",
&["circuit_name"]
)?,
bulkhead_concurrent: register_gauge_vec!(
"mockforge_chaos_bulkhead_concurrent_requests",
"Current concurrent requests in bulkhead",
&["bulkhead_name"]
)?,
orchestration_step_duration: register_histogram_vec!(
"mockforge_chaos_orchestration_step_duration_seconds",
"Duration of orchestration steps in seconds",
&["orchestration", "step"],
vec![0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0]
)?,
orchestration_executions_total: register_counter_vec!(
"mockforge_chaos_orchestration_executions_total",
"Total orchestration executions",
&["orchestration", "status"]
)?,
active_orchestrations: register_gauge_vec!(
"mockforge_chaos_active_orchestrations",
"Number of active orchestrations",
&["orchestration"]
)?,
assertion_results_total: register_counter_vec!(
"mockforge_chaos_assertion_results_total",
"Total assertion results",
&["orchestration", "result"]
)?,
hook_executions_total: register_counter_vec!(
"mockforge_chaos_hook_executions_total",
"Total hook executions",
&["hook_type", "status"]
)?,
recommendations_total: register_gauge_vec!(
"mockforge_chaos_recommendations_total",
"Number of AI recommendations",
&["category", "severity"]
)?,
chaos_impact_score: register_gauge_vec!(
"mockforge_chaos_impact_score",
"Overall chaos impact score (0.0-1.0)",
&["time_window"]
)?,
})
}
pub fn record_scenario(&self, scenario_type: &str, success: bool) {
self.scenarios_total
.with_label_values(&[scenario_type, if success { "success" } else { "failure" }])
.inc();
}
pub fn record_fault(&self, fault_type: &str, endpoint: &str) {
self.faults_injected_total.with_label_values(&[fault_type, endpoint]).inc();
}
pub fn record_latency(&self, endpoint: &str, latency_ms: f64) {
self.latency_injected.with_label_values(&[endpoint]).observe(latency_ms);
}
pub fn record_jitter(&self, endpoint: &str, jitter_ms: f64) {
self.jitter_applied.with_label_values(&[endpoint]).observe(jitter_ms);
}
pub fn record_bandwidth_throttle(&self, endpoint: &str, direction: &str, delay_ms: f64) {
self.bandwidth_throttle_delay
.with_label_values(&[endpoint, direction])
.observe(delay_ms);
}
pub fn record_rate_limit_violation(&self, endpoint: &str) {
self.rate_limit_violations_total.with_label_values(&[endpoint]).inc();
}
pub fn update_circuit_breaker_state(&self, circuit_name: &str, state: f64) {
self.circuit_breaker_state.with_label_values(&[circuit_name]).set(state);
}
pub fn update_bulkhead_concurrent(&self, bulkhead_name: &str, count: f64) {
self.bulkhead_concurrent.with_label_values(&[bulkhead_name]).set(count);
}
pub fn record_step_duration(&self, orchestration: &str, step: &str, duration_secs: f64) {
self.orchestration_step_duration
.with_label_values(&[orchestration, step])
.observe(duration_secs);
}
pub fn record_orchestration_execution(&self, orchestration: &str, success: bool) {
self.orchestration_executions_total
.with_label_values(&[orchestration, if success { "success" } else { "failure" }])
.inc();
}
pub fn update_active_orchestrations(&self, orchestration: &str, active: bool) {
if active {
self.active_orchestrations.with_label_values(&[orchestration]).inc();
} else {
self.active_orchestrations.with_label_values(&[orchestration]).dec();
}
}
pub fn record_assertion(&self, orchestration: &str, passed: bool) {
self.assertion_results_total
.with_label_values(&[orchestration, if passed { "passed" } else { "failed" }])
.inc();
}
pub fn record_hook(&self, hook_type: &str, success: bool) {
self.hook_executions_total
.with_label_values(&[hook_type, if success { "success" } else { "failure" }])
.inc();
}
pub fn update_recommendations(&self, category: &str, severity: &str, count: f64) {
self.recommendations_total.with_label_values(&[category, severity]).set(count);
}
pub fn update_impact_score(&self, time_window: &str, score: f64) {
self.chaos_impact_score.with_label_values(&[time_window]).set(score);
}
pub fn snapshot(&self) -> ChaosStatsSnapshot {
use prometheus::core::Collector;
let mut faults_by_type: HashMap<String, HashMap<String, u64>> = HashMap::new();
let mut faults_total_by_type: HashMap<String, u64> = HashMap::new();
let mut faults_grand_total: u64 = 0;
for fam in self.faults_injected_total.collect() {
walk_counter(&fam, |labels, count| {
let fault_type =
labels.get("fault_type").cloned().unwrap_or_else(|| "unknown".to_string());
let endpoint =
labels.get("endpoint").cloned().unwrap_or_else(|| "unknown".to_string());
faults_by_type.entry(fault_type.clone()).or_default().insert(endpoint, count);
*faults_total_by_type.entry(fault_type).or_default() += count;
faults_grand_total += count;
});
}
let mut rate_limit_by_endpoint: HashMap<String, u64> = HashMap::new();
let mut rate_limit_total: u64 = 0;
for fam in self.rate_limit_violations_total.collect() {
walk_counter(&fam, |labels, count| {
let endpoint =
labels.get("endpoint").cloned().unwrap_or_else(|| "unknown".to_string());
rate_limit_by_endpoint.insert(endpoint, count);
rate_limit_total += count;
});
}
let mut latency_samples_by_endpoint: HashMap<String, u64> = HashMap::new();
let mut latency_avg_ms_by_endpoint: HashMap<String, f64> = HashMap::new();
for fam in self.latency_injected.collect() {
for m in fam.get_metric() {
let endpoint = m
.get_label()
.iter()
.find(|l| l.name() == "endpoint")
.map(|l| l.value().to_string())
.unwrap_or_else(|| "unknown".to_string());
let hist = m.get_histogram();
let count = hist.sample_count();
latency_samples_by_endpoint.insert(endpoint.clone(), count);
if count > 0 {
latency_avg_ms_by_endpoint.insert(endpoint, hist.sample_sum() / count as f64);
}
}
}
let mut jitter_samples_by_endpoint: HashMap<String, u64> = HashMap::new();
let mut jitter_avg_ms_by_endpoint: HashMap<String, f64> = HashMap::new();
for fam in self.jitter_applied.collect() {
for m in fam.get_metric() {
let endpoint = m
.get_label()
.iter()
.find(|l| l.name() == "endpoint")
.map(|l| l.value().to_string())
.unwrap_or_else(|| "unknown".to_string());
let hist = m.get_histogram();
let count = hist.sample_count();
jitter_samples_by_endpoint.insert(endpoint.clone(), count);
if count > 0 {
jitter_avg_ms_by_endpoint.insert(endpoint, hist.sample_sum() / count as f64);
}
}
}
let mut bandwidth_throttle_samples: HashMap<String, u64> = HashMap::new();
let mut bandwidth_throttle_total_ms: u64 = 0;
for fam in self.bandwidth_throttle_delay.collect() {
for m in fam.get_metric() {
let direction = m
.get_label()
.iter()
.find(|l| l.name() == "direction")
.map(|l| l.value().to_string())
.unwrap_or_else(|| "unknown".to_string());
let hist = m.get_histogram();
let count = hist.sample_count();
*bandwidth_throttle_samples.entry(direction).or_default() += count;
bandwidth_throttle_total_ms += hist.sample_sum() as u64;
}
}
ChaosStatsSnapshot {
faults_by_type,
faults_total_by_type,
faults_grand_total,
rate_limit_violations_by_endpoint: rate_limit_by_endpoint,
rate_limit_violations_total: rate_limit_total,
latency_samples_by_endpoint,
latency_avg_ms_by_endpoint,
jitter_samples_by_endpoint,
jitter_avg_ms_by_endpoint,
bandwidth_throttle_samples_by_direction: bandwidth_throttle_samples,
bandwidth_throttle_total_ms,
}
}
}
fn walk_counter<F>(fam: &MetricFamily, mut visit: F)
where
F: FnMut(HashMap<String, String>, u64),
{
for m in fam.get_metric() {
let labels: HashMap<String, String> = m
.get_label()
.iter()
.map(|l| (l.name().to_string(), l.value().to_string()))
.collect();
let count = m.get_counter().value() as u64;
visit(labels, count);
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ChaosStatsSnapshot {
pub faults_by_type: HashMap<String, HashMap<String, u64>>,
pub faults_total_by_type: HashMap<String, u64>,
pub faults_grand_total: u64,
pub rate_limit_violations_by_endpoint: HashMap<String, u64>,
pub rate_limit_violations_total: u64,
pub latency_samples_by_endpoint: HashMap<String, u64>,
#[serde(default)]
pub latency_avg_ms_by_endpoint: HashMap<String, f64>,
#[serde(default)]
pub jitter_samples_by_endpoint: HashMap<String, u64>,
#[serde(default)]
pub jitter_avg_ms_by_endpoint: HashMap<String, f64>,
#[serde(default)]
pub bandwidth_throttle_samples_by_direction: HashMap<String, u64>,
#[serde(default)]
pub bandwidth_throttle_total_ms: u64,
}
impl Default for ChaosMetrics {
fn default() -> Self {
Self::new().expect("Failed to create chaos metrics")
}
}
pub static CHAOS_METRICS: Lazy<ChaosMetrics> =
Lazy::new(|| ChaosMetrics::new().expect("Failed to initialize chaos metrics"));
pub fn registry() -> &'static Registry {
prometheus::default_registry()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_creation() {
let _metrics = &*CHAOS_METRICS;
}
#[test]
fn test_record_scenario() {
let metrics = CHAOS_METRICS.scenarios_total.clone();
let before = metrics.with_label_values(&["test", "success"]).get();
CHAOS_METRICS.record_scenario("test", true);
let after = metrics.with_label_values(&["test", "success"]).get();
assert!(after > before);
}
#[test]
fn test_record_latency() {
CHAOS_METRICS.record_latency("/api/test", 100.0);
}
#[test]
fn snapshot_reflects_counter_increments() {
let endpoint = "/api/test_snapshot_endpoint_unique_xyz";
let baseline = CHAOS_METRICS.snapshot();
let baseline_count = baseline
.faults_by_type
.get("http_error")
.and_then(|m| m.get(endpoint))
.copied()
.unwrap_or(0);
CHAOS_METRICS.record_fault("http_error", endpoint);
CHAOS_METRICS.record_fault("http_error", endpoint);
CHAOS_METRICS.record_rate_limit_violation(endpoint);
CHAOS_METRICS.record_latency(endpoint, 42.0);
let snap = CHAOS_METRICS.snapshot();
assert_eq!(
snap.faults_by_type
.get("http_error")
.and_then(|m| m.get(endpoint))
.copied()
.unwrap_or(0),
baseline_count + 2,
"fault count for {endpoint} did not advance by 2"
);
assert!(
snap.faults_total_by_type.get("http_error").copied().unwrap_or(0) >= 2,
"faults_total_by_type[http_error] should reflect the inc"
);
assert!(
snap.rate_limit_violations_by_endpoint.get(endpoint).copied().unwrap_or(0) >= 1,
"rate_limit_violations_by_endpoint did not record"
);
assert!(
snap.latency_samples_by_endpoint.get(endpoint).copied().unwrap_or(0) >= 1,
"latency histogram count did not record"
);
}
}