aster_forge_actix_observability/
lib.rs1#![cfg_attr(
8 not(test),
9 deny(
10 clippy::unwrap_used,
11 clippy::unreachable,
12 clippy::expect_used,
13 clippy::panic,
14 clippy::unimplemented,
15 clippy::todo
16 )
17)]
18
19use actix_web::Scope;
20
21pub fn configure_prometheus_route(scope: Scope) -> Scope {
27 #[cfg(feature = "prometheus")]
28 {
29 scope.route("/metrics", actix_web::web::get().to(prometheus_metrics))
30 }
31
32 #[cfg(not(feature = "prometheus"))]
33 {
34 scope
35 }
36}
37
38#[cfg(feature = "prometheus")]
39async fn prometheus_metrics() -> actix_web::HttpResponse {
40 if !aster_forge_metrics::prometheus::is_initialized() {
41 tracing::debug!("metrics probe failed because metrics are not initialized");
42 return actix_web::HttpResponse::ServiceUnavailable()
43 .body("metrics registry is not initialized");
44 }
45
46 match aster_forge_metrics::prometheus::export_metrics() {
47 Ok(body) => {
48 tracing::debug!(bytes = body.len(), "metrics probe exported metrics");
49 actix_web::HttpResponse::Ok()
50 .content_type("text/plain; version=0.0.4; charset=utf-8")
51 .body(body)
52 }
53 Err(error) => {
54 tracing::debug!(error = %error, "metrics probe export failed");
55 actix_web::HttpResponse::ServiceUnavailable().body(error)
56 }
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use actix_web::{App, http::StatusCode, test, web};
63
64 use super::configure_prometheus_route;
65
66 #[cfg(not(feature = "prometheus"))]
67 #[actix_web::test]
68 async fn prometheus_route_is_not_registered_without_feature() {
69 use actix_web::HttpResponse;
70
71 let app = test::init_service(App::new().service(configure_prometheus_route(
72 web::scope("").route("/healthz", web::get().to(HttpResponse::Ok)),
73 )))
74 .await;
75
76 let health =
77 test::call_service(&app, test::TestRequest::get().uri("/healthz").to_request()).await;
78 assert_eq!(health.status(), StatusCode::OK);
79
80 let metrics =
81 test::call_service(&app, test::TestRequest::get().uri("/metrics").to_request()).await;
82 assert_eq!(metrics.status(), StatusCode::NOT_FOUND);
83 }
84
85 #[cfg(feature = "prometheus")]
86 #[actix_web::test]
87 async fn prometheus_route_exports_text_after_registry_init() {
88 aster_forge_metrics::prometheus::init_metrics()
89 .expect("metrics registry should initialize");
90 let recorder = aster_forge_metrics::prometheus::PrometheusMetricsRecorder;
91 aster_forge_metrics::MetricsRecorder::record_http_request(
92 &recorder, "GET", "/healthz", 200, 0.01,
93 );
94 let app =
95 test::init_service(App::new().service(configure_prometheus_route(web::scope(""))))
96 .await;
97
98 let response =
99 test::call_service(&app, test::TestRequest::get().uri("/metrics").to_request()).await;
100 assert_eq!(response.status(), StatusCode::OK);
101
102 let body = test::read_body(response).await;
103 let body = std::str::from_utf8(&body).expect("metrics body should be utf-8");
104 assert!(body.contains("http_requests_total"));
105 assert!(body.contains("route=\"/healthz\""));
106 assert!(body.contains("process_uptime_seconds"));
107 }
108}