Skip to main content

alice_analytics/
export.rs

1//! Data Export — JSON / Prometheus テキスト形式
2//!
3//! `MetricSnapshot` から JSON 文字列や Prometheus exposition format への変換。
4//! 外部依存なし(手動フォーマット)。
5
6#[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// ============================================================================
18// JSON Export
19// ============================================================================
20
21/// `MetricSnapshot` を JSON 文字列に変換。
22#[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/// 複数の `MetricSnapshot` を JSON 配列に変換。
56#[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// ============================================================================
71// Prometheus Exposition Format
72// ============================================================================
73
74/// `MetricSnapshot` を Prometheus exposition format に変換。
75///
76/// ```text
77/// # TYPE metric_counter counter
78/// metric_counter{name_hash="12345"} 100.0
79/// # TYPE metric_gauge gauge
80/// metric_gauge{name_hash="12345"} 42.0
81/// # TYPE metric_p99 gauge
82/// metric_p99{name_hash="12345"} 99.5
83/// ```
84#[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    // Counter
92    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    // Gauge
100    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    // Cardinality
108    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    // Quantiles
116    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    // Event count
134    let _ = writeln!(
135        out,
136        "{prefix}_events_total{{name_hash=\"{hash}\"}} {}",
137        snapshot.event_count
138    );
139
140    out
141}
142
143/// 複数の `MetricSnapshot` を Prometheus 形式に変換。
144#[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// ============================================================================
156// Helpers
157// ============================================================================
158
159/// f64をフォーマット(NaN/Inf対応)。
160#[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// ============================================================================
176// Tests
177// ============================================================================
178
179#[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        // 2つのオブジェクトが含まれる
218        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        // 2つ分のメトリクス
243        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}