openzeppelin_relayer/api/routes/
metrics.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
//! This module provides HTTP endpoints for interacting with system metrics.
//!
//! # Endpoints
//!
//! - `/metrics`: Returns a list of all available metric names in JSON format.
//! - `/metrics/{metric_name}`: Returns the details of a specific metric in plain text format.
//! - `/debug/metrics/scrape`: Triggers an update of system metrics and returns the result in plain
//!   text format.
//!
//! # Usage
//!
//! These endpoints are designed to be used with a Prometheus server to scrape and monitor system
//! metrics.

use crate::metrics::{update_system_metrics, REGISTRY};
use actix_web::{get, web, HttpResponse, Responder};
use prometheus::{Encoder, TextEncoder};

/// Metrics routes implementation
///
/// Note: OpenAPI documentation for these endpoints can be found in the `openapi.rs` file
/// Returns a list of all available metric names in JSON format.
///
/// # Returns
///
/// An `HttpResponse` containing a JSON array of metric names.
#[utoipa::path(
    get,
    path = "/metrics",
    tag = "Metrics",
    responses(
        (status = 200, description = "Metric names list", body = Vec<String>),
        (status = 401, description = "Unauthorized"),
    )
)]
#[get("/metrics")]
async fn list_metrics() -> impl Responder {
    // Gather the metric families from the registry and extract metric names.
    let metric_families = REGISTRY.gather();
    let metric_names: Vec<String> = metric_families
        .iter()
        .map(|mf| mf.get_name().to_string())
        .collect();
    HttpResponse::Ok().json(metric_names)
}

/// Returns the details of a specific metric in plain text format.
///
/// # Parameters
///
/// - `path`: The name of the metric to retrieve details for.
///
/// # Returns
///
/// An `HttpResponse` containing the metric details in plain text, or a 404 error if the metric is
/// not found.
#[utoipa::path(
    get,
    path = "/metrics/{metric_name}",
    tag = "Metrics",
    params(
        ("metric_name" = String, Path, description = "Name of the metric to retrieve, e.g. utopia_transactions_total")
    ),
    responses(
        (status = 200, description = "Metric details in Prometheus text format", content_type = "text/plain", body = String),
        (status = 401, description = "Unauthorized - missing or invalid API key"),
        (status = 403, description = "Forbidden - insufficient permissions to access this metric"),
        (status = 404, description = "Metric not found"),
        (status = 429, description = "Too many requests - rate limit for metrics access exceeded")
    ),
    security(
        ("bearer_auth" = ["metrics:read"])
    )
)]
#[get("/metrics/{metric_name}")]
async fn metric_detail(path: web::Path<String>) -> impl Responder {
    let metric_name = path.into_inner();
    let metric_families = REGISTRY.gather();

    for mf in metric_families {
        if mf.get_name() == metric_name {
            let encoder = TextEncoder::new();
            let mut buffer = Vec::new();
            if let Err(e) = encoder.encode(&[mf], &mut buffer) {
                return HttpResponse::InternalServerError().body(format!("Encoding error: {}", e));
            }
            return HttpResponse::Ok()
                .content_type(encoder.format_type())
                .body(buffer);
        }
    }
    HttpResponse::NotFound().body("Metric not found")
}

/// Triggers an update of system metrics and returns the result in plain text format.
///
/// # Returns
///
/// An `HttpResponse` containing the updated metrics in plain text, or an error message if the
/// update fails.
#[utoipa::path(
    get,
    path = "/debug/metrics/scrape",
    tag = "Metrics",
    responses(
        (status = 200, description = "Complete metrics in Prometheus exposition format", content_type = "text/plain",   body = String),
        (status = 401, description = "Unauthorized")
    )
)]
#[get("/debug/metrics/scrape")]
async fn scrape_metrics() -> impl Responder {
    update_system_metrics();
    match crate::metrics::gather_metrics() {
        Ok(body) => HttpResponse::Ok().content_type("text/plain;").body(body),
        Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)),
    }
}

