use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use strum_macros::{AsRefStr, Display, EnumString};
use xxhash_rust::xxh64::xxh64;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumString, Display, AsRefStr,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
pub enum MetricCategory {
#[strum(serialize = "network.tcp")]
#[serde(rename = "network.tcp")]
NetworkTcp,
#[strum(serialize = "network.ping")]
#[serde(rename = "network.ping")]
NetworkPing,
#[strum(serialize = "network.http")]
#[serde(rename = "network.http")]
NetworkHttp,
Crypto,
Polymarket,
Stock,
Custom,
}
pub type StaticTags = BTreeMap<String, String>;
pub type DynamicTags = BTreeMap<String, String>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricSeries {
pub series_id: u64,
pub category: MetricCategory,
pub name: String,
pub target: String,
pub static_tags: StaticTags,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl MetricSeries {
pub fn compute_series_id(
category: MetricCategory,
name: &str,
target: &str,
static_tags: &StaticTags,
) -> u64 {
let tags_str = static_tags
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(",");
let input = format!("{}|{}|{}|{}", category.as_ref(), name, target, tags_str);
xxh64(input.as_bytes(), 0)
}
pub fn new(
category: MetricCategory,
name: impl Into<String>,
target: impl Into<String>,
static_tags: StaticTags,
description: Option<String>,
) -> Self {
let name = name.into();
let target = target.into();
let series_id = Self::compute_series_id(category, &name, &target, &static_tags);
let now = Utc::now();
Self {
series_id,
category,
name,
target,
static_tags,
description,
created_at: now,
updated_at: now,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricValue {
pub ts: DateTime<Utc>,
pub series_id: u64,
pub value: f64,
pub unit: Option<String>,
pub success: bool,
pub duration_ms: u32,
pub dynamic_tags: DynamicTags,
}
impl MetricValue {
pub fn new(series_id: u64, value: f64, success: bool) -> Self {
Self {
ts: Utc::now(),
series_id,
value,
unit: None,
success,
duration_ms: 0,
dynamic_tags: DynamicTags::new(),
}
}
pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
self.unit = Some(unit.into());
self
}
pub fn with_duration_ms(mut self, duration_ms: u32) -> Self {
self.duration_ms = duration_ms;
self
}
pub fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.dynamic_tags.insert(key.into(), value.into());
self
}
pub fn with_tags(mut self, tags: DynamicTags) -> Self {
self.dynamic_tags.extend(tags);
self
}
}
pub type EventPayload = BTreeMap<String, String>;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumString, Display, AsRefStr,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
pub enum EventSource {
#[strum(serialize = "collector.network.tcp")]
#[serde(rename = "collector.network.tcp")]
CollectorNetworkTcp,
#[strum(serialize = "collector.network.ping")]
#[serde(rename = "collector.network.ping")]
CollectorNetworkPing,
#[strum(serialize = "collector.network.http")]
#[serde(rename = "collector.network.http")]
CollectorNetworkHttp,
#[strum(serialize = "rule.engine")]
#[serde(rename = "rule.engine")]
RuleEngine,
System,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumString, Display, AsRefStr,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum EventKind {
Alert,
Error,
System,
Audit,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumString, Display, AsRefStr,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum EventSeverity {
Debug,
Info,
Warn,
Error,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub id: Option<i64>,
pub ts: DateTime<Utc>,
pub source: EventSource,
pub kind: EventKind,
pub severity: EventSeverity,
pub message: String,
pub payload: EventPayload,
}
impl Event {
pub fn new(
source: EventSource,
kind: EventKind,
severity: EventSeverity,
message: impl Into<String>,
) -> Self {
Self {
id: None,
ts: Utc::now(),
source,
kind,
severity,
message: message.into(),
payload: EventPayload::new(),
}
}
pub fn with_payload(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.payload.insert(key.into(), value.into());
self
}
pub fn with_payloads(mut self, entries: EventPayload) -> Self {
self.payload.extend(entries);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_metric_category_from_str() {
assert_eq!(
MetricCategory::from_str("network.tcp").unwrap(),
MetricCategory::NetworkTcp
);
assert_eq!(
MetricCategory::from_str("crypto").unwrap(),
MetricCategory::Crypto
);
assert_eq!(
MetricCategory::from_str("CUSTOM").unwrap(),
MetricCategory::Custom
);
}
#[test]
fn test_metric_category_as_ref() {
assert_eq!(MetricCategory::NetworkTcp.as_ref(), "network.tcp");
assert_eq!(MetricCategory::Crypto.as_ref(), "crypto");
}
#[test]
fn test_series_id_computation() {
let mut tags = StaticTags::new();
tags.insert("host".to_string(), "prod-1".to_string());
tags.insert("region".to_string(), "us-west".to_string());
let id1 = MetricSeries::compute_series_id(
MetricCategory::NetworkTcp,
"latency",
"127.0.0.1:6379",
&tags,
);
let id2 = MetricSeries::compute_series_id(
MetricCategory::NetworkTcp,
"latency",
"127.0.0.1:6379",
&tags,
);
assert_eq!(id1, id2);
let id3 = MetricSeries::compute_series_id(
MetricCategory::NetworkTcp,
"latency",
"127.0.0.1:6380",
&tags,
);
assert_ne!(id1, id3);
}
#[test]
fn test_series_id_tag_order_independence() {
let mut tags1 = StaticTags::new();
tags1.insert("a".to_string(), "1".to_string());
tags1.insert("b".to_string(), "2".to_string());
let mut tags2 = StaticTags::new();
tags2.insert("b".to_string(), "2".to_string());
tags2.insert("a".to_string(), "1".to_string());
let id1 = MetricSeries::compute_series_id(MetricCategory::Custom, "test", "target", &tags1);
let id2 = MetricSeries::compute_series_id(MetricCategory::Custom, "test", "target", &tags2);
assert_eq!(id1, id2, "Tag insertion order should not affect series_id");
}
#[test]
fn test_metric_series_new() {
let tags = StaticTags::new();
let series = MetricSeries::new(
MetricCategory::NetworkTcp,
"latency",
"127.0.0.1:6379",
tags,
Some("Redis latency".to_string()),
);
assert!(series.series_id > 0);
assert_eq!(series.category, MetricCategory::NetworkTcp);
assert_eq!(series.name, "latency");
assert_eq!(series.target, "127.0.0.1:6379");
}
#[test]
fn test_metric_value_builder() {
let value = MetricValue::new(12345, 100.5, true)
.with_unit("ms")
.with_duration_ms(15)
.with_tag("status_code", "200")
.with_tag("path", "/api/v1");
assert_eq!(value.series_id, 12345);
assert_eq!(value.value, 100.5);
assert_eq!(value.unit, Some("ms".to_string()));
assert!(value.success);
assert_eq!(value.duration_ms, 15);
assert_eq!(
value.dynamic_tags.get("status_code"),
Some(&"200".to_string())
);
}
#[test]
fn test_event_kind_from_str() {
assert_eq!(EventKind::from_str("alert").unwrap(), EventKind::Alert);
assert_eq!(EventKind::from_str("ERROR").unwrap(), EventKind::Error);
}
#[test]
fn test_event_kind_as_str() {
assert_eq!(EventKind::Alert.as_ref(), "alert");
assert_eq!(EventKind::System.as_ref(), "system");
}
#[test]
fn test_event_severity_from_str() {
assert_eq!(
EventSeverity::from_str("critical").unwrap(),
EventSeverity::Critical
);
assert_eq!(
EventSeverity::from_str("WARN").unwrap(),
EventSeverity::Warn
);
}
#[test]
fn test_event_severity_as_str() {
assert_eq!(EventSeverity::Debug.as_ref(), "debug");
assert_eq!(EventSeverity::Critical.as_ref(), "critical");
}
}