Skip to main content

linkprobe_core/export/
prometheus.rs

1use crate::result::RunResult;
2
3fn escape_label(s: &str) -> String {
4    s.replace('\\', "\\\\")
5        .replace('"', "\\\"")
6        .replace('\n', "\\n")
7}
8
9/// OpenMetrics / Prometheus text exposition for a successful [`RunResult`].
10///
11/// Emits stable gauge names: `linkprobe_ok`, `linkprobe_latency_milliseconds`,
12/// `linkprobe_jitter_milliseconds`, `linkprobe_download_bits_per_second`,
13/// `linkprobe_upload_bits_per_second`, and `linkprobe_packet_loss`. Only metrics present on
14/// the result are written.
15pub fn format_openmetrics(result: &RunResult) -> String {
16    let backend = escape_label(&result.backend);
17    let server = escape_label(&result.server.name);
18    let labels = format!("backend=\"{backend}\", server=\"{server}\"");
19    let mut out = String::new();
20
21    out.push_str("# HELP linkprobe_ok 1 if the last probe succeeded.\n");
22    out.push_str("# TYPE linkprobe_ok gauge\n");
23    out.push_str(&format!("linkprobe_ok{{{labels}}} 1\n"));
24
25    if let Some(ms) = result.measurement.latency_ms {
26        out.push_str("# HELP linkprobe_latency_milliseconds Round-trip latency.\n");
27        out.push_str("# TYPE linkprobe_latency_milliseconds gauge\n");
28        out.push_str(&format!(
29            "linkprobe_latency_milliseconds{{{labels}}} {ms}\n"
30        ));
31    }
32    if let Some(ms) = result.measurement.jitter_ms {
33        out.push_str("# HELP linkprobe_jitter_milliseconds Jitter.\n");
34        out.push_str("# TYPE linkprobe_jitter_milliseconds gauge\n");
35        out.push_str(&format!("linkprobe_jitter_milliseconds{{{labels}}} {ms}\n"));
36    }
37    if let Some(ref dl) = result.measurement.download {
38        out.push_str("# HELP linkprobe_download_bits_per_second Download throughput.\n");
39        out.push_str("# TYPE linkprobe_download_bits_per_second gauge\n");
40        out.push_str(&format!(
41            "linkprobe_download_bits_per_second{{{labels}}} {}\n",
42            dl.bps
43        ));
44    }
45    if let Some(ref ul) = result.measurement.upload {
46        out.push_str("# HELP linkprobe_upload_bits_per_second Upload throughput.\n");
47        out.push_str("# TYPE linkprobe_upload_bits_per_second gauge\n");
48        out.push_str(&format!(
49            "linkprobe_upload_bits_per_second{{{labels}}} {}\n",
50            ul.bps
51        ));
52    }
53    if let Some(loss) = result.measurement.packet_loss {
54        out.push_str("# HELP linkprobe_packet_loss Packet loss ration in [0,1].\n");
55        out.push_str("# TYPE linkprobe_packet_loss gauge\n");
56        out.push_str(&format!("linkprobe_packet_loss{{{labels}}} {loss}\n"));
57    }
58
59    out.push('\n');
60    out
61}
62
63/// OpenMetrics text for a failed probe (for scrape daemons that must always expose metrics).
64///
65/// Sets `linkprobe_ok` to `0` and adds a `linkprobe_last_error` series with the error message.
66pub fn format_openmetrics_failed(backend: &str, server: &str, err: &str) -> String {
67    let backend = escape_label(backend);
68    let server = escape_label(server);
69    let err = escape_label(err);
70    let labels = format!("backend=\"{backend}\", server=\"{server}\"");
71    let mut out = String::new();
72    out.push_str("# HELP linkprobe_ok 1 if the last probe succeeded.\n");
73    out.push_str("# TYPE linkprobe_ok gauge\n");
74    out.push_str(&format!("linkprobe_ok{{{labels}}} 0\n"));
75    out.push_str("# HELP linkprobe_last_error Last probe error message.\n");
76    out.push_str("# TYPE linkprobe_last_error gauge\n");
77    out.push_str(&format!(
78        "linkprobe_last_error{{{labels},error=\"{err}\"}} 1\n"
79    ));
80    out.push('\n');
81    out
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::measurement::{Measurement, Throughput};
88    use crate::server::Server;
89
90    #[test]
91    fn formats_gauges() {
92        let result = RunResult::new(
93            "librespeed",
94            Server::librespeed("https://example/"),
95            Measurement {
96                latency_ms: Some(12.5),
97                jitter_ms: Some(1.0),
98                download: Some(Throughput::from_bps(100_000_000.0)),
99                upload: Some(Throughput::from_bps(50_000_000.0)),
100                packet_loss: None,
101            },
102        );
103        let text = format_openmetrics(&result);
104        assert!(text.contains("linkprobe_ok{"));
105        assert!(text.contains("linkprobe_latency_milliseconds{"));
106        assert!(text.contains("12.5"));
107        assert!(text.contains("linkprobe_download_bits_per_second{"));
108        assert!(!text.contains("linkprobe_packet_loss_ratio"));
109    }
110
111    #[test]
112    fn formats_failure_gauges() {
113        let text = format_openmetrics_failed("librespeed", "example", "timeout");
114        assert!(text.contains("linkprobe_ok{"));
115        assert!(text.contains("} 0\n"));
116    }
117}