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 = ¶ms.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)
}