use crate::utils::get_hostname;
use super::{MetricBuilder, MetricExporter};
pub const UP_METRIC: &str = "all_smi_up";
pub const BUILD_INFO_METRIC: &str = "all_smi_build_info";
pub struct ExporterStatusMetricExporter {
ready: bool,
}
impl ExporterStatusMetricExporter {
pub fn new(ready: bool) -> Self {
Self { ready }
}
}
impl MetricExporter for ExporterStatusMetricExporter {
fn export_metrics(&self) -> String {
let hostname = get_hostname();
let up_labels = [
("instance", hostname.as_str()),
("hostname", hostname.as_str()),
];
let mut builder = MetricBuilder::new();
builder
.help(
UP_METRIC,
"1 once the exporter has completed a collection cycle, 0 before that",
)
.type_(UP_METRIC, "gauge")
.metric(UP_METRIC, &up_labels, u8::from(self.ready));
let build_labels = [
("instance", hostname.as_str()),
("hostname", hostname.as_str()),
("version", env!("CARGO_PKG_VERSION")),
("os", std::env::consts::OS),
("arch", std::env::consts::ARCH),
];
builder
.help(
BUILD_INFO_METRIC,
"Build information for the running all-smi binary; always 1",
)
.type_(BUILD_INFO_METRIC, "gauge")
.metric(BUILD_INFO_METRIC, &build_labels, 1);
builder.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn not_ready_emits_up_zero() {
let rendered = ExporterStatusMetricExporter::new(false).export_metrics();
assert!(
rendered.contains("} 0\n"),
"all_smi_up must be 0 before the first collection cycle: {rendered}"
);
assert!(rendered.contains("# TYPE all_smi_up gauge\n"));
assert!(rendered.contains("all_smi_up{instance="));
}
#[test]
fn ready_emits_up_one() {
let rendered = ExporterStatusMetricExporter::new(true).export_metrics();
let up_line = rendered
.lines()
.find(|l| l.starts_with("all_smi_up{"))
.expect("all_smi_up sample line");
assert!(up_line.ends_with(" 1"), "expected up=1, got {up_line}");
}
#[test]
fn always_emits_something_in_both_states() {
for ready in [false, true] {
let rendered = ExporterStatusMetricExporter::new(ready).export_metrics();
assert!(!rendered.is_empty());
assert!(rendered.contains("all_smi_build_info{"));
}
}
#[test]
fn build_info_carries_version_os_and_arch() {
let rendered = ExporterStatusMetricExporter::new(true).export_metrics();
let line = rendered
.lines()
.find(|l| l.starts_with("all_smi_build_info{"))
.expect("build info sample line");
assert!(
line.contains(&format!("version=\"{}\"", env!("CARGO_PKG_VERSION"))),
"missing version label: {line}"
);
assert!(line.contains(&format!("os=\"{}\"", std::env::consts::OS)));
assert!(line.contains(&format!("arch=\"{}\"", std::env::consts::ARCH)));
assert!(line.ends_with(" 1"), "build info must always be 1: {line}");
}
#[test]
fn both_families_share_one_host_identity() {
let hostname = get_hostname();
let rendered = ExporterStatusMetricExporter::new(true).export_metrics();
for family in ["all_smi_up{", "all_smi_build_info{"] {
let line = rendered
.lines()
.find(|l| l.starts_with(family))
.unwrap_or_else(|| panic!("missing {family}"));
assert!(line.contains(&format!("instance=\"{hostname}\"")), "{line}");
assert!(line.contains(&format!("hostname=\"{hostname}\"")), "{line}");
}
}
}