alice_analytics/
export.rs1#[cfg(feature = "std")]
7extern crate alloc;
8
9#[cfg(feature = "std")]
10use alloc::format;
11#[cfg(feature = "std")]
12use alloc::string::String;
13
14#[cfg(feature = "std")]
15use crate::pipeline::MetricSnapshot;
16
17#[cfg(feature = "std")]
23#[must_use]
24pub fn snapshot_to_json(snapshot: &MetricSnapshot) -> String {
25 format!(
26 concat!(
27 "{{",
28 "\"name_hash\":{},",
29 "\"counter\":{},",
30 "\"gauge\":{},",
31 "\"cardinality\":{},",
32 "\"p50\":{},",
33 "\"p95\":{},",
34 "\"p99\":{},",
35 "\"mean\":{},",
36 "\"min\":{},",
37 "\"max\":{},",
38 "\"event_count\":{}",
39 "}}"
40 ),
41 snapshot.name_hash,
42 format_f64(snapshot.counter),
43 format_f64(snapshot.gauge),
44 format_f64(snapshot.cardinality),
45 format_f64(snapshot.p50),
46 format_f64(snapshot.p95),
47 format_f64(snapshot.p99),
48 format_f64(snapshot.mean),
49 format_f64(snapshot.min),
50 format_f64(snapshot.max),
51 snapshot.event_count,
52 )
53}
54
55#[cfg(feature = "std")]
57#[must_use]
58pub fn snapshots_to_json(snapshots: &[MetricSnapshot]) -> String {
59 let mut out = String::from("[");
60 for (i, snap) in snapshots.iter().enumerate() {
61 if i > 0 {
62 out.push(',');
63 }
64 out.push_str(&snapshot_to_json(snap));
65 }
66 out.push(']');
67 out
68}
69
70#[cfg(feature = "std")]
85#[must_use]
86pub fn snapshot_to_prometheus(snapshot: &MetricSnapshot, prefix: &str) -> String {
87 use core::fmt::Write;
88 let hash = snapshot.name_hash;
89 let mut out = String::with_capacity(512);
90
91 let _ = writeln!(out, "# TYPE {prefix}_counter counter");
93 let _ = writeln!(
94 out,
95 "{prefix}_counter{{name_hash=\"{hash}\"}} {}",
96 format_f64(snapshot.counter)
97 );
98
99 let _ = writeln!(out, "# TYPE {prefix}_gauge gauge");
101 let _ = writeln!(
102 out,
103 "{prefix}_gauge{{name_hash=\"{hash}\"}} {}",
104 format_f64(snapshot.gauge)
105 );
106
107 let _ = writeln!(out, "# TYPE {prefix}_cardinality gauge");
109 let _ = writeln!(
110 out,
111 "{prefix}_cardinality{{name_hash=\"{hash}\"}} {}",
112 format_f64(snapshot.cardinality)
113 );
114
115 let _ = writeln!(out, "# TYPE {prefix}_latency summary");
117 let _ = writeln!(
118 out,
119 "{prefix}_latency{{name_hash=\"{hash}\",quantile=\"0.5\"}} {}",
120 format_f64(snapshot.p50)
121 );
122 let _ = writeln!(
123 out,
124 "{prefix}_latency{{name_hash=\"{hash}\",quantile=\"0.95\"}} {}",
125 format_f64(snapshot.p95)
126 );
127 let _ = writeln!(
128 out,
129 "{prefix}_latency{{name_hash=\"{hash}\",quantile=\"0.99\"}} {}",
130 format_f64(snapshot.p99)
131 );
132
133 let _ = writeln!(
135 out,
136 "{prefix}_events_total{{name_hash=\"{hash}\"}} {}",
137 snapshot.event_count
138 );
139
140 out
141}
142
143#[cfg(feature = "std")]
145#[must_use]
146pub fn snapshots_to_prometheus(snapshots: &[MetricSnapshot], prefix: &str) -> String {
147 let mut out = String::new();
148 for snap in snapshots {
149 out.push_str(&snapshot_to_prometheus(snap, prefix));
150 out.push('\n');
151 }
152 out
153}
154
155#[cfg(feature = "std")]
161fn format_f64(v: f64) -> String {
162 if v.is_nan() {
163 "null".to_string()
164 } else if v.is_infinite() {
165 if v.is_sign_positive() {
166 "\"+Inf\"".to_string()
167 } else {
168 "\"-Inf\"".to_string()
169 }
170 } else {
171 format!("{v}")
172 }
173}
174
175#[cfg(test)]
180#[cfg(feature = "std")]
181mod tests {
182 use super::*;
183
184 fn make_snapshot() -> MetricSnapshot {
185 MetricSnapshot {
186 name_hash: 12345,
187 counter: 100.0,
188 gauge: 42.0,
189 cardinality: 50.0,
190 p50: 25.0,
191 p95: 80.0,
192 p99: 99.5,
193 mean: 45.0,
194 min: 1.0,
195 max: 200.0,
196 event_count: 1000,
197 }
198 }
199
200 #[test]
201 fn json_single_snapshot() {
202 let snap = make_snapshot();
203 let json = snapshot_to_json(&snap);
204 assert!(json.contains("\"name_hash\":12345"));
205 assert!(json.contains("\"counter\":100"));
206 assert!(json.contains("\"event_count\":1000"));
207 assert!(json.starts_with('{'));
208 assert!(json.ends_with('}'));
209 }
210
211 #[test]
212 fn json_array() {
213 let snaps = vec![make_snapshot(), make_snapshot()];
214 let json = snapshots_to_json(&snaps);
215 assert!(json.starts_with('['));
216 assert!(json.ends_with(']'));
217 assert_eq!(json.matches("name_hash").count(), 2);
219 }
220
221 #[test]
222 fn json_empty_array() {
223 let json = snapshots_to_json(&[]);
224 assert_eq!(json, "[]");
225 }
226
227 #[test]
228 fn prometheus_single() {
229 let snap = make_snapshot();
230 let prom = snapshot_to_prometheus(&snap, "alice");
231 assert!(prom.contains("# TYPE alice_counter counter"));
232 assert!(prom.contains("alice_counter{name_hash=\"12345\"} 100"));
233 assert!(prom.contains("# TYPE alice_gauge gauge"));
234 assert!(prom.contains("quantile=\"0.99\""));
235 assert!(prom.contains("alice_events_total"));
236 }
237
238 #[test]
239 fn prometheus_multiple() {
240 let snaps = vec![make_snapshot(), make_snapshot()];
241 let prom = snapshots_to_prometheus(&snaps, "test");
242 assert_eq!(prom.matches("# TYPE test_counter counter").count(), 2);
244 }
245
246 #[test]
247 fn json_nan_handling() {
248 let mut snap = make_snapshot();
249 snap.p99 = f64::NAN;
250 let json = snapshot_to_json(&snap);
251 assert!(json.contains("null"));
252 }
253
254 #[test]
255 fn json_inf_handling() {
256 let mut snap = make_snapshot();
257 snap.max = f64::INFINITY;
258 let json = snapshot_to_json(&snap);
259 assert!(json.contains("+Inf"));
260 }
261
262 #[test]
263 fn prometheus_prefix() {
264 let snap = make_snapshot();
265 let prom = snapshot_to_prometheus(&snap, "my_app");
266 assert!(prom.contains("my_app_counter"));
267 assert!(prom.contains("my_app_gauge"));
268 assert!(prom.contains("my_app_latency"));
269 }
270
271 #[test]
272 fn format_f64_normal() {
273 assert_eq!(format_f64(42.5), "42.5");
274 assert_eq!(format_f64(0.0), "0");
275 }
276}