use std::{fmt::Debug, time::Duration};
use chrono::NaiveDateTime;
use prometheus_client::{
collector::Collector,
metrics::{gauge::ConstGauge, info::Info},
registry::{Registry, Unit},
};
use serde_with::{serde_as, DisplayFromStr};
use crate::{
prometheus_ext::{AsMetrics, EncodeExt},
MikrotikClient,
};
impl MikrotikClient {
#[tracing::instrument(level = "debug", err)]
pub async fn get_resource(&self) -> anyhow::Result<Resource> {
self.get_all("/rest/system/resource").await
}
#[tracing::instrument(level = "debug", err)]
pub async fn get_health(&self) -> anyhow::Result<Vec<HealthRecord>> {
self.get_all("/rest/system/health").await
}
}
#[serde_as]
#[derive(serde::Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct Resource {
pub architecture_name: String, pub board_name: String, pub cpu: String, pub platform: String, pub factory_software: String, pub version: String, #[serde_as(as = "DisplayFromStr")]
#[serde(default)]
pub bad_blocks: f64,
#[serde_as(as = "DisplayFromStr")]
pub cpu_count: u8,
#[serde_as(as = "DisplayFromStr")]
pub cpu_frequency: u64,
#[serde_as(as = "DisplayFromStr")]
pub cpu_load: u16,
#[serde_as(as = "MikrotikBuildTimeDeserialize")]
pub build_time: NaiveDateTime,
#[serde_as(as = "MikrotikUptimeDeserialize")]
pub uptime: Duration,
#[serde_as(as = "DisplayFromStr")]
pub total_hdd_space: u64,
#[serde_as(as = "DisplayFromStr")]
pub free_hdd_space: u64,
#[serde_as(as = "DisplayFromStr")]
pub total_memory: u64, #[serde_as(as = "DisplayFromStr")]
pub free_memory: u64, #[serde_as(as = "Option<DisplayFromStr>")]
pub write_sect_since_reboot: Option<u64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub write_sect_total: Option<u64>,
}
impl AsMetrics for Resource {
fn register_as_metrics(self: Box<Self>, registry: &mut Registry) {
registry
.sub_registry_with_prefix("resource")
.register_collector(self);
}
}
impl Collector for Resource {
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
fn encode(
&self,
mut encoder: prometheus_client::encoding::DescriptorEncoder,
) -> Result<(), std::fmt::Error> {
let Resource {
architecture_name,
board_name,
cpu,
platform,
factory_software,
version,
bad_blocks,
cpu_count,
cpu_frequency,
cpu_load,
build_time,
uptime,
total_hdd_space,
free_hdd_space,
total_memory,
free_memory,
write_sect_since_reboot,
write_sect_total,
} = self;
Info::new(vec![
("architecture_name", architecture_name.as_str()),
("board_name", board_name.as_str()),
("cpu", cpu.as_str()),
("platform", platform.as_str()),
("factory_software", factory_software.as_str()),
("version", version.as_str()),
("build_time", build_time.to_string().as_str()),
])
.encode_e(&mut encoder, "", "")?;
for (name, value) in [
("bad_blocks", *bad_blocks),
("cpu_count", f64::from(*cpu_count)),
("cpu_frequency", *cpu_frequency as f64),
("cpu_load", f64::from(*cpu_load)),
("uptime", uptime.as_secs() as f64),
("total_hdd_space", *total_hdd_space as f64),
("free_hdd_space", *free_hdd_space as f64),
("total_memory", *total_memory as f64),
("free_memory", *free_memory as f64),
] {
ConstGauge::new(value).encode_e(&mut encoder, name, "")?;
}
if let Some(write_sect_since_reboot) = write_sect_since_reboot {
ConstGauge::new(*write_sect_since_reboot as f64).encode_e(
&mut encoder,
"write_sect_since_reboot",
"",
)?;
}
if let Some(write_sect_total) = write_sect_total {
ConstGauge::new(*write_sect_total as f64).encode_e(
&mut encoder,
"write_sect_total",
"",
)?;
}
Ok(())
}
}
#[serde_as]
#[derive(serde::Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct HealthRecord {
#[serde(rename = ".id")]
pub id: String,
pub name: String,
#[serde(rename = "type")]
pub unit: String,
#[serde_as(as = "DisplayFromStr")]
pub value: f64,
}
impl AsMetrics for HealthRecord {
fn register_as_metrics(self: Box<Self>, registry: &mut Registry) {
registry
.sub_registry_with_prefix("health")
.sub_registry_with_label(("name".into(), self.name.clone().into()))
.register_collector(self);
}
}
impl Collector for HealthRecord {
fn encode(
&self,
mut encoder: prometheus_client::encoding::DescriptorEncoder,
) -> Result<(), std::fmt::Error> {
let (unit, name) = match self.unit.as_str() {
"C" => (Unit::Celsius, "temperature"),
"V" => (Unit::Volts, "voltage"),
_ => {
(
Unit::Other(self.unit.to_ascii_lowercase()),
self.name.rsplit(['-', '_']).next().unwrap_or_default(),
)
},
};
ConstGauge::new(self.value).encode_e_unit(&mut encoder, name, "", &unit)?;
Ok(())
}
}
struct MikrotikBuildTimeDeserialize;
impl<'de> serde_with::DeserializeAs<'de, NaiveDateTime> for MikrotikBuildTimeDeserialize {
fn deserialize_as<D>(deserializer: D) -> Result<NaiveDateTime, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s: &str = serde::de::Deserialize::deserialize(deserializer)?;
NaiveDateTime::parse_from_str(s, "%b/%d/%Y %H:%M:%S").map_err(serde::de::Error::custom)
}
}
struct MikrotikUptimeDeserialize;
impl<'de> serde_with::DeserializeAs<'de, Duration>
for crate::mikrotik_api::resource::MikrotikUptimeDeserialize
{
fn deserialize_as<D>(deserializer: D) -> Result<Duration, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s: &str = serde::de::Deserialize::deserialize(deserializer)?;
humantime::parse_duration(s).map_err(serde::de::Error::custom)
}
}