openzeppelin_relayer/api/routes/
metrics.rsuse crate::metrics::{update_system_metrics, REGISTRY};
use actix_web::{get, web, HttpResponse, Responder};
use prometheus::{Encoder, TextEncoder};
#[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 {
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)
}
#[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")
}
#[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)),
}
}
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};
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(); registry
}
async fn mock_list_metrics() -> impl Responder {
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() {
let app = test::init_service(
App::new().service(web::resource("/metrics").route(web::get().to(mock_list_metrics))),
)
.await;
let req = test::TestRequest::get().uri("/metrics").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body = test::read_body(resp).await;
let metric_names: Vec<String> = serde_json::from_slice(&body).unwrap();
assert!(metric_names.contains(&"test_counter".to_string()));
}
async fn mock_metric_detail(path: web::Path<String>) -> impl Responder {
let metric_name = path.into_inner();
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() {
let app = test::init_service(App::new().service(
web::resource("/metrics/{metric_name}").route(web::get().to(mock_metric_detail)),
))
.await;
let req = test::TestRequest::get()
.uri("/metrics/test_counter")
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
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() {
let app = test::init_service(App::new().service(
web::resource("/metrics/{metric_name}").route(web::get().to(mock_metric_detail)),
))
.await;
let req = test::TestRequest::get()
.uri("/metrics/nonexistent")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 404);
}
#[actix_web::test]
async fn test_scrape_metrics() {
let app = test::init_service(App::new().service(scrape_metrics)).await;
let req = test::TestRequest::get()
.uri("/debug/metrics/scrape")
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
#[actix_web::test]
async fn test_scrape_metrics_error() {
async fn mock_scrape_metrics_error() -> impl Responder {
HttpResponse::InternalServerError().body("Error: test error")
}
let app = test::init_service(App::new().service(
web::resource("/debug/metrics/scrape").route(web::get().to(mock_scrape_metrics_error)),
))
.await;
let req = test::TestRequest::get()
.uri("/debug/metrics/scrape")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 500);
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() {
let app = test::init_service(App::new().configure(init)).await;
let req = test::TestRequest::get().uri("/metrics").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::get()
.uri("/metrics/test_counter")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 404);
let req = test::TestRequest::get()
.uri("/debug/metrics/scrape")
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
#[actix_web::test]
async fn test_metric_detail_encoding_error() {
async fn mock_metric_detail_with_encoding_error(path: web::Path<String>) -> impl Responder {
let metric_name = path.into_inner();
let registry = setup_test_registry();
let metric_families = registry.gather();
for mf in metric_families {
if mf.get_name() == metric_name {
return HttpResponse::InternalServerError()
.body("Encoding error: simulated error");
}
}
HttpResponse::NotFound().body("Metric not found")
}
let app = test::init_service(
App::new().service(
web::resource("/metrics/{metric_name}")
.route(web::get().to(mock_metric_detail_with_encoding_error)),
),
)
.await;
let req = test::TestRequest::get()
.uri("/metrics/test_counter")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 500);
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"));
}
}