Skip to main content

systemprompt_cli/commands/infrastructure/logs/
summary.rs

1//! `infra logs summary`: high-level log statistics — per-level counts, busiest
2//! modules, time span covered, and total stored rows.
3
4use anyhow::Result;
5use clap::Args;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9use systemprompt_logging::TraceQueryService;
10
11use super::duration::parse_since;
12use crate::CliConfig;
13use crate::shared::{CommandOutput, render_result};
14
15#[derive(Debug, Args)]
16pub struct SummaryArgs {
17    #[arg(
18        long,
19        help = "Only include logs since this duration (e.g., '1h', '24h', '7d')"
20    )]
21    pub since: Option<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
25pub struct LogsSummaryOutput {
26    pub total_logs: i64,
27    pub by_level: LevelCounts,
28    pub top_modules: Vec<ModuleCount>,
29    pub time_range: TimeRange,
30    pub database_info: DatabaseInfo,
31}
32
33#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
34pub struct LevelCounts {
35    pub error: i64,
36    pub warn: i64,
37    pub info: i64,
38    pub debug: i64,
39    pub trace: i64,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
43pub struct ModuleCount {
44    pub module: String,
45    pub count: i64,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
49pub struct TimeRange {
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub earliest: Option<String>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub latest: Option<String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub span_hours: Option<i64>,
56}
57
58#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
59pub struct DatabaseInfo {
60    pub logs_table_rows: i64,
61}
62
63crate::define_pool_command!(SummaryArgs => (), with_config);
64
65async fn execute_with_pool_inner(
66    args: SummaryArgs,
67    pool: &Arc<sqlx::PgPool>,
68    config: &CliConfig,
69) -> Result<()> {
70    let since_timestamp = parse_since(args.since.as_ref())?;
71    let service = TraceQueryService::new(Arc::clone(pool));
72
73    let (level_counts, top_modules, time_range, total_row_count) = tokio::try_join!(
74        service.count_logs_by_level(since_timestamp),
75        service.top_modules(since_timestamp, 10),
76        service.log_time_range(since_timestamp),
77        service.total_log_count(),
78    )?;
79
80    let by_level = build_level_counts(&level_counts);
81    let total_logs =
82        by_level.error + by_level.warn + by_level.info + by_level.debug + by_level.trace;
83
84    let span_hours = match (&time_range.earliest, &time_range.latest) {
85        (Some(e), Some(l)) => Some((*l - *e).num_hours()),
86        _ => None,
87    };
88
89    let output = LogsSummaryOutput {
90        total_logs,
91        by_level,
92        top_modules: top_modules
93            .into_iter()
94            .map(|r| ModuleCount {
95                module: r.module,
96                count: r.count,
97            })
98            .collect(),
99        time_range: TimeRange {
100            earliest: time_range
101                .earliest
102                .map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string()),
103            latest: time_range
104                .latest
105                .map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string()),
106            span_hours,
107        },
108        database_info: DatabaseInfo {
109            logs_table_rows: total_row_count,
110        },
111    };
112
113    render_result(&build_logs_summary(&output), config);
114
115    Ok(())
116}
117
118#[must_use]
119pub fn build_logs_summary(output: &LogsSummaryOutput) -> CommandOutput {
120    CommandOutput::card_value("Logs Summary", output)
121}
122
123fn build_level_counts(rows: &[systemprompt_logging::LevelCount]) -> LevelCounts {
124    let mut counts = LevelCounts {
125        error: 0,
126        warn: 0,
127        info: 0,
128        debug: 0,
129        trace: 0,
130    };
131
132    for row in rows {
133        match row.level.to_lowercase().as_str() {
134            "error" => counts.error = row.count,
135            "warn" | "warning" => counts.warn = row.count,
136            "info" => counts.info = row.count,
137            "debug" => counts.debug = row.count,
138            "trace" => counts.trace = row.count,
139            _ => {},
140        }
141    }
142
143    counts
144}