use std::{borrow::Cow, collections::HashMap, fmt::Error};
use anyhow::{bail, Context};
use iter_tools::Itertools;
use prometheus_client::{
collector::Collector,
encoding::DescriptorEncoder,
metrics::gauge::ConstGauge,
registry::Registry,
};
use serde_json::json;
use serde_with::serde_as;
use super::deserialize_bool;
use crate::{
prometheus_ext::{encode_extra, AsMetrics, EncodeExt},
MikrotikClient,
};
impl MikrotikClient {
#[tracing::instrument(level = "debug", err)]
pub async fn get_ethernet_and_monitor(&self) -> anyhow::Result<Vec<(Ethernet, Monitor)>> {
let ethernetes: Vec<Ethernet> = self
.get_all("/rest/interface/ethernet")
.await
.context("Ethernet fetch failed")?;
let eth_ids = ethernetes.iter().map(|e| e.id.clone()).join(",");
let mut url = self.endpoint.clone();
url.set_path("/rest/interface/ethernet/monitor");
let response = self
.client
.post(url)
.json(&json! ({"once": "1", "numbers": eth_ids}))
.basic_auth(&self.username, Some(&self.pass.0))
.send()
.await
.context("Monitor fetch failed")?;
let status = response.status();
if !status.is_success() {
let text = response.text().await.unwrap_or_default();
bail!("Failed to fetch monitor: {status} | {text}",)
}
let monitors: Vec<Monitor> = response.json().await.context("Monitor decode failed")?;
Ok(ethernetes.into_iter().zip(monitors.into_iter()).collect())
}
}
#[serde_as]
#[derive(serde::Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct Ethernet {
#[serde(rename = ".id")]
pub id: String,
pub name: String,
#[serde(deserialize_with = "deserialize_bool")]
pub disabled: bool,
pub mac_address: eui48::MacAddress,
pub orig_mac_address: eui48::MacAddress,
#[serde(flatten)]
pub extra: HashMap<String, String>,
}
impl AsMetrics for Ethernet {
fn register_as_metrics(self: Box<Self>, registry: &mut Registry) {
registry
.sub_registry_with_prefix("ethernet")
.sub_registry_with_label(("name".into(), Cow::from(self.name.clone())))
.register_collector(self);
}
}
impl Collector for Ethernet {
fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), Error> {
const FIELD_BLACKLIST: [&str; 0] = [];
let Ethernet {
id: _,
name: _,
disabled,
mac_address,
orig_mac_address,
extra,
} = self;
ConstGauge::new(i64::from(!(*disabled))).encode_e(&mut encoder, "enabled", "")?;
let mut info_fields = Vec::with_capacity(extra.len());
info_fields.push(("mac_address".to_owned(), mac_address.to_canonical()));
info_fields.push((
"original_mac_address".to_owned(),
orig_mac_address.to_canonical(),
));
encode_extra(&mut encoder, extra, vec![], &FIELD_BLACKLIST)?;
Ok(())
}
}
#[serde_as]
#[derive(serde::Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct Monitor {
pub name: String,
#[serde_as(as = "Option<MikrotikRateDeserialize>")]
pub rate: Option<f64>,
#[serde(flatten)]
pub extra: HashMap<String, String>,
}
impl AsMetrics for Monitor {
fn register_as_metrics(self: Box<Self>, registry: &mut Registry) {
registry
.sub_registry_with_prefix("ethernet_monitor")
.sub_registry_with_label(("name".into(), Cow::from(self.name.clone())))
.register_collector(self);
}
}
impl Collector for Monitor {
fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), Error> {
const FIELD_BLACKLIST: [&str; 3] = ["test", "eeprom", "eeprom_checksum"];
let Monitor {
name: _,
rate,
extra,
} = self;
if let Some(rate) = rate {
ConstGauge::new(*rate).encode_e(
&mut encoder,
"rate",
"Current connected rate for given ethernet",
)?;
};
encode_extra(&mut encoder, extra, vec![], &FIELD_BLACKLIST)?;
Ok(())
}
}
struct MikrotikRateDeserialize;
impl<'de> serde_with::DeserializeAs<'de, f64> for MikrotikRateDeserialize {
fn deserialize_as<D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s: String = serde::de::Deserialize::deserialize(deserializer)?;
Ok(
if let Some((number, postfix)) = s
.find(|c: char| !c.is_numeric() && c != '.')
.map(|i| s.split_at(i))
{
number.parse::<f64>().map_err(serde::de::Error::custom)?
* match postfix {
"Mbps" => 1_000_000_f64,
"Gbps" => 1_000_000_000_f64,
_ => {
return Err(serde::de::Error::custom(format!(
"Unknown rate postfix {postfix}"
)))
},
}
} else {
s.parse().map_err(serde::de::Error::custom)?
},
)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use serde::de::{value::StringDeserializer, IntoDeserializer};
use serde_with::DeserializeAs;
use super::*;
#[test]
#[allow(clippy::float_cmp)]
fn test_mikrotik_rate_deserialize() {
for (input, expected) in [
("10Mbps", 10_000_000.0),
("100Mbps", 100_000_000.0),
("1Gbps", 1_000_000_000.0),
("2.5Gbps", 2_500_000_000.0),
("10Gbps", 10_000_000_000.0),
] {
let deserializer: StringDeserializer<serde::de::value::Error> =
input.to_owned().into_deserializer();
assert_eq!(
MikrotikRateDeserialize::deserialize_as(deserializer).unwrap(),
expected
);
}
}
}