sentinel_agent_protocol/v2/
metrics.rs1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct MetricsReport {
9 pub agent_id: String,
10 pub timestamp_ms: u64,
11 pub interval_ms: u64,
12 #[serde(default)]
13 pub counters: Vec<CounterMetric>,
14 #[serde(default)]
15 pub gauges: Vec<GaugeMetric>,
16 #[serde(default)]
17 pub histograms: Vec<HistogramMetric>,
18}
19
20impl MetricsReport {
21 pub fn new(agent_id: impl Into<String>, interval_ms: u64) -> Self {
22 Self {
23 agent_id: agent_id.into(),
24 timestamp_ms: now_ms(),
25 interval_ms,
26 counters: Vec::new(),
27 gauges: Vec::new(),
28 histograms: Vec::new(),
29 }
30 }
31
32 pub fn is_empty(&self) -> bool {
33 self.counters.is_empty() && self.gauges.is_empty() && self.histograms.is_empty()
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct CounterMetric {
40 pub name: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub help: Option<String>,
43 #[serde(default)]
44 pub labels: HashMap<String, String>,
45 pub value: u64,
46}
47
48impl CounterMetric {
49 pub fn new(name: impl Into<String>, value: u64) -> Self {
50 Self { name: name.into(), help: None, labels: HashMap::new(), value }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct GaugeMetric {
57 pub name: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub help: Option<String>,
60 #[serde(default)]
61 pub labels: HashMap<String, String>,
62 pub value: f64,
63}
64
65impl GaugeMetric {
66 pub fn new(name: impl Into<String>, value: f64) -> Self {
67 Self { name: name.into(), help: None, labels: HashMap::new(), value }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct HistogramMetric {
74 pub name: String,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub help: Option<String>,
77 #[serde(default)]
78 pub labels: HashMap<String, String>,
79 pub sum: f64,
80 pub count: u64,
81 pub buckets: Vec<HistogramBucket>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct HistogramBucket {
87 #[serde(serialize_with = "serialize_le", deserialize_with = "deserialize_le")]
88 pub le: f64,
89 pub count: u64,
90}
91
92impl HistogramBucket {
93 pub fn new(le: f64) -> Self { Self { le, count: 0 } }
94 pub fn infinity() -> Self { Self { le: f64::INFINITY, count: 0 } }
95}
96
97fn serialize_le<S>(le: &f64, serializer: S) -> Result<S::Ok, S::Error>
98where S: serde::Serializer {
99 if le.is_infinite() { serializer.serialize_str("+Inf") } else { serializer.serialize_f64(*le) }
100}
101
102fn deserialize_le<'de, D>(deserializer: D) -> Result<f64, D::Error>
103where D: serde::Deserializer<'de> {
104 use serde::de::{self, Visitor};
105 struct LeVisitor;
106 impl<'de> Visitor<'de> for LeVisitor {
107 type Value = f64;
108 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.write_str("float or +Inf") }
109 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> { Ok(v) }
110 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> { Ok(v as f64) }
111 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> { Ok(v as f64) }
112 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
113 if v == "+Inf" || v == "Inf" { Ok(f64::INFINITY) } else { v.parse().map_err(de::Error::custom) }
114 }
115 }
116 deserializer.deserialize_any(LeVisitor)
117}
118
119pub mod standard {
121 pub const REQUESTS_TOTAL: &str = "agent_requests_total";
122 pub const REQUESTS_BLOCKED_TOTAL: &str = "agent_requests_blocked_total";
123 pub const REQUESTS_DURATION_SECONDS: &str = "agent_requests_duration_seconds";
124 pub const ERRORS_TOTAL: &str = "agent_errors_total";
125 pub const IN_FLIGHT_REQUESTS: &str = "agent_in_flight_requests";
126 pub const QUEUE_DEPTH: &str = "agent_queue_depth";
127}
128
129fn now_ms() -> u64 {
130 std::time::SystemTime::now()
131 .duration_since(std::time::UNIX_EPOCH)
132 .map(|d| d.as_millis() as u64)
133 .unwrap_or(0)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_metrics_report() {
142 let report = MetricsReport::new("test-agent", 10_000);
143 assert!(report.is_empty());
144 }
145
146 #[test]
147 fn test_counter_metric() {
148 let counter = CounterMetric::new("test_counter", 100);
149 assert_eq!(counter.value, 100);
150 }
151
152 #[test]
153 fn test_histogram_bucket_infinity() {
154 let bucket = HistogramBucket::infinity();
155 assert!(bucket.le.is_infinite());
156
157 let json = serde_json::to_string(&bucket).unwrap();
158 assert!(json.contains("+Inf"));
159
160 let parsed: HistogramBucket = serde_json::from_str(&json).unwrap();
161 assert!(parsed.le.is_infinite());
162 }
163}