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