Skip to main content

laminardb/
metrics.rs

1//! Prometheus metrics setup for LaminarDB server.
2
3use prometheus::{Encoder, IntCounter, IntGauge, Registry, TextEncoder};
4
5/// Create the prometheus registry with const labels and process collector.
6pub fn build_registry(const_labels: impl IntoIterator<Item = (String, String)>) -> Registry {
7    let labels: std::collections::HashMap<_, _> = const_labels.into_iter().collect();
8    let registry = Registry::new_custom(Some("laminardb".into()), Some(labels))
9        .expect("registry construction is infallible with valid label names");
10
11    #[cfg(target_os = "linux")]
12    {
13        let pc = prometheus::process_collector::ProcessCollector::for_self();
14        registry
15            .register(Box::new(pc))
16            .expect("process collector registration");
17    }
18
19    registry
20}
21
22/// Render the registry as Prometheus text format 0.0.4.
23pub fn render(registry: &Registry) -> Vec<u8> {
24    let mf = registry.gather();
25    let mut buf = Vec::with_capacity(4096);
26    TextEncoder::new()
27        .encode(&mf, &mut buf)
28        .expect("encoding is infallible");
29    buf
30}
31
32/// Server-level metrics (reload, uptime, connections).
33pub struct ServerMetrics {
34    pub reload_total: IntCounter,
35    pub uptime_seconds: IntGauge,
36    pub ws_connections: IntGauge,
37}
38
39impl ServerMetrics {
40    /// Register server metrics. Startup only.
41    #[must_use]
42    pub fn new(registry: &Registry) -> Self {
43        macro_rules! reg {
44            ($m:expr) => {{
45                let m = $m;
46                registry.register(Box::new(m.clone())).unwrap();
47                m
48            }};
49        }
50        Self {
51            reload_total: reg!(IntCounter::new("reload_total", "Config reload count").unwrap()),
52            uptime_seconds: reg!(
53                IntGauge::new("uptime_seconds", "Server uptime in seconds").unwrap()
54            ),
55            ws_connections: reg!(
56                IntGauge::new("ws_connections", "Active WebSocket connections").unwrap()
57            ),
58        }
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use laminar_db::EngineMetrics;
66
67    #[test]
68    fn registry_renders_engine_metrics() {
69        let registry = build_registry([
70            ("instance".into(), "test".into()),
71            ("pipeline".into(), "smoke".into()),
72        ]);
73        let engine = EngineMetrics::new(&registry);
74
75        engine.events_ingested.inc_by(100);
76        engine.events_emitted.inc_by(42);
77        engine.cycles.inc();
78
79        let text = String::from_utf8(render(&registry)).unwrap();
80        assert!(text.contains("laminardb_events_ingested_total"));
81        assert!(text.contains("laminardb_events_emitted_total"));
82        assert!(text.contains("laminardb_cycles_total"));
83    }
84
85    #[test]
86    fn server_metrics_appear_in_output() {
87        let registry = build_registry([
88            ("instance".into(), "test".into()),
89            ("pipeline".into(), "t".into()),
90        ]);
91        let srv = ServerMetrics::new(&registry);
92        srv.reload_total.inc();
93        srv.uptime_seconds.set(60);
94        srv.ws_connections.set(3);
95
96        let text = String::from_utf8(render(&registry)).unwrap();
97        assert!(text.contains("laminardb_reload_total"));
98        assert!(text.contains("laminardb_uptime_seconds"));
99        assert!(text.contains("laminardb_ws_connections"));
100    }
101}