use std::sync::Arc;
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::response::Response;
use crate::common::{AppState, redacted_error};
use crate::routing::json_response_http;
pub(crate) async fn diagnostic_snapshot(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Response {
if let Err(r) = crate::common::check_auth_write(&state, &headers) {
return r;
}
let client = match state.pool.get().await {
Ok(c) => c,
Err(e) => {
state.metrics.record_error();
return redacted_error(
"db_pool_error",
&e.to_string(),
StatusCode::SERVICE_UNAVAILABLE,
);
}
};
let table_counts: serde_json::Value = match client
.query(
"SELECT relname, reltuples::BIGINT AS estimate \
FROM pg_class c \
JOIN pg_namespace n ON n.oid = c.relnamespace \
WHERE n.nspname = '_pg_ripple' AND c.relkind = 'r' \
ORDER BY relname",
&[],
)
.await
{
Ok(rows) => {
let map: serde_json::Map<String, serde_json::Value> = rows
.iter()
.map(|r| {
let name: String = r.get(0);
let count: i64 = r.get(1);
(
name,
serde_json::Value::Number(serde_json::Number::from(count)),
)
})
.collect();
serde_json::Value::Object(map)
}
Err(_) => serde_json::json!({}),
};
let gucs: serde_json::Value = match client
.query(
"SELECT name, setting, unit, short_desc \
FROM pg_settings \
WHERE name LIKE 'pg_ripple.%' \
AND name NOT ILIKE '%key%' \
AND name NOT ILIKE '%password%' \
AND name NOT ILIKE '%secret%' \
AND name NOT ILIKE '%token%' \
ORDER BY name",
&[],
)
.await
{
Ok(rows) => {
let map: serde_json::Map<String, serde_json::Value> = rows
.iter()
.map(|r| {
let name: String = r.get(0);
let setting: String = r.get(1);
let unit: Option<String> = r.get(2);
let desc: String = r.get(3);
let val = serde_json::json!({
"value": setting,
"unit": unit,
"description": desc,
});
(name, val)
})
.collect();
serde_json::Value::Object(map)
}
Err(_) => serde_json::json!({}),
};
let extension_version: Option<String> = client
.query_opt(
"SELECT extversion FROM pg_extension WHERE extname = 'pg_ripple'",
&[],
)
.await
.ok()
.flatten()
.map(|r| r.get(0));
let http_companion_version = env!("CARGO_PKG_VERSION");
let m = &state.metrics;
let metrics_snapshot = serde_json::json!({
"sparql_queries_total": m.sparql_query_count(),
"datalog_queries_total": m.datalog_query_count(),
"errors_total": m.error_count(),
"select_count": m.select_count(),
"ask_count": m.ask_count(),
"construct_count": m.construct_count(),
"describe_count": m.describe_count(),
"update_count": m.update_count(),
"pool_size": state.pool.status().size,
"sameas_assertions_total": m.sameas_assertions_total(),
"temporal_facts_total": m.temporal_facts_total(),
});
let snapshot = serde_json::json!({
"generated_at": chrono_now_utc(),
"extension_version": extension_version,
"http_companion_version": http_companion_version,
"table_row_counts": table_counts,
"gucs": gucs,
"metrics_snapshot": metrics_snapshot,
});
json_response_http(StatusCode::OK, snapshot)
}
fn chrono_now_utc() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let days_since_epoch = secs / 86400;
let time_of_day = secs % 86400;
let hh = time_of_day / 3600;
let mm = (time_of_day % 3600) / 60;
let ss = time_of_day % 60;
let (y, mo, d) = days_to_ymd(days_since_epoch as u32);
format!("{y:04}-{mo:02}-{d:02}T{hh:02}:{mm:02}:{ss:02}Z")
}
fn days_to_ymd(days: u32) -> (u32, u32, u32) {
let z = days + 719468;
let era = z / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}