/// Initializes the HTTP services for the metrics module.
///
/// # Parameters
///
/// - `cfg`: The service configuration to which the metrics services will be added.
pub fn init(cfg: &mut web::ServiceConfig) {
    cfg.service(list_metrics);
    cfg.service(metric_detail);
    cfg.service(scrape_metrics);
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::{test, App};
    use prometheus::{Counter, Opts, Registry};

    // Helper function to create a test registry with a sample metric
    fn setup_test_registry() -> Registry {
        let registry = Registry::new();
        let counter = Counter::with_opts(Opts::new("test_counter", "A test counter")).unwrap();
        registry.register(Box::new(counter.clone())).unwrap();
        counter.inc(); // Set some value
        registry
    }

    // Mock implementation for list_metrics that uses our test registry
    async fn mock_list_metrics() -> impl Responder {
        // Use our test registry instead of the global one
        let registry = setup_test_registry();
        let metric_families = registry.gather();

        let metric_names: Vec<String> = metric_families
            .iter()
            .map(|mf| mf.get_name().to_string())
            .collect();

        HttpResponse::Ok().json(metric_names)
    }

    #[actix_web::test]
    async fn test_list_metrics() {
        // Create a test app with our mock handler
        let app = test::init_service(
            App::new().service(web::resource("/metrics").route(web::get().to(mock_list_metrics))),
        )
        .await;

        // Make request to list metrics
        let req = test::TestRequest::get().uri("/metrics").to_request();
        let resp = test::call_service(&app, req).await;

        // Verify response
        assert!(resp.status().is_success());

        // Parse response body as JSON
        let body = test::read_body(resp).await;
        let metric_names: Vec<String> = serde_json::from_slice(&body).unwrap();

        // Verify our test metric is in the list
        assert!(metric_names.contains(&"test_counter".to_string()));
    }

    // Mock implementation of the metric_detail handler for testing
    async fn mock_metric_detail(path: web::Path<String>) -> impl Responder {
        let metric_name = path.into_inner();

        // Create a test registry with our test_counter
        let registry = setup_test_registry();
        let metric_families = registry.gather();

        for mf in metric_families {
            if mf.get_name() == metric_name {
                let encoder = TextEncoder::new();
                let mut buffer = Vec::new();
                if let Err(e) = encoder.encode(&[mf], &mut buffer) {
                    return HttpResponse::InternalServerError()
                        .body(format!("Encoding error: {}", e));
                }
                return HttpResponse::Ok()
                    .content_type(encoder.format_type())
                    .body(buffer);
            }
        }
        HttpResponse::NotFound().body("Metric not found")
    }

    #[actix_web::test]
    async fn test_metric_detail() {
        // Create a test app with our mock handler
        let app = test::init_service(App::new().service(
            web::resource("/metrics/{metric_name}").route(web::get().to(mock_metric_detail)),
        ))
        .await;

        // Make request for our test metric
        let req = test::TestRequest::get()
            .uri("/metrics/test_counter")
            .to_request();
        let resp = test::call_service(&app, req).await;

        // Verify response
        assert!(resp.status().is_success());

        // Check that response contains our metric
        let body = test::read_body(resp).await;
        let body_str = String::from_utf8(body.to_vec()).unwrap();
        assert!(body_str.contains("test_counter"));
    }

    #[actix_web::test]
    async fn test_metric_detail_not_found() {
        // Create a test app with our mock handler
        let app = test::init_service(App::new().service(
            web::resource("/metrics/{metric_name}").route(web::get().to(mock_metric_detail)),
        ))
        .await;

        // Make request for a non-existent metric
        let req = test::TestRequest::get()
            .uri("/metrics/nonexistent")
            .to_request();
        let resp = test::call_service(&app, req).await;

        // Verify we get a 404 response
        assert_eq!(resp.status(), 404);
    }

    #[actix_web::test]
    async fn test_scrape_metrics() {
        // Create a test app with our endpoints
        let app = test::init_service(App::new().service(scrape_metrics)).await;

        // Make request to scrape metrics
        let req = test::TestRequest::get()
            .uri("/debug/metrics/scrape")
            .to_request();
        let resp = test::call_service(&app, req).await;

        // Verify response status
        assert!(resp.status().is_success());
    }

    #[actix_web::test]
    async fn test_scrape_metrics_error() {
        // We need to mock the gather_metrics function to return an error
        // This would typically be done with a mocking framework
        // For this example, we'll create a custom handler that simulates the error

        async fn mock_scrape_metrics_error() -> impl Responder {
            // Simulate an error from gather_metrics
            HttpResponse::InternalServerError().body("Error: test error")
        }

        // Create a test app with our mock error handler
        let app = test::init_service(App::new().service(
            web::resource("/debug/metrics/scrape").route(web::get().to(mock_scrape_metrics_error)),
        ))
        .await;

        // Make request to scrape metrics
        let req = test::TestRequest::get()
            .uri("/debug/metrics/scrape")
            .to_request();
        let resp = test::call_service(&app, req).await;

        // Verify we get a 500 response
        assert_eq!(resp.status(), 500);

        // Check that response contains our error message
        let body = test::read_body(resp).await;
        let body_str = String::from_utf8(body.to_vec()).unwrap();
        assert!(body_str.contains("Error: test error"));
    }

    #[actix_web::test]
    async fn test_init() {
        // Create a test app with our init function
        let app = test::init_service(App::new().configure(init)).await;

        // Test each endpoint to ensure they were properly registered

        // Test list_metrics endpoint
        let req = test::TestRequest::get().uri("/metrics").to_request();
        let resp = test::call_service(&app, req).await;

        // We expect this to succeed since list_metrics should work with any registry state
        assert!(resp.status().is_success());

        // Test metric_detail endpoint - we expect a 404 since test_counter doesn't exist in global registry
        let req = test::TestRequest::get()
            .uri("/metrics/test_counter")
            .to_request();
        let resp = test::call_service(&app, req).await;

        // We expect a 404 Not Found since test_counter doesn't exist in the global registry
        assert_eq!(resp.status(), 404);

        // Test scrape_metrics endpoint
        let req = test::TestRequest::get()
            .uri("/debug/metrics/scrape")
            .to_request();
        let resp = test::call_service(&app, req).await;
        // This should succeed as it doesn't depend on specific metrics existing
        assert!(resp.status().is_success());
    }

    #[actix_web::test]
    async fn test_metric_detail_encoding_error() {
        // Create a mock handler that simulates an encoding error
        async fn mock_metric_detail_with_encoding_error(path: web::Path<String>) -> impl Responder {
            let metric_name = path.into_inner();

            // Create a test registry with our test_counter
            let registry = setup_test_registry();
            let metric_families = registry.gather();

            for mf in metric_families {
                if mf.get_name() == metric_name {
                    // Simulate an encoding error by returning an error response directly
                    return HttpResponse::InternalServerError()
                        .body("Encoding error: simulated error");
                }
            }
            HttpResponse::NotFound().body("Metric not found")
        }

        // Create a test app with our mock error handler
        let app = test::init_service(
            App::new().service(
                web::resource("/metrics/{metric_name}")
                    .route(web::get().to(mock_metric_detail_with_encoding_error)),
            ),
        )
        .await;

        // Make request for our test metric - use "test_counter" which we know exists in setup_test_registry
        let req = test::TestRequest::get()
            .uri("/metrics/test_counter")
            .to_request();
        let resp = test::call_service(&app, req).await;

        // Verify we get a 500 response
        assert_eq!(resp.status(), 500);

        // Check that response contains our error message
        let body = test::read_body(resp).await;
        let body_str = String::from_utf8(body.to_vec()).unwrap();
        assert!(body_str.contains("Encoding error: simulated error"));
    }
}