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