From 97eb3ed86a229b27c9f1d152ad780d76e2dc9d46 Mon Sep 17 00:00:00 2001 From: Sergio Arroutbi Date: Wed, 13 May 2026 18:19:18 +0200 Subject: [PATCH] Add unified performance summary endpoint and resilient verifier handler Add GET /api/performance/summary that aggregates verifier reachability, latency, circuit breaker state, agent count, estimated attestation rate, capacity utilization, and database pool status into a single response for frontend dashboard summary cards. Modify verifier_metrics handler to degrade gracefully when the verifier is unreachable, returning reachable: false and circuit_breaker: "open" instead of propagating the error. Add three Mockoon integration tests exercising the summary and verifier endpoints with both reachable and unreachable verifier scenarios. Add curl integration test coverage for the new summary endpoint. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Sergio Arroutbi --- src/api/handlers/performance.rs | 70 +++++++++++++++-- src/api/routes.rs | 1 + tests/curl_integration_test.sh | 5 ++ tests/mockoon_integration.rs | 134 ++++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+), 6 deletions(-) diff --git a/src/api/handlers/performance.rs b/src/api/handlers/performance.rs index a5e93e9..d8e8423 100644 --- a/src/api/handlers/performance.rs +++ b/src/api/handlers/performance.rs @@ -2,28 +2,86 @@ use std::time::Instant; use axum::extract::State; use axum::Json; +use serde::Serialize; use crate::api::response::ApiResponse; use crate::error::AppResult; use crate::state::AppState; +#[derive(Debug, Serialize)] +pub struct PerformanceSummary { + pub verifier_reachable: bool, + pub verifier_latency_ms: u64, + pub circuit_breaker_state: String, + pub agent_count: usize, + pub estimated_attestation_rate: f64, + pub capacity_utilization_pct: f64, + pub database_pool_status: String, +} + +/// GET /api/performance/summary -- Aggregated dashboard summary. +pub async fn performance_summary( + State(state): State, +) -> AppResult>> { + let start = Instant::now(); + let keylime = state.keylime(); + let verifier_available = keylime.verifier_available().await; + + let (agent_count, verifier_reachable) = if verifier_available { + match keylime.list_verifier_agents().await { + Ok(agents) => (agents.len(), true), + Err(_) => (0, false), + } + } else { + (0, false) + }; + + let latency = start.elapsed().as_millis() as u64; + let circuit_breaker_state = if verifier_available { "closed" } else { "open" }; + let attestation_rate = agent_count as f64 / 30.0; + let utilization = compute_utilization_pct(agent_count as u64, 1000); + + Ok(Json(ApiResponse::ok(PerformanceSummary { + verifier_reachable, + verifier_latency_ms: latency, + circuit_breaker_state: circuit_breaker_state.to_string(), + agent_count, + estimated_attestation_rate: attestation_rate, + capacity_utilization_pct: utilization, + database_pool_status: "not_configured".to_string(), + }))) +} + /// GET /api/performance/verifiers -- Verifier cluster metrics (FR-064). pub async fn verifier_metrics( State(state): State, ) -> AppResult>> { let start = Instant::now(); - let agent_count = state.keylime().list_verifier_agents().await?.len(); + let keylime = state.keylime(); + let verifier_available = keylime.verifier_available().await; + + let (agent_count, reachable) = if verifier_available { + match keylime.list_verifier_agents().await { + Ok(agents) => (agents.len(), true), + Err(_) => (0, false), + } + } else { + (0, false) + }; + let latency = start.elapsed().as_millis() as u64; + let circuit_breaker = if reachable && verifier_available { + "closed" + } else { + "open" + }; Ok(Json(ApiResponse::ok(serde_json::json!({ "verifier_url": "configured", + "reachable": reachable, "agent_count": agent_count, "api_latency_ms": latency, - "circuit_breaker": if state.keylime().verifier_available().await { - "closed" - } else { - "open" - }, + "circuit_breaker": circuit_breaker, })))) } diff --git a/src/api/routes.rs b/src/api/routes.rs index dfb64db..a685f1f 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -217,6 +217,7 @@ fn integration_routes() -> Router { #[cfg(not(tarpaulin_include))] fn performance_routes() -> Router { Router::new() + .route("/summary", get(handlers::performance::performance_summary)) .route("/verifiers", get(handlers::performance::verifier_metrics)) .route("/database", get(handlers::performance::database_metrics)) .route( diff --git a/tests/curl_integration_test.sh b/tests/curl_integration_test.sh index 2f2fc0e..ea92eab 100755 --- a/tests/curl_integration_test.sh +++ b/tests/curl_integration_test.sh @@ -492,6 +492,11 @@ echo "" # -- Performance endpoints -- echo " Performance" echo " -----------" +run_test "Performance summary" "/api/performance/summary" +run_test " verifier reachable" "/api/performance/summary" \ + ".data.verifier_reachable" "true" +run_test " circuit breaker closed" "/api/performance/summary" \ + ".data.circuit_breaker_state" "closed" run_test "Verifier metrics" "/api/performance/verifiers" run_test "Database metrics" "/api/performance/database" run_test "API response times" "/api/performance/api-response-times" diff --git a/tests/mockoon_integration.rs b/tests/mockoon_integration.rs index c94b98c..ed0238c 100644 --- a/tests/mockoon_integration.rs +++ b/tests/mockoon_integration.rs @@ -10,10 +10,20 @@ #![cfg(feature = "mockoon")] use std::collections::HashMap; +use std::sync::Arc; +use axum::body::Body; +use axum::http::Request; +use tower::ServiceExt; + +use keylime_webtool_backend::api::routes::build_router; +use keylime_webtool_backend::config::{CircuitBreakerConfig, KeylimeConfig}; +use keylime_webtool_backend::keylime::client::KeylimeClient; use keylime_webtool_backend::keylime::models::{ RegistrarAgent, RuntimePolicy, VerifierAgent, VerifierResponse, }; +use keylime_webtool_backend::repository::{InMemoryCacheBackend, Repositories}; +use keylime_webtool_backend::state::AppState; const VERIFIER_BASE: &str = "http://localhost:3000"; const REGISTRAR_BASE: &str = "http://localhost:3001"; @@ -971,3 +981,127 @@ async fn test_mockoon_all_agents_have_ekcert() { } } } + +// --------------------------------------------------------------------------- +// Performance endpoint tests (using full backend stack against Mockoon) +// --------------------------------------------------------------------------- + +fn build_test_app(verifier_url: &str) -> axum::Router { + let config = KeylimeConfig { + verifier_url: verifier_url.to_string(), + registrar_url: REGISTRAR_BASE.to_string(), + mtls: None, + timeout_secs: 5, + observation_interval_secs: 30, + circuit_breaker: CircuitBreakerConfig { + failure_threshold: 1, + reset_timeout_secs: 3600, + }, + }; + let client = KeylimeClient::new(config).unwrap(); + let repos = Repositories::in_memory(); + let state = AppState::new( + client, + repos.alert, + repos.attestation, + repos.policy, + repos.audit, + Arc::new(InMemoryCacheBackend::new()), + None, + false, + ); + build_router(state) +} + +#[tokio::test] +async fn test_mockoon_performance_summary_returns_expected_fields() { + if std::env::var("MOCKOON_VERIFIER").is_err() { + eprintln!("Skipping: MOCKOON_VERIFIER not set"); + return; + } + + let app = build_test_app(VERIFIER_BASE); + let req = Request::builder() + .uri("/api/performance/summary") + .body(Body::empty()) + .unwrap(); + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), 200); + + let body: serde_json::Value = serde_json::from_slice( + &axum::body::to_bytes(resp.into_body(), usize::MAX) + .await + .unwrap(), + ) + .unwrap(); + assert!(body["success"].as_bool().unwrap()); + + let data = &body["data"]; + assert_eq!(data["verifier_reachable"], true); + assert!(data["verifier_latency_ms"].as_u64().is_some()); + assert_eq!(data["circuit_breaker_state"], "closed"); + assert_eq!(data["agent_count"], 7); + assert!(data["estimated_attestation_rate"].as_f64().is_some()); + assert!(data["capacity_utilization_pct"].as_f64().is_some()); + assert_eq!(data["database_pool_status"], "not_configured"); +} + +#[tokio::test] +async fn test_mockoon_performance_verifiers_returns_valid_metrics() { + if std::env::var("MOCKOON_VERIFIER").is_err() { + eprintln!("Skipping: MOCKOON_VERIFIER not set"); + return; + } + + let app = build_test_app(VERIFIER_BASE); + let req = Request::builder() + .uri("/api/performance/verifiers") + .body(Body::empty()) + .unwrap(); + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), 200); + + let body: serde_json::Value = serde_json::from_slice( + &axum::body::to_bytes(resp.into_body(), usize::MAX) + .await + .unwrap(), + ) + .unwrap(); + assert!(body["success"].as_bool().unwrap()); + + let data = &body["data"]; + assert_eq!(data["verifier_url"], "configured"); + assert_eq!(data["reachable"], true); + assert_eq!(data["agent_count"], 7); + assert!(data["api_latency_ms"].as_u64().is_some()); + assert_eq!(data["circuit_breaker"], "closed"); +} + +#[tokio::test] +async fn test_mockoon_performance_verifiers_circuit_breaker_open() { + if std::env::var("MOCKOON_VERIFIER").is_err() { + eprintln!("Skipping: MOCKOON_VERIFIER not set"); + return; + } + + let app = build_test_app("http://localhost:19999"); + + let req = Request::builder() + .uri("/api/performance/verifiers") + .body(Body::empty()) + .unwrap(); + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), 200); + + let body: serde_json::Value = serde_json::from_slice( + &axum::body::to_bytes(resp.into_body(), usize::MAX) + .await + .unwrap(), + ) + .unwrap(); + + let data = &body["data"]; + assert_eq!(data["reachable"], false); + assert_eq!(data["agent_count"], 0); + assert_eq!(data["circuit_breaker"], "open"); +}