openzeppelin_relayer/api/routes/
metrics.rs1use crate::metrics::{update_system_metrics, REGISTRY};
16use actix_web::{get, web, HttpResponse, Responder};
17use prometheus::{Encoder, TextEncoder};
18
19#[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 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#[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#[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
119pub 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 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(); registry
143 }
144
145 async fn mock_list_metrics() -> impl Responder {
147 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 let app = test::init_service(
163 App::new().service(web::resource("/metrics").route(web::get().to(mock_list_metrics))),
164 )
165 .await;
166
167 let req = test::TestRequest::get().uri("/metrics").to_request();
169 let resp = test::call_service(&app, req).await;
170
171 assert!(resp.status().is_success());
173
174 let body = test::read_body(resp).await;
176 let metric_names: Vec<String> = serde_json::from_slice(&body).unwrap();
177
178 assert!(metric_names.contains(&"test_counter".to_string()));
180 }
181
182 async fn mock_metric_detail(path: web::Path<String>) -> impl Responder {
184 let metric_name = path.into_inner();
185
186 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 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 let req = test::TestRequest::get()
216 .uri("/metrics/test_counter")
217 .to_request();
218 let resp = test::call_service(&app, req).await;
219
220 assert!(resp.status().is_success());
222
223 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 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 let req = test::TestRequest::get()
239 .uri("/metrics/nonexistent")
240 .to_request();
241 let resp = test::call_service(&app, req).await;
242
243 assert_eq!(resp.status(), 404);
245 }
246
247 #[actix_web::test]
248 async fn test_scrape_metrics() {
249 let app = test::init_service(App::new().service(scrape_metrics)).await;
251
252 let req = test::TestRequest::get()
254 .uri("/debug/metrics/scrape")
255 .to_request();
256 let resp = test::call_service(&app, req).await;
257
258 assert!(resp.status().is_success());
260 }
261
262 #[actix_web::test]
263 async fn test_scrape_metrics_error() {
264 async fn mock_scrape_metrics_error() -> impl Responder {
269 HttpResponse::InternalServerError().body("Error: test error")
271 }
272
273 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 let req = test::TestRequest::get()
281 .uri("/debug/metrics/scrape")
282 .to_request();
283 let resp = test::call_service(&app, req).await;
284
285 assert_eq!(resp.status(), 500);
287
288 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 let app = test::init_service(App::new().configure(init)).await;
298
299 let req = test::TestRequest::get().uri("/metrics").to_request();
303 let resp = test::call_service(&app, req).await;
304
305 assert!(resp.status().is_success());
307
308 let req = test::TestRequest::get()
310 .uri("/metrics/test_counter")
311 .to_request();
312 let resp = test::call_service(&app, req).await;
313
314 assert_eq!(resp.status(), 404);
316
317 let req = test::TestRequest::get()
319 .uri("/debug/metrics/scrape")
320 .to_request();
321 let resp = test::call_service(&app, req).await;
322 assert!(resp.status().is_success());
324 }
325
326 #[actix_web::test]
327 async fn test_metric_detail_encoding_error() {
328 async fn mock_metric_detail_with_encoding_error(path: web::Path<String>) -> impl Responder {
330 let metric_name = path.into_inner();
331
332 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 return HttpResponse::InternalServerError()
340 .body("Encoding error: simulated error");
341 }
342 }
343 HttpResponse::NotFound().body("Metric not found")
344 }
345
346 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 let req = test::TestRequest::get()
357 .uri("/metrics/test_counter")
358 .to_request();
359 let resp = test::call_service(&app, req).await;
360
361 assert_eq!(resp.status(), 500);
363
364 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}