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"); +}