use bevy::prelude::*;
use std::collections::{HashMap, VecDeque};
use super::types::{
AggregatedMetric, AggregationType, MetricDefinition, MetricId, MetricReport, MetricSnapshot,
MetricValue,
};
#[derive(Resource, Clone, Reflect)]
#[reflect(Resource)]
pub struct MetricsConfig {
pub max_values_per_metric: usize,
pub enable_periodic_snapshots: bool,
pub snapshot_period: u32,
pub enable_auto_report: bool,
pub report_period: u32,
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
max_values_per_metric: 1000,
enable_periodic_snapshots: false,
snapshot_period: 1,
enable_auto_report: false,
report_period: 7,
}
}
}
#[derive(Resource, Reflect)]
#[reflect(Resource)]
pub struct MetricsRegistry {
#[reflect(ignore)]
definitions: HashMap<MetricId, MetricDefinition>,
#[reflect(ignore)]
values: HashMap<MetricId, VecDeque<MetricValue>>,
config: MetricsConfig,
}
impl MetricsRegistry {
pub fn new() -> Self {
Self::with_config(MetricsConfig::default())
}
pub fn with_config(config: MetricsConfig) -> Self {
Self {
definitions: HashMap::new(),
values: HashMap::new(),
config,
}
}
pub fn define(&mut self, definition: MetricDefinition) {
self.definitions
.insert(definition.metric_id.clone(), definition);
}
pub fn record(&mut self, value: MetricValue) -> Result<(), String> {
if !self.definitions.contains_key(&value.metric_id) {
return Err(format!("Metric not defined: {:?}", value.metric_id));
}
let values = self.values.entry(value.metric_id.clone()).or_default();
values.push_back(value);
while values.len() > self.config.max_values_per_metric {
values.pop_front();
}
Ok(())
}
pub fn remove(&mut self, metric_id: &MetricId) {
self.definitions.remove(metric_id);
self.values.remove(metric_id);
}
pub fn clear(&mut self) {
self.definitions.clear();
self.values.clear();
}
pub fn all_values(&self) -> Vec<(MetricId, Vec<MetricValue>)> {
self.values
.iter()
.map(|(id, values)| (id.clone(), values.iter().cloned().collect()))
.collect()
}
pub fn get_definition(&self, metric_id: &MetricId) -> Option<&MetricDefinition> {
self.definitions.get(metric_id)
}
pub fn all_definitions(&self) -> Vec<&MetricDefinition> {
self.definitions.values().collect()
}
pub fn get_values(&self, metric_id: &MetricId) -> Option<&VecDeque<MetricValue>> {
self.values.get(metric_id)
}
pub fn aggregate(
&self,
metric_id: &MetricId,
aggregation: AggregationType,
period_start: u64,
period_end: u64,
) -> Option<AggregatedMetric> {
let values = self.values.get(metric_id)?;
let filtered: Vec<&MetricValue> = values
.iter()
.filter(|v| v.timestamp >= period_start && v.timestamp <= period_end)
.collect();
if filtered.is_empty() {
return None;
}
let numeric_values: Vec<f64> = filtered.iter().map(|v| v.value).collect();
let value = match aggregation {
AggregationType::Sum => numeric_values.iter().sum(),
AggregationType::Count => numeric_values.len() as f64,
AggregationType::Average => {
numeric_values.iter().sum::<f64>() / numeric_values.len() as f64
}
AggregationType::Min => numeric_values.iter().cloned().fold(f64::INFINITY, f64::min),
AggregationType::Max => numeric_values
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max),
AggregationType::P50 => calculate_percentile(&numeric_values, 50.0),
AggregationType::P95 => calculate_percentile(&numeric_values, 95.0),
AggregationType::P99 => calculate_percentile(&numeric_values, 99.0),
AggregationType::Last => *numeric_values.last().unwrap_or(&0.0),
AggregationType::Rate => {
let period_seconds = period_end.saturating_sub(period_start);
if period_seconds == 0 {
0.0
} else {
numeric_values.len() as f64 / period_seconds as f64
}
}
};
Some(AggregatedMetric::new(
metric_id.clone(),
aggregation,
value,
period_start,
period_end,
filtered.len(),
))
}
}
impl Default for MetricsRegistry {
fn default() -> Self {
Self::new()
}
}
fn calculate_percentile(values: &[f64], percentile: f64) -> f64 {
if values.is_empty() {
return 0.0;
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let index = (sorted.len() as f64 * percentile / 100.0).ceil() as usize;
let clamped_index = index.clamp(1, sorted.len()) - 1;
sorted[clamped_index]
}
#[derive(Resource, Default, Reflect)]
#[reflect(Resource)]
pub struct MetricsHistory {
#[reflect(ignore)]
snapshots: VecDeque<MetricSnapshot>,
#[reflect(ignore)]
reports: VecDeque<MetricReport>,
}
impl MetricsHistory {
pub fn new() -> Self {
Self::default()
}
pub fn add_snapshot(&mut self, snapshot: MetricSnapshot) {
self.snapshots.push_back(snapshot);
while self.snapshots.len() > 100 {
self.snapshots.pop_front();
}
}
pub fn add_report(&mut self, report: MetricReport) {
self.reports.push_back(report);
while self.reports.len() > 50 {
self.reports.pop_front();
}
}
pub fn snapshots(&self) -> &VecDeque<MetricSnapshot> {
&self.snapshots
}
pub fn reports(&self) -> &VecDeque<MetricReport> {
&self.reports
}
pub fn latest_snapshot(&self) -> Option<&MetricSnapshot> {
self.snapshots.back()
}
pub fn latest_report(&self) -> Option<&MetricReport> {
self.reports.back()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugins::metrics::types::MetricType;
#[test]
fn test_registry_define_and_record() {
let mut registry = MetricsRegistry::new();
let definition =
MetricDefinition::new(MetricId::new("fps"), MetricType::Gauge, "Frames per second");
registry.define(definition);
let value = MetricValue::new(MetricId::new("fps"), 60.0, 1000);
assert!(registry.record(value).is_ok());
let values = registry.get_values(&MetricId::new("fps")).unwrap();
assert_eq!(values.len(), 1);
assert_eq!(values[0].value, 60.0);
}
#[test]
fn test_registry_record_undefined_metric() {
let mut registry = MetricsRegistry::new();
let value = MetricValue::new(MetricId::new("fps"), 60.0, 1000);
assert!(registry.record(value).is_err());
}
#[test]
fn test_registry_windowed_storage() {
let config = MetricsConfig {
max_values_per_metric: 3,
..Default::default()
};
let mut registry = MetricsRegistry::with_config(config);
let definition =
MetricDefinition::new(MetricId::new("fps"), MetricType::Gauge, "Frames per second");
registry.define(definition);
for i in 0..5 {
let value = MetricValue::new(MetricId::new("fps"), i as f64, 1000 + i);
registry.record(value).unwrap();
}
let values = registry.get_values(&MetricId::new("fps")).unwrap();
assert_eq!(values.len(), 3);
assert_eq!(values[0].value, 2.0); assert_eq!(values[2].value, 4.0); }
#[test]
fn test_registry_aggregate_sum() {
let mut registry = MetricsRegistry::new();
let definition =
MetricDefinition::new(MetricId::new("damage"), MetricType::Counter, "Total damage");
registry.define(definition);
for i in 1..=5 {
registry
.record(MetricValue::new(
MetricId::new("damage"),
i as f64,
1000 + i,
))
.unwrap();
}
let aggregated = registry
.aggregate(&MetricId::new("damage"), AggregationType::Sum, 1000, 2000)
.unwrap();
assert_eq!(aggregated.value, 15.0); assert_eq!(aggregated.sample_count, 5);
}
#[test]
fn test_registry_aggregate_average() {
let mut registry = MetricsRegistry::new();
let definition =
MetricDefinition::new(MetricId::new("fps"), MetricType::Gauge, "Frames per second");
registry.define(definition);
registry
.record(MetricValue::new(MetricId::new("fps"), 50.0, 1000))
.unwrap();
registry
.record(MetricValue::new(MetricId::new("fps"), 60.0, 1001))
.unwrap();
registry
.record(MetricValue::new(MetricId::new("fps"), 70.0, 1002))
.unwrap();
let aggregated = registry
.aggregate(&MetricId::new("fps"), AggregationType::Average, 1000, 2000)
.unwrap();
assert_eq!(aggregated.value, 60.0); }
#[test]
fn test_registry_aggregate_percentile() {
let mut registry = MetricsRegistry::new();
let definition = MetricDefinition::new(
MetricId::new("latency"),
MetricType::Histogram,
"Request latency",
);
registry.define(definition);
for i in 1..=10 {
registry
.record(MetricValue::new(
MetricId::new("latency"),
i as f64 * 10.0,
1000 + i,
))
.unwrap();
}
let p50 = registry
.aggregate(&MetricId::new("latency"), AggregationType::P50, 1000, 2000)
.unwrap();
let p95 = registry
.aggregate(&MetricId::new("latency"), AggregationType::P95, 1000, 2000)
.unwrap();
assert_eq!(p50.value, 50.0); assert_eq!(p95.value, 100.0); }
#[test]
fn test_registry_remove() {
let mut registry = MetricsRegistry::new();
let definition =
MetricDefinition::new(MetricId::new("fps"), MetricType::Gauge, "Frames per second");
registry.define(definition);
registry
.record(MetricValue::new(MetricId::new("fps"), 60.0, 1000))
.unwrap();
registry.remove(&MetricId::new("fps"));
assert!(registry.get_definition(&MetricId::new("fps")).is_none());
assert!(registry.get_values(&MetricId::new("fps")).is_none());
}
#[test]
fn test_metrics_history() {
let mut history = MetricsHistory::new();
let snapshot = MetricSnapshot::new(1000);
history.add_snapshot(snapshot);
assert_eq!(history.snapshots().len(), 1);
assert_eq!(history.latest_snapshot().unwrap().timestamp, 1000);
}
}