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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use std::{borrow::Cow, iter, sync::Arc};

use axum::{extract::State, response::IntoResponse};
use futures::stream::{FuturesUnordered, StreamExt};
use prometheus_client::{
    metrics::{gauge::ConstGauge, info::Info},
    registry::Registry,
};
use url::Url;

use crate::{
    config_file::ExporterConfig,
    mikrotik_api::clock::Clock,
    prometheus_ext::AsMetrics,
    with_timezone::WithTimezoneTrait,
    AppState,
    MikrotikClient,
};

#[tracing::instrument(skip(state), level = "debug", err)]
pub async fn export_metrics(
    State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, String> {
    let router_results = state
        .routers
        .iter()
        .filter(|(_, _, exporter_config)| exporter_config.enabled)
        .map(scrape_router)
        .collect::<FuturesUnordered<_>>()
        .collect::<Vec<_>>()
        .await;

    let mut metrics_registry = Registry::with_prefix("mikrotik_ros");

    for (name, endpoint, metrics) in router_results {
        let router_registry = metrics_registry.sub_registry_with_label((
            Cow::from("target"),
            Cow::from(endpoint.authority().to_owned()),
        ));

        router_registry.register(
            "target_name",
            "Name of the target router from exporter configuration",
            Info::new([("target_name", name.clone())]),
        );

        let mut success = |s: u32| {
            router_registry.register(
                "scrape_success",
                "Was the scrape successful",
                ConstGauge::new(s),
            );
        };

        let (clock, metrics) = match metrics {
            Err(error) => {
                tracing::error!(
                    endpoint = display(endpoint),
                    router_name = display(name),
                    error = %error,
                    error.debug = ?error,
                    "Failed to export metrics"
                );
                success(0);
                continue;
            },
            Ok(metrics) => metrics,
        };
        success(1);

        Box::new(clock).register_as_metrics(router_registry);

        for metric in metrics {
            metric.register_as_metrics(router_registry);
        }
    }


    let mut out = String::new();
    prometheus_client::encoding::text::encode(&mut out, &metrics_registry)
        .map_err(|e| format!("Failed to encode metrics. See log\n\n{e}"))?;

    Ok(out)
}

#[allow(clippy::as_conversions)]
async fn scrape_router(
    params: &(String, MikrotikClient, ExporterConfig),
) -> (
    String,
    Url,
    anyhow::Result<(Clock, Vec<Box<dyn AsMetrics + Send>>)>,
) {
    let name = params.0.clone();
    let api = &params.1;
    let endpoint = api.endpoint().clone();
    let clock = api.get_clock().await;

    let clock = match clock {
        Ok(clock) => clock,
        Err(err) => return (name, endpoint, Err(err)),
    };

    let metrics: anyhow::Result<(Clock, Vec<Box<dyn AsMetrics + Send>>)> = async {
        let metrics: Vec<Box<dyn AsMetrics + Send>> =
            iter::once(Box::new(api.get_identity().await?) as Box<dyn AsMetrics + Send>)
                .chain(api.get_interfaces().await?.into_iter().map(
                    |i| -> Box<dyn AsMetrics + Send> {
                        Box::new(i.with_timezone(clock.gmt_offset))
                    },
                ))
                .chain(iter::once(
                    Box::new(api.get_resource().await?) as Box<dyn AsMetrics + Send>
                ))
                .chain(
                    api.get_health()
                        .await?
                        .into_iter()
                        .map(|h| -> Box<dyn AsMetrics + Send> { Box::new(h) }),
                )
                .chain(api.get_ethernet_and_monitor().await?.into_iter().flat_map(
                    |(eth, monitor)| -> [Box<dyn AsMetrics + Send>; 2] {
                        [Box::new(eth), Box::new(monitor)]
                    },
                ))
                .collect();

        Ok((clock, metrics))
    }
    .await;

    (name, endpoint, metrics)
}