grapsus_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 {
51 name: name.into(),
52 help: None,
53 labels: HashMap::new(),
54 value,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct GaugeMetric {
62 pub name: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub help: Option<String>,
65 #[serde(default)]
66 pub labels: HashMap<String, String>,
67 pub value: f64,
68}
69
70impl GaugeMetric {
71 pub fn new(name: impl Into<String>, value: f64) -> Self {
72 Self {
73 name: name.into(),
74 help: None,
75 labels: HashMap::new(),
76 value,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct HistogramMetric {
84 pub name: String,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub help: Option<String>,
87 #[serde(default)]
88 pub labels: HashMap<String, String>,
89 pub sum: f64,
90 pub count: u64,
91 pub buckets: Vec<HistogramBucket>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct HistogramBucket {
97 #[serde(serialize_with = "serialize_le", deserialize_with = "deserialize_le")]
98 pub le: f64,
99 pub count: u64,
100}
101
102impl HistogramBucket {
103 pub fn new(le: f64) -> Self {
104 Self { le, count: 0 }
105 }
106 pub fn infinity() -> Self {
107 Self {
108 le: f64::INFINITY,
109 count: 0,
110 }
111 }
112}
113
114fn serialize_le<S>(le: &f64, serializer: S) -> Result<S::Ok, S::Error>
115where
116 S: serde::Serializer,
117{
118 if le.is_infinite() {
119 serializer.serialize_str("+Inf")
120 } else {
121 serializer.serialize_f64(*le)
122 }
123}
124
125fn deserialize_le<'de, D>(deserializer: D) -> Result<f64, D::Error>
126where
127 D: serde::Deserializer<'de>,
128{
129 use serde::de::{self, Visitor};
130 struct LeVisitor;
131 impl<'de> Visitor<'de> for LeVisitor {
132 type Value = f64;
133 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
134 f.write_str("float or +Inf")
135 }
136 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
137 Ok(v)
138 }
139 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
140 Ok(v as f64)
141 }
142 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
143 Ok(v as f64)
144 }
145 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
146 if v == "+Inf" || v == "Inf" {
147 Ok(f64::INFINITY)
148 } else {
149 v.parse().map_err(de::Error::custom)
150 }
151 }
152 }
153 deserializer.deserialize_any(LeVisitor)
154}
155
156pub mod standard {
158 pub const REQUESTS_TOTAL: &str = "agent_requests_total";
159 pub const REQUESTS_BLOCKED_TOTAL: &str = "agent_requests_blocked_total";
160 pub const REQUESTS_DURATION_SECONDS: &str = "agent_requests_duration_seconds";
161 pub const ERRORS_TOTAL: &str = "agent_errors_total";
162 pub const IN_FLIGHT_REQUESTS: &str = "agent_in_flight_requests";
163 pub const QUEUE_DEPTH: &str = "agent_queue_depth";
164}
165
166fn now_ms() -> u64 {
167 std::time::SystemTime::now()
168 .duration_since(std::time::UNIX_EPOCH)
169 .map(|d| d.as_millis() as u64)
170 .unwrap_or(0)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_metrics_report() {
179 let report = MetricsReport::new("test-agent", 10_000);
180 assert!(report.is_empty());
181 }
182
183 #[test]
184 fn test_counter_metric() {
185 let counter = CounterMetric::new("test_counter", 100);
186 assert_eq!(counter.value, 100);
187 }
188
189 #[test]
190 fn test_histogram_bucket_infinity() {
191 let bucket = HistogramBucket::infinity();
192 assert!(bucket.le.is_infinite());
193
194 let json = serde_json::to_string(&bucket).unwrap();
195 assert!(json.contains("+Inf"));
196
197 let parsed: HistogramBucket = serde_json::from_str(&json).unwrap();
198 assert!(parsed.le.is_infinite());
199 }
200}