systemprompt_cli/commands/infrastructure/logs/
summary.rs1use 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}