1use axum::{http::StatusCode, response::IntoResponse};
2
3use crate::{ApiError, ApiErrorStatus};
4
5#[cfg(feature = "telemetry")]
6fn collect_hopr_metrics() -> Result<String, ApiErrorStatus> {
7 hopr_metrics::gather_all_metrics()
8 .map(|metrics| {
9 metrics
10 .lines()
11 .filter(|line| {
12 !(line.starts_with("hopr_session_")
13 || line.starts_with("# HELP hopr_session_")
14 || line.starts_with("# TYPE hopr_session_"))
15 })
16 .collect::<Vec<_>>()
17 .join("\n")
18 })
19 .map_err(|_| ApiErrorStatus::UnknownFailure("Failed to gather metrics".into()))
20}
21
22#[cfg(not(feature = "telemetry"))]
23fn collect_hopr_metrics() -> Result<String, ApiErrorStatus> {
24 Err(ApiErrorStatus::UnknownFailure("BUILT WITHOUT METRICS SUPPORT".into()))
25}
26
27#[utoipa::path(
29 get,
30 path = const_format::formatcp!("/metrics"),
31 description = "Retrieve Prometheus metrics from the running node",
32 responses(
33 (status = 200, description = "Fetched node metrics", body = String),
34 (status = 401, description = "Invalid authorization token.", body = ApiError),
35 (status = 422, description = "Unknown failure", body = ApiError)
36 ),
37 security(
38 ("api_token" = []),
39 ("bearer_token" = [])
40 ),
41 tag = "Metrics"
42 )]
43pub(super) async fn metrics() -> impl IntoResponse {
44 match collect_hopr_metrics() {
45 Ok(metrics) => (StatusCode::OK, metrics).into_response(),
46 Err(error) => (StatusCode::UNPROCESSABLE_ENTITY, error).into_response(),
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use anyhow::Result;
53 use axum::{body::to_bytes, response::IntoResponse};
54
55 use super::*;
56
57 #[cfg(feature = "telemetry")]
58 #[tokio::test]
59 async fn collect_metrics_filters_out_session_metrics() -> Result<()> {
60 let session_metric_name = "hopr_session_metrics_endpoint_test".to_string();
61 let non_session_metric_name = "hopr_metrics_endpoint_test".to_string();
62
63 let session_metric =
64 hopr_metrics::MultiCounter::new(&session_metric_name, "session endpoint filtering test", &["session_id"])?;
65
66 let non_session_metric =
67 hopr_metrics::MultiCounter::new(&non_session_metric_name, "endpoint non-session metric test", &["kind"])?;
68
69 session_metric.increment(&["test-session"]);
70 non_session_metric.increment(&["test-kind"]);
71
72 let collected_metrics = collect_hopr_metrics()
73 .map_err(|error| anyhow::anyhow!("collect_hopr_metrics should return metrics: {error}"))?;
74
75 assert!(!collected_metrics.contains(&session_metric_name));
76 assert!(collected_metrics.contains(&non_session_metric_name));
77
78 let body = to_bytes(metrics().await.into_response().into_body(), usize::MAX).await?;
79 let body_text = String::from_utf8(body.to_vec())?;
80
81 assert!(!body_text.contains(&session_metric_name),);
82 assert!(body_text.contains(&non_session_metric_name));
83
84 Ok(())
85 }
86}