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