systemprompt-cli 0.1.22

systemprompt.io OS - CLI for agent orchestration, AI operations, and system management
Documentation
use anyhow::Result;
use clap::Args;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use systemprompt_logging::TraceQueryService;

use super::duration::parse_since;
use crate::CliConfig;
use crate::shared::{CommandResult, RenderingHints, render_result};

#[derive(Debug, Args)]
pub struct SummaryArgs {
    #[arg(
        long,
        help = "Only include logs since this duration (e.g., '1h', '24h', '7d')"
    )]
    pub since: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct LogsSummaryOutput {
    pub total_logs: i64,
    pub by_level: LevelCounts,
    pub top_modules: Vec<ModuleCount>,
    pub time_range: TimeRange,
    pub database_info: DatabaseInfo,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct LevelCounts {
    pub error: i64,
    pub warn: i64,
    pub info: i64,
    pub debug: i64,
    pub trace: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ModuleCount {
    pub module: String,
    pub count: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TimeRange {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub earliest: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub latest: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub span_hours: Option<i64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DatabaseInfo {
    pub logs_table_rows: i64,
}

crate::define_pool_command!(SummaryArgs => (), with_config);

async fn execute_with_pool_inner(
    args: SummaryArgs,
    pool: &Arc<sqlx::PgPool>,
    config: &CliConfig,
) -> Result<()> {
    let since_timestamp = parse_since(args.since.as_ref())?;
    let service = TraceQueryService::new(Arc::clone(pool));

    let (level_counts, top_modules, time_range, total_row_count) = tokio::try_join!(
        service.count_logs_by_level(since_timestamp),
        service.top_modules(since_timestamp, 10),
        service.log_time_range(since_timestamp),
        service.total_log_count(),
    )?;

    let by_level = build_level_counts(&level_counts);
    let total_logs =
        by_level.error + by_level.warn + by_level.info + by_level.debug + by_level.trace;

    let span_hours = match (&time_range.earliest, &time_range.latest) {
        (Some(e), Some(l)) => Some((*l - *e).num_hours()),
        _ => None,
    };

    let output = LogsSummaryOutput {
        total_logs,
        by_level,
        top_modules: top_modules
            .into_iter()
            .map(|r| ModuleCount {
                module: r.module,
                count: r.count,
            })
            .collect(),
        time_range: TimeRange {
            earliest: time_range
                .earliest
                .map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string()),
            latest: time_range
                .latest
                .map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string()),
            span_hours,
        },
        database_info: DatabaseInfo {
            logs_table_rows: total_row_count,
        },
    };

    if config.is_json_output() {
        let hints = RenderingHints::default();
        let result = CommandResult::card(output)
            .with_title("Logs Summary")
            .with_hints(hints);
        render_result(&result);
    } else {
        render_text_output(&output);
    }

    Ok(())
}

fn build_level_counts(rows: &[systemprompt_logging::LevelCount]) -> LevelCounts {
    let mut counts = LevelCounts {
        error: 0,
        warn: 0,
        info: 0,
        debug: 0,
        trace: 0,
    };

    for row in rows {
        match row.level.to_lowercase().as_str() {
            "error" => counts.error = row.count,
            "warn" | "warning" => counts.warn = row.count,
            "info" => counts.info = row.count,
            "debug" => counts.debug = row.count,
            "trace" => counts.trace = row.count,
            _ => {},
        }
    }

    counts
}

fn render_text_output(output: &LogsSummaryOutput) {
    use systemprompt_logging::CliService;

    CliService::section("Logs Summary");

    CliService::key_value("Total Logs", &output.total_logs.to_string());

    CliService::subsection("By Level");
    if output.by_level.error > 0 {
        CliService::error(&format!("  Errors:   {}", output.by_level.error));
    } else {
        CliService::key_value("  Errors", &output.by_level.error.to_string());
    }
    if output.by_level.warn > 0 {
        CliService::warning(&format!("  Warnings: {}", output.by_level.warn));
    } else {
        CliService::key_value("  Warnings", &output.by_level.warn.to_string());
    }
    CliService::key_value("  Info", &output.by_level.info.to_string());
    CliService::key_value("  Debug", &output.by_level.debug.to_string());
    CliService::key_value("  Trace", &output.by_level.trace.to_string());

    if !output.top_modules.is_empty() {
        CliService::subsection("Top Modules");
        for module in &output.top_modules {
            CliService::info(&format!("  {} ({})", module.module, module.count));
        }
    }

    CliService::subsection("Time Range");
    if let Some(ref earliest) = output.time_range.earliest {
        CliService::key_value("  Earliest", earliest);
    }
    if let Some(ref latest) = output.time_range.latest {
        CliService::key_value("  Latest", latest);
    }
    if let Some(span) = output.time_range.span_hours {
        CliService::key_value("  Span", &format!("{} hours", span));
    }

    CliService::subsection("Database");
    CliService::key_value(
        "  Total Rows",
        &output.database_info.logs_table_rows.to_string(),
    );
}