openzeppelin_relayer/api/routes/
metrics.rs

1//! This module provides HTTP endpoints for interacting with system metrics.
2//!
3//! # Endpoints
4//!
5//! - `/metrics`: Returns a list of all available metric names in JSON format.
6//! - `/metrics/{metric_name}`: Returns the details of a specific metric in plain text format.
7//! - `/debug/metrics/scrape`: Triggers an update of system metrics and returns the result in plain
8//!   text format.
9//!
10//! # Usage
11//!
12//! These endpoints are designed to be used with a Prometheus server to scrape and monitor system
13//! metrics.
14
15use crate::metrics::{update_system_metrics, REGISTRY};
16use actix_web::{get, web, HttpResponse, Responder};
17use prometheus::{Encoder, TextEncoder};
18
19/// Metrics routes implementation
20///
21/// Note: OpenAPI documentation for these endpoints can be found in the `openapi.rs` file
22/// Returns a list of all available metric names in JSON format.
23///
24/// # Returns
25///
26/// An `HttpResponse` containing a JSON array of metric names.
27#[utoipa::path(
28    get,
29    path = "/metrics",
30    tag = "Metrics",
31    responses(
32        (status = 200, description = "Metric names list", body = Vec<String>),
33        (status = 401, description = "Unauthorized"),
34    )
35)]
36#[get("/metrics")]
37async fn list_metrics() -> impl Responder {
38    // Gather the metric families from the registry and extract metric names.
39    let metric_families = REGISTRY.gather();
40    let metric_names: Vec<String> = metric_families
41        .iter()
42        .map(|mf| mf.name().to_string())
43        .collect();
44    HttpResponse::Ok().json(metric_names)
45}
46
47/// Returns the details of a specific metric in plain text format.
48///
49/// # Parameters
50///
51/// - `path`: The name of the metric to retrieve details for.
52///
53/// # Returns
54///
55/// An `HttpResponse` containing the metric details in plain text, or a 404 error if the metric is
56/// not found.
57#[utoipa::path(
58    get,
59    path = "/metrics/{metric_name}",
60    tag = "Metrics",
61    params(
62        ("metric_name" = String, Path, description = "Name of the metric to retrieve, e.g. utopia_transactions_total")
63    ),
64    responses(
65        (status = 200, description = "Metric details in Prometheus text format", content_type = "text/plain", body = String),
66        (status = 401, description = "Unauthorized - missing or invalid API key"),
67        (status = 403, description = "Forbidden - insufficient permissions to access this metric"),
68        (status = 404, description = "Metric not found"),
69        (status = 429, description = "Too many requests - rate limit for metrics access exceeded")
70    ),
71    security(
72        ("bearer_auth" = ["metrics:read"])
73    )
74)]
75#[get("/metrics/{metric_name}")]
76async fn metric_detail(path: web::Path<String>) -> impl Responder {
77    let metric_name = path.into_inner();
78    let metric_families = REGISTRY.gather();
79
80    for mf in metric_families {
81        if mf.name() == metric_name {
82            let encoder = TextEncoder::new();
83            let mut buffer = Vec::new();
84            if let Err(e) = encoder.encode(&[mf], &mut buffer) {
85                return HttpResponse::InternalServerError().body(format!("Encoding error: {}", e));
86            }
87            return HttpResponse::Ok()
88                .content_type(encoder.format_type())
89                .body(buffer);
90        }
91    }
92    HttpResponse::NotFound().body("Metric not found")
93}
94
95/// Triggers an update of system metrics and returns the result in plain text format.
96///
97/// # Returns
98///
99/// An `HttpResponse` containing the updated metrics in plain text, or an error message if the
100/// update fails.
101#[utoipa::path(
102    get,
103    path = "/debug/metrics/scrape",
104    tag = "Metrics",
105    responses(
106        (status = 200, description = "Complete metrics in Prometheus exposition format", content_type = "text/plain",   body = String),
107        (status = 401, description = "Unauthorized")
108    )
109)]
110#[get("/debug/metrics/scrape")]
111async fn scrape_metrics() -> impl Responder {
112    update_system_metrics();
113    match crate::metrics::gather_metrics() {
114        Ok(body) => HttpResponse::Ok().content_type("text/plain;").body(body),
115        Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)),
116    }
117}
118
119/// Initializes the HTTP services for the metrics module.
120///
121/// # Parameters
122///
123/// - `cfg`: The service configuration to which the metrics services will be added.
124pub fn init(cfg: &mut web::ServiceConfig) {
125    cfg.service(list_metrics);
126    cfg.service(metric_detail);
127    cfg.service(scrape_metrics);
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use actix_web::{test, App};
134    use prometheus::{Counter, Opts, Registry};
135
136    // Helper function to create a test registry with a sample metric
137    fn setup_test_registry() -> Registry {
138        let registry = Registry::new();
139        let counter = Counter::with_opts(Opts::new("test_counter", "A test counter")).unwrap();
140        registry.register(Box::new(counter.clone())).unwrap();
141        counter.inc(); // Set some value
142        registry
143    }
144
145    // Mock implementation for list_metrics that uses our test registry
146    async fn mock_list_metrics() -> impl Responder {
147        // Use our test registry instead of the global one
148        let registry = setup_test_registry();
149        let metric_families = registry.gather();
150
151        let metric_names: Vec<String> = metric_families
152            .iter()
153            .map(|mf| mf.name().to_string())
154            .collect();
155
156        HttpResponse::Ok().json(metric_names)
157    }
158
159    #[actix_web::test]
160    async fn test_list_metrics() {
161        // Create a test app with our mock handler
162        let app = test::init_service(
163            App::new().service(web::resource("/metrics").route(web::get().to(mock_list_metrics))),
164        )
165        .await;
166
167        // Make request to list metrics
168        let req = test::TestRequest::get().uri("/metrics").to_request();
169        let resp = test::call_service(&app, req).await;
170
171        // Verify response
172        assert!(resp.status().is_success());
173
174        // Parse response body as JSON
175        let body = test::read_body(resp).await;
176        let metric_names: Vec<String> = serde_json::from_slice(&body).unwrap();
177
178        // Verify our test metric is in the list
179        assert!(metric_names.contains(&"test_counter".to_string()));
180    }
181
182    // Mock implementation of the metric_detail handler for testing
183    async fn mock_metric_detail(path: web::Path<String>) -> impl Responder {
184        let metric_name = path.into_inner();
185
186        // Create a test registry with our test_counter
187        let registry = setup_test_registry();
188        let metric_families = registry.gather();
189
190        for mf in metric_families {
191            if mf.name() == metric_name {
192                let encoder = TextEncoder::new();
193                let mut buffer = Vec::new();
194                if let Err(e) = encoder.encode(&[mf], &mut buffer) {
195                    return HttpResponse::InternalServerError()
196                        .body(format!("Encoding error: {}", e));
197                }
198                return HttpResponse::Ok()
199                    .content_type(encoder.format_type())
200                    .body(buffer);
201            }
202        }
203        HttpResponse::NotFound().body("Metric not found")
204    }
205
206    #[actix_web::test]
207    async fn test_metric_detail() {
208        // Create a test app with our mock handler
209        let app = test::init_service(App::new().service(
210            web::resource("/metrics/{metric_name}").route(web::get().to(mock_metric_detail)),
211        ))
212        .await;
213
214        // Make request for our test metric
215        let req = test::TestRequest::get()
216            .uri("/metrics/test_counter")
217            .to_request();
218        let resp = test::call_service(&app, req).await;
219
220        // Verify response
221        assert!(resp.status().is_success());
222
223        // Check that response contains our metric
224        let body = test::read_body(resp).await;
225        let body_str = String::from_utf8(body.to_vec()).unwrap();
226        assert!(body_str.contains("test_counter"));
227    }
228
229    #[actix_web::test]
230    async fn test_metric_detail_not_found() {
231        // Create a test app with our mock handler
232        let app = test::init_service(App::new().service(
233            web::resource("/metrics/{metric_name}").route(web::get().to(mock_metric_detail)),
234        ))
235        .await;
236
237        // Make request for a non-existent metric
238        let req = test::TestRequest::get()
239            .uri("/metrics/nonexistent")
240            .to_request();
241        let resp = test::call_service(&app, req).await;
242
243        // Verify we get a 404 response
244        assert_eq!(resp.status(), 404);
245    }
246
247    #[actix_web::test]
248    async fn test_scrape_metrics() {
249        // Create a test app with our endpoints
250        let app = test::init_service(App::new().service(scrape_metrics)).await;
251
252        // Make request to scrape metrics
253        let req = test::TestRequest::get()
254            .uri("/debug/metrics/scrape")
255            .to_request();
256        let resp = test::call_service(&app, req).await;
257
258        // Verify response status
259        assert!(resp.status().is_success());
260    }
261
262    #[actix_web::test]
263    async fn test_scrape_metrics_error() {
264        // We need to mock the gather_metrics function to return an error
265        // This would typically be done with a mocking framework
266        // For this example, we'll create a custom handler that simulates the error
267
268        async fn mock_scrape_metrics_error() -> impl Responder {
269            // Simulate an error from gather_metrics
270            HttpResponse::InternalServerError().body("Error: test error")
271        }
272
273        // Create a test app with our mock error handler
274        let app = test::init_service(App::new().service(
275            web::resource("/debug/metrics/scrape").route(web::get().to(mock_scrape_metrics_error)),
276        ))
277        .await;
278
279        // Make request to scrape metrics
280        let req = test::TestRequest::get()
281            .uri("/debug/metrics/scrape")
282            .to_request();
283        let resp = test::call_service(&app, req).await;
284
285        // Verify we get a 500 response
286        assert_eq!(resp.status(), 500);
287
288        // Check that response contains our error message
289        let body = test::read_body(resp).await;
290        let body_str = String::from_utf8(body.to_vec()).unwrap();
291        assert!(body_str.contains("Error: test error"));
292    }
293
294    #[actix_web::test]
295    async fn test_init() {
296        // Create a test app with our init function
297        let app = test::init_service(App::new().configure(init)).await;
298
299        // Test each endpoint to ensure they were properly registered
300
301        // Test list_metrics endpoint
302        let req = test::TestRequest::get().uri("/metrics").to_request();
303        let resp = test::call_service(&app, req).await;
304
305        // We expect this to succeed since list_metrics should work with any registry state
306        assert!(resp.status().is_success());
307
308        // Test metric_detail endpoint - we expect a 404 since test_counter doesn't exist in global registry
309        let req = test::TestRequest::get()
310            .uri("/metrics/test_counter")
311            .to_request();
312        let resp = test::call_service(&app, req).await;
313
314        // We expect a 404 Not Found since test_counter doesn't exist in the global registry
315        assert_eq!(resp.status(), 404);
316
317        // Test scrape_metrics endpoint
318        let req = test::TestRequest::get()
319            .uri("/debug/metrics/scrape")
320            .to_request();
321        let resp = test::call_service(&app, req).await;
322        // This should succeed as it doesn't depend on specific metrics existing
323        assert!(resp.status().is_success());
324    }
325
326    #[actix_web::test]
327    async fn test_metric_detail_encoding_error() {
328        // Create a mock handler that simulates an encoding error
329        async fn mock_metric_detail_with_encoding_error(path: web::Path<String>) -> impl Responder {
330            let metric_name = path.into_inner();
331
332            // Create a test registry with our test_counter
333            let registry = setup_test_registry();
334            let metric_families = registry.gather();
335
336            for mf in metric_families {
337                if mf.name() == metric_name {
338                    // Simulate an encoding error by returning an error response directly
339                    return HttpResponse::InternalServerError()
340                        .body("Encoding error: simulated error");
341                }
342            }
343            HttpResponse::NotFound().body("Metric not found")
344        }
345
346        // Create a test app with our mock error handler
347        let app = test::init_service(
348            App::new().service(
349                web::resource("/metrics/{metric_name}")
350                    .route(web::get().to(mock_metric_detail_with_encoding_error)),
351            ),
352        )
353        .await;
354
355        // Make request for our test metric - use "test_counter" which we know exists in setup_test_registry
356        let req = test::TestRequest::get()
357            .uri("/metrics/test_counter")
358            .to_request();
359        let resp = test::call_service(&app, req).await;
360
361        // Verify we get a 500 response
362        assert_eq!(resp.status(), 500);
363
364        // Check that response contains our error message
365        let body = test::read_body(resp).await;
366        let body_str = String::from_utf8(body.to_vec()).unwrap();
367        assert!(body_str.contains("Encoding error: simulated error"));
368    }
369}