use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub struct MetricId(pub String);
impl MetricId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
}
impl From<&str> for MetricId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl From<String> for MetricId {
fn from(s: String) -> Self {
Self(s)
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub enum MetricType {
Counter,
Gauge,
Histogram,
}
#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub struct MetricValue {
pub metric_id: MetricId,
pub value: f64,
pub timestamp: u64,
#[serde(default)]
pub metadata: String,
}
impl MetricValue {
pub fn new(metric_id: MetricId, value: f64, timestamp: u64) -> Self {
Self {
metric_id,
value,
timestamp,
metadata: String::new(),
}
}
pub fn with_metadata(
metric_id: MetricId,
value: f64,
timestamp: u64,
metadata: impl Into<String>,
) -> Self {
Self {
metric_id,
value,
timestamp,
metadata: metadata.into(),
}
}
}
#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub struct MetricDefinition {
pub metric_id: MetricId,
pub metric_type: MetricType,
pub description: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub labels: HashMap<String, String>,
}
impl MetricDefinition {
pub fn new(
metric_id: MetricId,
metric_type: MetricType,
description: impl Into<String>,
) -> Self {
Self {
metric_id,
metric_type,
description: description.into(),
tags: Vec::new(),
labels: HashMap::new(),
}
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub enum AggregationType {
Sum,
Count,
Average,
Min,
Max,
P50,
P95,
P99,
Last,
Rate,
}
#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub struct AggregatedMetric {
pub metric_id: MetricId,
pub aggregation_type: AggregationType,
pub value: f64,
pub period_start: u64,
pub period_end: u64,
pub sample_count: usize,
}
impl AggregatedMetric {
pub fn new(
metric_id: MetricId,
aggregation_type: AggregationType,
value: f64,
period_start: u64,
period_end: u64,
sample_count: usize,
) -> Self {
Self {
metric_id,
aggregation_type,
value,
period_start,
period_end,
sample_count,
}
}
}
#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub struct MetricSnapshot {
pub timestamp: u64,
pub values: HashMap<MetricId, Vec<MetricValue>>,
pub label: Option<String>,
}
impl MetricSnapshot {
pub fn new(timestamp: u64) -> Self {
Self {
timestamp,
values: HashMap::new(),
label: None,
}
}
pub fn with_label(timestamp: u64, label: impl Into<String>) -> Self {
Self {
timestamp,
values: HashMap::new(),
label: Some(label.into()),
}
}
pub fn add_value(&mut self, value: MetricValue) {
self.values
.entry(value.metric_id.clone())
.or_default()
.push(value);
}
}
#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub struct MetricReport {
pub period_start: u64,
pub period_end: u64,
pub aggregated_metrics: Vec<AggregatedMetric>,
pub label: Option<String>,
}
impl MetricReport {
pub fn new(period_start: u64, period_end: u64) -> Self {
Self {
period_start,
period_end,
aggregated_metrics: Vec::new(),
label: None,
}
}
pub fn with_label(period_start: u64, period_end: u64, label: impl Into<String>) -> Self {
Self {
period_start,
period_end,
aggregated_metrics: Vec::new(),
label: Some(label.into()),
}
}
pub fn add_aggregated(&mut self, metric: AggregatedMetric) {
self.aggregated_metrics.push(metric);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metric_id_creation() {
let id1 = MetricId::new("fps");
let id2 = MetricId::from("fps");
let id3: MetricId = "fps".into();
assert_eq!(id1, id2);
assert_eq!(id2, id3);
}
#[test]
fn test_metric_value_creation() {
let value = MetricValue::new(MetricId::new("fps"), 60.0, 1000);
assert_eq!(value.metric_id, MetricId::new("fps"));
assert_eq!(value.value, 60.0);
assert_eq!(value.timestamp, 1000);
assert_eq!(value.metadata, "");
}
#[test]
fn test_metric_definition_builder() {
let def =
MetricDefinition::new(MetricId::new("fps"), MetricType::Gauge, "Frames per second")
.with_tag("performance")
.with_label("category", "rendering");
assert_eq!(def.metric_id, MetricId::new("fps"));
assert_eq!(def.metric_type, MetricType::Gauge);
assert_eq!(def.description, "Frames per second");
assert_eq!(def.tags, vec!["performance"]);
assert_eq!(def.labels.get("category"), Some(&"rendering".to_string()));
}
#[test]
fn test_metric_snapshot() {
let mut snapshot = MetricSnapshot::with_label(1000, "test_snapshot");
snapshot.add_value(MetricValue::new(MetricId::new("fps"), 60.0, 1000));
snapshot.add_value(MetricValue::new(MetricId::new("fps"), 58.0, 1001));
assert_eq!(snapshot.timestamp, 1000);
assert_eq!(snapshot.label, Some("test_snapshot".to_string()));
assert_eq!(snapshot.values.len(), 1);
assert_eq!(snapshot.values[&MetricId::new("fps")].len(), 2);
}
#[test]
fn test_metric_report() {
let mut report = MetricReport::with_label(1000, 2000, "weekly_report");
report.add_aggregated(AggregatedMetric::new(
MetricId::new("fps"),
AggregationType::Average,
59.5,
1000,
2000,
100,
));
assert_eq!(report.period_start, 1000);
assert_eq!(report.period_end, 2000);
assert_eq!(report.label, Some("weekly_report".to_string()));
assert_eq!(report.aggregated_metrics.len(), 1);
assert_eq!(report.aggregated_metrics[0].value, 59.5);
}
}