use std::process::ExitCode;
use anyhow::{Result, bail};
use astrid_core::kernel_api::{
AdminRequestKind, AdminResponseBody, AuditHealth, AuditPruneResult, AuditStats,
};
use clap::{Args, Subcommand};
use serde::Serialize;
use crate::admin_client::{connect_as_active_agent, into_result};
use crate::value_formatter::{ValueFormat, emit_structured};
#[derive(Subcommand, Debug, Clone)]
pub(crate) enum AuditCommand {
Stats(AuditStatsArgs),
Prune(AuditPruneArgs),
Health(AuditHealthArgs),
}
#[derive(Args, Debug, Clone)]
pub(crate) struct AuditArgs {
#[command(subcommand)]
pub command: AuditCommand,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct AuditStatsArgs {
#[arg(long, default_value = "pretty")]
pub format: String,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct AuditPruneArgs {
#[arg(long, default_value_t = 1, value_name = "N")]
pub retain_entries: u64,
#[arg(long, value_name = "BYTES")]
pub retain_bytes: Option<u64>,
#[arg(long, default_value = "pretty")]
pub format: String,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct AuditHealthArgs {
#[arg(long, default_value = "pretty")]
pub format: String,
}
#[derive(Debug, Clone, Serialize)]
struct AuditStatsOutput {
stats: AuditStats,
health: AuditHealth,
}
pub(crate) async fn run(args: &AuditArgs) -> Result<ExitCode> {
match &args.command {
AuditCommand::Stats(args) => run_stats(args).await,
AuditCommand::Prune(args) => run_prune(args).await,
AuditCommand::Health(args) => run_health(args).await,
}
}
async fn run_stats(args: &AuditStatsArgs) -> Result<ExitCode> {
let mut client = connect_as_active_agent().await?;
let stats = match into_result(client.request(AdminRequestKind::AuditStats).await?)? {
AdminResponseBody::AuditStats(stats) => stats,
other => bail!("unexpected response from kernel: {other:?}"),
};
let health = match into_result(client.request(AdminRequestKind::AuditHealth).await?)? {
AdminResponseBody::AuditHealth(health) => health,
other => bail!("unexpected response from kernel: {other:?}"),
};
let degraded = stats.degraded || health.degraded;
let format = ValueFormat::parse(&args.format);
if format.is_pretty() {
print_stats_pretty(&stats, &health);
} else {
emit_structured(&AuditStatsOutput { stats, health }, format)?;
}
Ok(if degraded {
ExitCode::from(2)
} else {
ExitCode::SUCCESS
})
}
async fn run_prune(args: &AuditPruneArgs) -> Result<ExitCode> {
if args.retain_entries == 0 {
bail!("--retain-entries must be at least 1");
}
if args.retain_bytes == Some(0) {
bail!("--retain-bytes must be greater than 0");
}
let mut client = connect_as_active_agent().await?;
let body = into_result(
client
.request(AdminRequestKind::AuditPrune {
retain_entries: args.retain_entries,
retain_bytes: args.retain_bytes,
})
.await?,
)?;
let AdminResponseBody::AuditPruned(result) = body else {
bail!("unexpected response from kernel: {body:?}");
};
let format = ValueFormat::parse(&args.format);
if format.is_pretty() {
print_prune_pretty(&result);
} else {
emit_structured(&result, format)?;
}
Ok(ExitCode::SUCCESS)
}
async fn run_health(args: &AuditHealthArgs) -> Result<ExitCode> {
let mut client = connect_as_active_agent().await?;
let body = into_result(client.request(AdminRequestKind::AuditHealth).await?)?;
let AdminResponseBody::AuditHealth(health) = body else {
bail!("unexpected response from kernel: {body:?}");
};
let degraded = health.degraded;
let format = ValueFormat::parse(&args.format);
if format.is_pretty() {
print_health_pretty(&health);
} else {
emit_structured(&health, format)?;
}
Ok(if degraded {
ExitCode::from(2)
} else {
ExitCode::SUCCESS
})
}
fn print_stats_pretty(stats: &AuditStats, health: &AuditHealth) {
println!("Audit totals");
println!(" entries: {}", stats.total_count);
println!(" bytes: {}", stats.total_bytes);
println!(
" segments: {} ({} sealed)",
stats.segments, stats.sealed_segments
);
println!(" eligible: {}", stats.eligible_segments);
println!(
" cap: {} entries / {} bytes",
stats.cap_entries, stats.cap_bytes
);
println!(
" retention: {}",
if stats.degraded {
"degraded"
} else {
"healthy"
}
);
print_health_pretty(health);
}
fn print_health_pretty(health: &AuditHealth) {
println!("Audit ingestion");
println!(" accepted: {}", health.accepted);
println!(" persisted: {}", health.persisted);
println!(" failed: {}", health.failed);
println!(" queue full: {}", health.queue_full);
println!(" queue depth: {}", health.queue_depth);
println!(
" worker: {}",
if health.worker_alive { "alive" } else { "dead" }
);
println!(
" status: {}",
if health.degraded {
"degraded"
} else {
"healthy"
}
);
if let Some(error) = &health.last_error {
println!(" last error: {error}");
}
}
fn print_prune_pretty(result: &AuditPruneResult) {
println!("Audit prune receipt");
println!(" generation: {}", result.generation);
println!(" receipt hash: {}", result.receipt_hash);
println!(" session: {}", result.session);
if let Some(principal) = &result.principal {
println!(" principal: {principal}");
}
if let Some(segment) = result.segment {
println!(" segment: {segment}");
}
if let Some(ordinal) = result.seal_ordinal {
println!(" seal ordinal: {ordinal}");
}
println!(
" omitted: {} entries / {} bytes",
result.omitted_count, result.omitted_bytes
);
println!(
" retained: {} entries / {} bytes",
result.retained_count, result.retained_bytes
);
println!(
" physical reclaimed: {} bytes",
result.physical_reclaimed_bytes
);
if result.physical_reclaim_pending {
println!(" physical status: pending compaction");
}
}