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