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