use super::metrics::{AgentSample, MetricsStore, MonitorMetrics};
use std::collections::BTreeMap;
pub(super) const CONTENT_TYPE: &str = "text/plain; version=0.0.4";
pub(super) fn render(store: &MetricsStore) -> String {
let monitors = &store.monitors;
let mut out = String::new();
help(
&mut out,
"ringo_monitor_runs_total",
"counter",
"Total monitor runs by result.",
);
for (name, m) in monitors {
line(
&mut out,
"ringo_monitor_runs_total",
&[("monitor", name), ("result", "pass")],
m.runs_pass as f64,
);
line(
&mut out,
"ringo_monitor_runs_total",
&[("monitor", name), ("result", "fail")],
m.runs_fail as f64,
);
line(
&mut out,
"ringo_monitor_runs_total",
&[("monitor", name), ("result", "timeout")],
m.runs_timeout as f64,
);
}
help(
&mut out,
"ringo_monitor_last_success",
"gauge",
"Whether the most recent run passed (1) or failed (0).",
);
for (name, m) in monitors {
line(
&mut out,
"ringo_monitor_last_success",
&[("monitor", name)],
b2f(m.last_success),
);
}
help(
&mut out,
"ringo_monitor_last_duration_seconds",
"gauge",
"Wall-clock duration of the most recent run.",
);
for (name, m) in monitors {
line(
&mut out,
"ringo_monitor_last_duration_seconds",
&[("monitor", name)],
m.last_duration_s,
);
}
help(
&mut out,
"ringo_monitor_last_run_timestamp_seconds",
"gauge",
"Unix time of the most recent run's completion.",
);
for (name, m) in monitors {
line(
&mut out,
"ringo_monitor_last_run_timestamp_seconds",
&[("monitor", name)],
m.last_run_unix as f64,
);
}
help(
&mut out,
"ringo_scenario_last_success",
"gauge",
"Whether the scenario passed (1) or failed (0) in the most recent run.",
);
for (monitor, m) in monitors {
for (scenario, s) in &m.scenarios {
line(
&mut out,
"ringo_scenario_last_success",
&[("monitor", monitor), ("scenario", scenario)],
b2f(s.passed),
);
}
}
help(
&mut out,
"ringo_agent_registered",
"gauge",
"Whether the agent was registered at the run's end (1/0).",
);
for_each_agent(monitors, &mut out, "ringo_agent_registered", |a| {
Some(b2f(a.registered))
});
help(
&mut out,
"ringo_call_mos",
"gauge",
"Estimated Mean Opinion Score (1.0–4.5).",
);
for_each_agent(monitors, &mut out, "ringo_call_mos", |a| a.mos);
help(
&mut out,
"ringo_call_jitter_ms",
"gauge",
"Receive-side jitter, milliseconds.",
);
for_each_agent(monitors, &mut out, "ringo_call_jitter_ms", |a| a.jitter_ms);
help(
&mut out,
"ringo_call_packet_loss_pct",
"gauge",
"Receive-side packet loss, percent.",
);
for_each_agent(monitors, &mut out, "ringo_call_packet_loss_pct", |a| {
a.packet_loss_pct
});
help(
&mut out,
"ringo_call_rtt_ms",
"gauge",
"Round-trip time, milliseconds.",
);
for_each_agent(monitors, &mut out, "ringo_call_rtt_ms", |a| a.rtt_ms);
out
}
fn for_each_agent(
monitors: &BTreeMap<String, MonitorMetrics>,
out: &mut String,
metric: &str,
pick: impl Fn(&AgentSample) -> Option<f64>,
) {
for (monitor, m) in monitors {
for (scenario, s) in &m.scenarios {
for (agent, sample) in &s.agents {
if let Some(v) = pick(sample) {
line(
out,
metric,
&[
("monitor", monitor),
("scenario", scenario),
("agent", agent),
],
v,
);
}
}
}
}
}
fn help(out: &mut String, metric: &str, kind: &str, help: &str) {
out.push_str(&format!("# HELP {metric} {help}\n# TYPE {metric} {kind}\n"));
}
fn line(out: &mut String, metric: &str, labels: &[(&str, &str)], value: f64) {
out.push_str(metric);
if !labels.is_empty() {
out.push('{');
for (i, (k, v)) in labels.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&format!("{k}=\"{}\"", escape(v)));
}
out.push('}');
}
out.push_str(&format!(" {value}\n"));
}
fn b2f(b: bool) -> f64 {
if b { 1.0 } else { 0.0 }
}
fn escape(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serve::runner::{AgentMetric, RunOutcome, ScenarioOutcome};
use std::time::Duration;
fn store_with(passed: bool, scenarios: Vec<ScenarioOutcome>) -> MetricsStore {
let mut store = MetricsStore::default();
store.record(
"smoke",
&RunOutcome {
passed,
error: None,
duration: Duration::from_millis(1500),
timed_out: false,
scenarios,
},
1_700_000_000,
);
store
}
#[test]
fn renders_nested_labels() {
let store = store_with(
true,
vec![ScenarioOutcome {
name: "callee rejects".into(),
passed: true,
agents: vec![AgentMetric {
agent: "Caller".into(),
registered: true,
mos: Some(4.2),
jitter_ms: Some(2.0),
packet_loss_pct: Some(0.0),
rtt_ms: Some(18.0),
}],
}],
);
let out = render(&store);
assert!(
out.contains(r#"ringo_monitor_runs_total{monitor="smoke",result="pass"} 1"#),
"{out}"
);
assert!(
out.contains(
r#"ringo_scenario_last_success{monitor="smoke",scenario="callee rejects"} 1"#
),
"{out}"
);
assert!(
out.contains(
r#"ringo_call_mos{monitor="smoke",scenario="callee rejects",agent="Caller"} 4.2"#
),
"{out}"
);
}
#[test]
fn absent_quality_field_is_skipped() {
let store = store_with(
false,
vec![ScenarioOutcome {
name: "registers".into(),
passed: false,
agents: vec![AgentMetric {
agent: "a".into(),
registered: false,
mos: None,
jitter_ms: None,
packet_loss_pct: None,
rtt_ms: None,
}],
}],
);
let out = render(&store);
assert!(
out.contains(
r#"ringo_agent_registered{monitor="smoke",scenario="registers",agent="a"} 0"#
),
"{out}"
);
assert!(!out.contains("ringo_call_mos{monitor=\"smoke\""), "{out}");
}
}