use anyhow::Result;
use serde_json::{json, Value};
use trusty_common::console_metrics::{make_report, ServiceHealth};
use trusty_common::memory_core::PalaceRegistry;
use crate::AppState;
const MAX_PALACES_IN_REPORT: usize = 20;
pub fn descriptor() -> Value {
json!({
"name": "console_metrics",
"description": "Return a ConsoleMetricsReport with palace aggregate statistics \
(palace_count, total_drawers, total_vectors, total_kg_triples) and per-palace \
detail (first 20). Used by the trusty-console dashboard metrics poller.",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
})
}
struct PalaceStats {
palace_count: usize,
total_drawers: usize,
total_vectors: usize,
total_kg_triples: usize,
palace_entries: Vec<Value>,
}
pub async fn handle_console_metrics(state: &AppState, _args: Value) -> Result<Value> {
let root = state.data_root.clone();
let palace_infos =
match tokio::task::spawn_blocking(move || PalaceRegistry::list_palaces(&root))
.await
.map_err(|e| anyhow::anyhow!("join list_palaces: {e}"))?
{
Ok(v) => v,
Err(e) => {
tracing::warn!("console_metrics: list_palaces failed: {e:#}");
Vec::new()
}
};
let root2 = state.data_root.clone();
let registry = state.registry.clone();
let stats =
tokio::task::spawn_blocking(move || collect_palace_stats(®istry, &root2, &palace_infos))
.await
.map_err(|e| anyhow::anyhow!("join collect_palace_stats: {e}"))?;
let metrics = json!({
"palace_count": stats.palace_count,
"total_drawers": stats.total_drawers,
"total_vectors": stats.total_vectors,
"total_kg_triples": stats.total_kg_triples,
"palaces": stats.palace_entries,
});
let report = make_report(
"trusty-memory",
"Trusty Memory",
env!("CARGO_PKG_VERSION"),
ServiceHealth::Ok,
metrics,
1,
);
Ok(serde_json::to_value(&report)?)
}
fn collect_palace_stats(
registry: &trusty_common::memory_core::PalaceRegistry,
data_root: &std::path::Path,
palace_infos: &[trusty_common::memory_core::Palace],
) -> PalaceStats {
let palace_count = palace_infos.len();
let mut total_drawers: usize = 0;
let mut total_vectors: usize = 0;
let mut total_kg_triples: usize = 0;
let mut palace_entries: Vec<Value> =
Vec::with_capacity(palace_count.min(MAX_PALACES_IN_REPORT));
for info in palace_infos.iter().take(MAX_PALACES_IN_REPORT) {
let palace_id = info.id.as_str().to_string();
let name = info.name.clone();
match registry.open_palace(data_root, &info.id) {
Ok(handle) => {
let drawer_count = handle.drawers.read().len();
let vector_count = handle.vector_store.index_size();
let kg_triple_count = handle.kg.count_active_triples();
total_drawers += drawer_count;
total_vectors += vector_count;
total_kg_triples += kg_triple_count;
palace_entries.push(json!({
"id": palace_id,
"name": name,
"drawer_count": drawer_count,
"vector_count": vector_count,
"kg_triple_count": kg_triple_count,
}));
}
Err(e) => {
tracing::warn!(
palace = %palace_id,
"console_metrics: open failed (skipped): {e:#}"
);
palace_entries.push(json!({
"id": palace_id,
"name": name,
"drawer_count": 0,
"vector_count": 0,
"kg_triple_count": 0,
"error": e.to_string(),
}));
}
}
}
for info in palace_infos.iter().skip(MAX_PALACES_IN_REPORT) {
if let Ok(handle) = registry.open_palace(data_root, &info.id) {
total_drawers += handle.drawers.read().len();
total_vectors += handle.vector_store.index_size();
total_kg_triples += handle.kg.count_active_triples();
}
}
PalaceStats {
palace_count,
total_drawers,
total_vectors,
total_kg_triples,
palace_entries,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[serial_test::serial]
#[tokio::test]
async fn handle_console_metrics_returns_valid_report() {
unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
}
let tmp = tempfile::tempdir().expect("tempdir");
let state = crate::AppState::new(tmp.path().to_path_buf());
let result = handle_console_metrics(&state, serde_json::json!({}))
.await
.expect("console_metrics must not return Err");
assert_eq!(result["service_id"], "trusty-memory");
assert_eq!(result["display_name"], "Trusty Memory");
assert!(result["version"].is_string());
assert!(result["status"].is_string());
assert_eq!(result["metrics_schema_version"], 1);
assert!(result["collected_at_unix"].is_number());
assert_eq!(result["metrics"]["palace_count"], 0);
assert_eq!(result["metrics"]["total_drawers"], 0);
assert_eq!(result["metrics"]["total_vectors"], 0);
assert_eq!(result["metrics"]["total_kg_triples"], 0);
assert!(result["metrics"]["palaces"].is_array());
assert_eq!(result["metrics"]["palaces"].as_array().unwrap().len(), 0);
}
}