Skip to main content

hoprd_api/
root.rs

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/// Retrieve Prometheus metrics from the running node.
28#[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}