use axum::Json;
use axum::extract::State;
use chrono::{DateTime, Utc};
use serde::Serialize;
use tracing::debug;
use vti_common::error::AppError;
use crate::auth::AdminAuth;
use crate::registry::{HealthStatus, list_sync_jobs};
use crate::server::AppState;
#[derive(Serialize)]
pub struct HealthResponse {
status: &'static str,
version: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
vtc_did: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
vta_did: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
mediator_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
mediator_did: Option<String>,
}
pub async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
debug!("health check");
let config = state.config.read().await;
let vtc_did = config.vtc_did.clone();
let vta_did = config.vta_did.clone();
let (mediator_url, mediator_did) = config
.messaging
.as_ref()
.map(|m| (Some(m.mediator_url.clone()), Some(m.mediator_did.clone())))
.unwrap_or((None, None));
Json(HealthResponse {
status: "ok",
version: env!("CARGO_PKG_VERSION"),
vtc_did,
vta_did,
mediator_url,
mediator_did,
})
}
#[derive(Serialize)]
pub struct DiagnosticsResponse {
pub registry_status: String,
pub queue_depth: u64,
pub rtbf_batched_count: u64,
pub failed_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub oldest_pending_age_seconds: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_success_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_failure_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_error: Option<String>,
}
pub async fn diagnostics(
State(state): State<AppState>,
_auth: AdminAuth,
) -> Result<Json<DiagnosticsResponse>, AppError> {
let jobs = list_sync_jobs(&state.sync_queue_ks).await?;
let now = Utc::now();
let queue_depth = jobs
.iter()
.filter(|j| {
matches!(
j.state,
crate::registry::SyncJobState::Pending | crate::registry::SyncJobState::InFlight
)
})
.count() as u64;
let rtbf_batched_count = jobs
.iter()
.filter(|j| j.rtbf_batched && j.state == crate::registry::SyncJobState::Pending)
.count() as u64;
let failed_count = jobs
.iter()
.filter(|j| j.state == crate::registry::SyncJobState::Failed)
.count() as u64;
let oldest_pending_age_seconds = jobs
.iter()
.filter(|j| j.state == crate::registry::SyncJobState::Pending && j.next_attempt_at <= now)
.map(|j| (now - j.created_at).num_seconds())
.max();
let snapshot = state.registry_health.snapshot().await;
let registry_status = match snapshot.status {
HealthStatus::Active => "active",
HealthStatus::Degraded => "degraded",
}
.to_string();
debug!(
queue_depth,
rtbf_batched_count,
failed_count,
registry_status = %registry_status,
"diagnostics queried"
);
Ok(Json(DiagnosticsResponse {
registry_status,
queue_depth,
rtbf_batched_count,
failed_count,
oldest_pending_age_seconds,
last_success_at: snapshot.last_success_at,
last_failure_at: snapshot.last_failure_at,
last_error: snapshot.last_error,
}))
}