use jiff::Timestamp;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatKind {
Gauge,
Counter,
}
impl StatKind {
pub fn prometheus(self) -> &'static str {
match self {
StatKind::Gauge => "gauge",
StatKind::Counter => "counter",
}
}
pub fn munin(self) -> &'static str {
match self {
StatKind::Gauge => "GAUGE",
StatKind::Counter => "COUNTER",
}
}
}
#[derive(Debug, Clone)]
pub struct Stat {
pub name: &'static str,
pub value: f64,
pub kind: StatKind,
pub labels: Vec<(&'static str, String)>,
pub help: Option<String>,
pub group: Option<&'static str>,
pub namespace: Option<&'static str>,
}
impl Stat {
pub fn gauge(name: &'static str, value: f64) -> Self {
Self {
name,
value,
kind: StatKind::Gauge,
labels: Vec::new(),
help: None,
group: None,
namespace: None,
}
}
pub fn counter(name: &'static str, value: f64) -> Self {
Self {
name,
value,
kind: StatKind::Counter,
labels: Vec::new(),
help: None,
group: None,
namespace: None,
}
}
pub fn label(mut self, key: &'static str, value: impl Into<String>) -> Self {
self.labels.push((key, value.into()));
self
}
pub fn help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
pub fn group(mut self, group: &'static str) -> Self {
self.group = Some(group);
self
}
pub fn namespace(mut self, namespace: &'static str) -> Self {
self.namespace = Some(namespace);
self
}
pub fn namespace_or<'a>(&self, check: &'a str) -> &'a str {
match self.namespace {
Some(ns) => ns,
None => check,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct StatusCounts {
pub passing: u32,
pub warning: u32,
pub failing: u32,
pub skipped: u32,
pub broken: u32,
}
impl StatusCounts {
pub fn total(&self) -> u32 {
self.passing + self.warning + self.failing + self.skipped + self.broken
}
pub fn active(&self) -> u32 {
self.total() - self.skipped
}
pub fn by_state(&self) -> [(&'static str, u32); 5] {
[
("passing", self.passing),
("warning", self.warning),
("failing", self.failing),
("skipped", self.skipped),
("broken", self.broken),
]
}
}
#[derive(Debug, Clone)]
pub struct MetricsSnapshot {
pub computed_at: Timestamp,
pub stats: Vec<(&'static str, Stat)>,
pub counts: StatusCounts,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gauge_builder_defaults() {
let s = Stat::gauge("age_seconds", 42.0);
assert_eq!(s.name, "age_seconds");
assert_eq!(s.value, 42.0);
assert_eq!(s.kind, StatKind::Gauge);
assert!(s.labels.is_empty());
assert!(s.help.is_none());
}
#[test]
fn counter_kind() {
assert_eq!(Stat::counter("requests_total", 1.0).kind, StatKind::Counter);
}
#[test]
fn labels_keep_insertion_order() {
let s = Stat::gauge("jobs", 3.0)
.label("status", "Queued")
.label("queue", "fhir");
assert_eq!(
s.labels,
vec![
("status", "Queued".to_string()),
("queue", "fhir".to_string()),
]
);
}
#[test]
fn help_is_attached() {
let s = Stat::gauge("x", 1.0).help("a thing");
assert_eq!(s.help.as_deref(), Some("a thing"));
}
#[test]
fn group_defaults_none_and_is_attachable() {
assert!(Stat::gauge("x", 1.0).group.is_none());
assert_eq!(
Stat::gauge("used_bytes", 1.0).group("bytes").group,
Some("bytes")
);
}
#[test]
fn namespace_defaults_to_check_and_overrides() {
assert_eq!(
Stat::gauge("x", 1.0).namespace_or("http_errors"),
"http_errors"
);
assert_eq!(
Stat::gauge("x", 1.0)
.namespace("http")
.namespace_or("http_errors"),
"http"
);
}
#[test]
fn kind_wire_tokens() {
assert_eq!(StatKind::Gauge.prometheus(), "gauge");
assert_eq!(StatKind::Counter.prometheus(), "counter");
assert_eq!(StatKind::Gauge.munin(), "GAUGE");
assert_eq!(StatKind::Counter.munin(), "COUNTER");
}
}