1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
use std::{borrow::Cow, collections::HashMap, fmt::Debug};

use chrono::{NaiveDateTime, TimeZone};
use prometheus_client::{
    collector::Collector,
    encoding::DescriptorEncoder,
    metrics::gauge::ConstGauge,
    registry::Registry,
};
use serde_with::serde_as;

use super::deserialize_bool;
use crate::{
    prometheus_ext::{encode_extra, AsMetrics, EncodeExt},
    with_timezone::WithTimezone,
    MikrotikClient,
};

impl MikrotikClient {
    #[tracing::instrument(level = "debug", err)]
    pub async fn get_interfaces(&self) -> anyhow::Result<Vec<Interface>> {
        self.get_all("/rest/interface").await
    }
}

#[serde_as]
#[derive(serde::Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct Interface {
    #[serde(rename = ".id")]
    pub id: String,
    pub name: String,

    pub mac_address: Option<eui48::MacAddress>,

    #[serde(deserialize_with = "deserialize_bool")]
    pub disabled: bool,

    #[serde_as(deserialize_as = "Option<MikrotikDatetimeDeserialize>")]
    pub last_link_up_time: Option<NaiveDateTime>,

    #[serde(flatten)]
    pub extra: HashMap<String, String>,
}

impl<T> AsMetrics for WithTimezone<Interface, T>
where
    T: TimeZone + Debug + Sync + Send + 'static + Copy,
{
    fn register_as_metrics(self: Box<Self>, registry: &mut Registry) {
        registry
            .sub_registry_with_prefix("interface")
            .sub_registry_with_label(("name".into(), Cow::from(self.0.name.clone())))
            .register_collector(self);
    }
}
impl<T> Collector for WithTimezone<Interface, T>
where
    T: TimeZone + Debug + Sync + Send + 'static + Copy,
{
    fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
        const FIELD_BLACKLIST: [&str; 1] = ["test"];
        let WithTimezone(interface, tz) = self;

        let Interface {
            id: _,
            name: _,
            mac_address,
            disabled,
            last_link_up_time,
            extra,
        } = interface;


        ConstGauge::new(i64::from(!(*disabled))).encode_e(&mut encoder, "enabled", "")?;


        if let Some(datetime) =
            last_link_up_time.and_then(|up_time| up_time.and_local_timezone(*tz).single())
        {
            ConstGauge::new(datetime.timestamp()).encode_e(
                &mut encoder,
                "last_link_up_time",
                "Last time the link went up",
            )?;
        };

        let mut info_fields = Vec::with_capacity(extra.len());

        if let Some(mac) = mac_address {
            info_fields.push(("mac_address".to_owned(), mac.to_canonical()));
        }

        encode_extra(&mut encoder, extra, info_fields, &FIELD_BLACKLIST)?;

        Ok(())
    }
}

struct MikrotikDatetimeDeserialize;

impl<'de> serde_with::DeserializeAs<'de, NaiveDateTime> for MikrotikDatetimeDeserialize {
    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, "%Y-%m-%d %H:%M:%S").map_err(serde::de::Error::custom)
    }
}