metrale_telemetry/export/
events.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The `/v1/events` stream: one JSON object per SSE `data:` line
4//! (JSONL); the server sends one per device-sample period.
5//!
6//! Schema (version [`EVENT_SCHEMA_VERSION`]); every key is always present,
7//! and a quantity the device does not report is `null`, never 0:
8//!
9//! ```text
10//! { "v": 1, "seq": <u64, per-stream>, "kind": "snapshot",
11//!   "level": "off"|"basic"|"kernel", "at_ns": <telemetry clock>,
12//!   "device_state": "not_started"|"available"|"unavailable",
13//!   "device": null | { "power_mw", "energy_counter_mj", "graphics_clock_mhz",
14//!       "sm_clock_mhz", "mem_clock_mhz", "temperature_c",
15//!       "clocks_event_reasons", "mem_used_bytes", "mem_total_bytes",
16//!       "pcie_tx_kbps", "pcie_rx_kbps", "nvlink_active_links",
17//!       "samples", "age_ns", "read_errors" },
18//!   "energy": { "gpu_mj_total", "gpu_counter_mj", "tokens_total",
19//!       "joules_per_token_live", "joules_per_token_cumulative",
20//!       "counter_resets", "counter_wraps" },
21//!   "sched": { "pending", "active", "prefilling", "swapped",
22//!       "kv_blocks_free", "kv_blocks_total", "ssm_slots_used",
23//!       "ssm_slots_total", "ticks", "stream_syncs", "blocking_d2h",
24//!       "graph_replays", "graph_captures" },
25//!   "spec": [ { "drafts", "steps", "acceptance_by_depth": [f64] } ],
26//!   "requests": { "finished", "ttft_mean_ms", "tpot_mean_ms", "e2e_mean_ms",
27//!       "energy_mean_j", "energy_unattributed" } }
28//! ```
29//!
30//! Owner: telemetry.
31//! Invariants: an event line holds no newline.
32
33use serde::Serialize;
34
35use crate::snapshot::TelemetrySnapshot;
36
37pub const EVENT_SCHEMA_VERSION: u32 = 1;
38
39#[derive(Serialize)]
40struct Event<'a> {
41    v: u32,
42    seq: u64,
43    kind: &'static str,
44    #[serde(flatten)]
45    snap: &'a TelemetrySnapshot,
46}
47
48/// 2026-09-26: One event line, with no trailing newline (SSE framing adds it).
49pub fn snapshot_line(seq: u64, snap: &TelemetrySnapshot) -> String {
50    serde_json::to_string(&Event {
51        v: EVENT_SCHEMA_VERSION,
52        seq,
53        kind: "snapshot",
54        snap,
55    })
56    .expect("a snapshot is plain data and always serialises")
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::clock::MonotonicClock;
63    use crate::hub::Telemetry;
64    use crate::level::{Level, TelemetryConfig};
65
66    #[test]
67    fn a_line_is_one_json_object_carrying_every_schema_key() {
68        static CLOCK: MonotonicClock = MonotonicClock;
69        let t = Telemetry::new(&CLOCK);
70        t.configure(&TelemetryConfig::serving(Level::Basic, 0));
71        t.tokens(3);
72        let line = snapshot_line(7, &t.snapshot());
73        assert!(!line.contains('\n'), "one line per event");
74        let v: serde_json::Value = serde_json::from_str(&line).unwrap();
75        assert_eq!(v["v"], 1);
76        assert_eq!(v["seq"], 7);
77        assert_eq!(v["kind"], "snapshot");
78        assert_eq!(v["level"], "basic");
79        assert_eq!(v["device_state"], "not_started");
80        assert!(v["device"].is_null(), "no sample yet is null, not zeros");
81        assert!(v["energy"]["joules_per_token_live"].is_null());
82        for k in ["pending", "kv_blocks_free", "graph_replays", "stream_syncs"] {
83            assert!(v["sched"].get(k).is_some(), "sched.{k}");
84        }
85        for k in ["finished", "tpot_mean_ms", "energy_unattributed"] {
86            assert!(v["requests"].get(k).is_some(), "requests.{k}");
87        }
88    }
89}