use crate::{LabelSet, MetricMetadata, Registry};
#[derive(Debug, Clone, serde::Serialize)]
pub struct CounterSeries {
pub name: String,
pub labels: LabelSet,
pub value: u64,
pub metadata: Option<MetricMetadata>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct GaugeSeries {
pub name: String,
pub labels: LabelSet,
pub value: f64,
pub metadata: Option<MetricMetadata>,
}
#[cfg(feature = "timer")]
#[derive(Debug, Clone, serde::Serialize)]
pub struct TimerSeries {
pub name: String,
pub labels: LabelSet,
pub stats: crate::TimerStats,
pub metadata: Option<MetricMetadata>,
}
#[cfg(feature = "meter")]
#[derive(Debug, Clone, serde::Serialize)]
pub struct RateSeries {
pub name: String,
pub labels: LabelSet,
pub stats: crate::RateStats,
pub metadata: Option<MetricMetadata>,
}
#[cfg(feature = "histogram")]
#[derive(Debug, Clone, serde::Serialize)]
pub struct HistogramSeries {
pub name: String,
pub labels: LabelSet,
pub histogram: crate::HistogramSnapshot,
pub metadata: Option<MetricMetadata>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct RegistrySnapshot {
pub schema_version: u32,
pub counters: Vec<CounterSeries>,
pub gauges: Vec<GaugeSeries>,
#[cfg(feature = "timer")]
pub timers: Vec<TimerSeries>,
#[cfg(feature = "meter")]
pub rate_meters: Vec<RateSeries>,
#[cfg(feature = "histogram")]
pub histograms: Vec<HistogramSeries>,
pub cardinality: CardinalitySnapshot,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct CardinalitySnapshot {
pub current: usize,
pub cap: usize,
pub overflows: u64,
}
const SCHEMA_VERSION: u32 = 1;
#[must_use]
pub fn snapshot(registry: &Registry) -> RegistrySnapshot {
let mut counters = Vec::new();
#[cfg(feature = "count")]
{
for (name, labels, c) in registry.counter_entries() {
let metadata = registry.metadata(&name);
counters.push(CounterSeries {
name,
labels,
value: c.get(),
metadata,
});
}
counters.sort_by(|a, b| (a.name.as_str(), &a.labels).cmp(&(b.name.as_str(), &b.labels)));
}
let mut gauges = Vec::new();
#[cfg(feature = "gauge")]
{
for (name, labels, g) in registry.gauge_entries() {
let metadata = registry.metadata(&name);
gauges.push(GaugeSeries {
name,
labels,
value: g.get(),
metadata,
});
}
gauges.sort_by(|a, b| (a.name.as_str(), &a.labels).cmp(&(b.name.as_str(), &b.labels)));
}
#[cfg(feature = "timer")]
let mut timers = Vec::new();
#[cfg(feature = "timer")]
{
for (name, labels, t) in registry.timer_entries() {
let metadata = registry.metadata(&name);
timers.push(TimerSeries {
name,
labels,
stats: t.stats(),
metadata,
});
}
timers.sort_by(|a, b| (a.name.as_str(), &a.labels).cmp(&(b.name.as_str(), &b.labels)));
}
#[cfg(feature = "meter")]
let mut rate_meters = Vec::new();
#[cfg(feature = "meter")]
{
for (name, labels, r) in registry.rate_meter_entries() {
let metadata = registry.metadata(&name);
rate_meters.push(RateSeries {
name,
labels,
stats: r.stats(),
metadata,
});
}
rate_meters.sort_by(|a, b| (a.name.as_str(), &a.labels).cmp(&(b.name.as_str(), &b.labels)));
}
#[cfg(feature = "histogram")]
let mut histograms = Vec::new();
#[cfg(feature = "histogram")]
{
for (name, labels, h) in registry.histogram_entries() {
let metadata = registry.metadata(&name);
histograms.push(HistogramSeries {
name,
labels,
histogram: h.snapshot(),
metadata,
});
}
histograms.sort_by(|a, b| (a.name.as_str(), &a.labels).cmp(&(b.name.as_str(), &b.labels)));
}
RegistrySnapshot {
schema_version: SCHEMA_VERSION,
counters,
gauges,
#[cfg(feature = "timer")]
timers,
#[cfg(feature = "meter")]
rate_meters,
#[cfg(feature = "histogram")]
histograms,
cardinality: CardinalitySnapshot {
current: registry.cardinality_count(),
cap: registry.cardinality_cap(),
overflows: registry.cardinality_overflows(),
},
}
}
#[must_use]
pub fn render(registry: &Registry) -> String {
serde_json::to_string(&snapshot(registry)).expect("RegistrySnapshot serialises to JSON")
}
#[must_use]
pub fn render_pretty(registry: &Registry) -> String {
serde_json::to_string_pretty(&snapshot(registry)).expect("RegistrySnapshot serialises to JSON")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_registry_snapshot() {
let r = Registry::new();
let snap = snapshot(&r);
assert_eq!(snap.schema_version, SCHEMA_VERSION);
assert!(snap.counters.is_empty());
assert_eq!(snap.cardinality.current, 0);
assert!(snap.cardinality.cap > 0);
}
#[test]
#[cfg(feature = "count")]
fn counter_snapshot_includes_labels_and_value() {
let r = Registry::new();
r.describe_counter("hits", "Total hits", crate::Unit::Custom("hits"));
r.get_or_create_counter("hits").add(7);
let labels = LabelSet::from([("status", "200")]);
r.get_or_create_counter_with("hits", &labels).add(3);
let snap = snapshot(&r);
assert_eq!(snap.counters.len(), 2);
assert!(snap.counters.iter().any(|c| c.value == 7));
assert!(snap.counters.iter().any(|c| c.value == 3));
let labelled = snap.counters.iter().find(|c| !c.labels.is_empty()).unwrap();
assert_eq!(labelled.labels.get("status"), Some("200"));
assert!(labelled.metadata.is_some());
}
#[test]
fn render_produces_valid_json() {
let r = Registry::new();
let body = render(&r);
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["schema_version"], 1);
}
#[test]
fn render_pretty_indents() {
let r = Registry::new();
let body = render_pretty(&r);
assert!(body.contains('\n'));
}
#[test]
#[cfg(feature = "gauge")]
fn gauge_snapshot_renders() {
let r = Registry::new();
r.get_or_create_gauge("temp").set(21.5);
r.get_or_create_gauge_with("temp", &LabelSet::from([("zone", "a")]))
.set(18.0);
let snap = snapshot(&r);
assert_eq!(snap.gauges.len(), 2);
assert!(snap
.gauges
.iter()
.any(|g| g.labels.is_empty() && g.value == 21.5));
assert!(snap
.gauges
.iter()
.any(|g| !g.labels.is_empty() && g.value == 18.0));
}
#[test]
#[cfg(feature = "timer")]
fn timer_snapshot_includes_stats() {
let r = Registry::new();
let t = r.get_or_create_timer("rpc");
t.record(std::time::Duration::from_millis(7));
let snap = snapshot(&r);
assert_eq!(snap.timers.len(), 1);
assert_eq!(snap.timers[0].stats.count, 1);
let body = render(&r);
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["timers"][0]["name"], "rpc");
}
#[test]
#[cfg(feature = "meter")]
fn rate_meter_snapshot_includes_stats() {
let r = Registry::new();
r.get_or_create_rate_meter("qps").tick_n(5);
let snap = snapshot(&r);
assert_eq!(snap.rate_meters.len(), 1);
assert_eq!(snap.rate_meters[0].stats.total_events, 5);
}
#[test]
fn cardinality_snapshot_reports_cap_and_overflows() {
let r = Registry::new();
r.set_cardinality_cap(1);
#[cfg(feature = "count")]
{
let _ = r.get_or_create_counter_with("c", &LabelSet::from([("k", "1")]));
let _ = r.get_or_create_counter_with("c", &LabelSet::from([("k", "2")]));
}
let snap = snapshot(&r);
assert_eq!(snap.cardinality.cap, 1);
#[cfg(feature = "count")]
{
assert_eq!(snap.cardinality.current, 1);
assert!(snap.cardinality.overflows >= 1);
}
}
#[test]
#[cfg(feature = "histogram")]
fn histogram_snapshot_round_trips() {
let r = Registry::new();
r.configure_histogram("rtt", [0.01, 0.05]);
let h = r.get_or_create_histogram("rtt");
h.observe(0.005);
h.observe(0.03);
let body = render(&r);
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["histograms"][0]["name"], "rtt");
assert_eq!(v["histograms"][0]["histogram"]["count"], 2);
}
}