Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 64 additions & 6 deletions src/api/handlers/performance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppState>,
) -> AppResult<Json<ApiResponse<PerformanceSummary>>> {
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<AppState>,
) -> AppResult<Json<ApiResponse<serde_json::Value>>> {
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,
}))))
}

Expand Down
1 change: 1 addition & 0 deletions src/api/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ fn integration_routes() -> Router<AppState> {
#[cfg(not(tarpaulin_include))]
fn performance_routes() -> Router<AppState> {
Router::new()
.route("/summary", get(handlers::performance::performance_summary))
.route("/verifiers", get(handlers::performance::verifier_metrics))
.route("/database", get(handlers::performance::database_metrics))
.route(
Expand Down
5 changes: 5 additions & 0 deletions tests/curl_integration_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
134 changes: 134 additions & 0 deletions tests/mockoon_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
}