systemprompt_cli/commands/infrastructure/logs/
mod.rs1mod audit;
2mod audit_display;
3mod cleanup;
4mod delete;
5pub mod duration;
6mod export;
7pub mod request;
8mod search;
9mod search_queries;
10pub mod shared;
11mod show;
12mod stream;
13mod summary;
14pub mod tools;
15pub mod trace;
16pub mod types;
17mod view;
18
19pub use shared::{
20 cost_microdollars_to_dollars, display_log_row, format_duration_ms, format_timestamp,
21 truncate_id,
22};
23pub use types::{MessageRow, ToolCallRow};
24
25use anyhow::{bail, Result};
26use clap::Subcommand;
27use schemars::JsonSchema;
28use serde::{Deserialize, Serialize};
29use systemprompt_runtime::DatabaseContext;
30
31use crate::shared::render_result;
32use crate::CliConfig;
33
34#[derive(Debug, Subcommand)]
35pub enum LogsCommands {
36 #[command(
37 about = "View log entries",
38 after_help = "EXAMPLES:\n systemprompt infra logs view --tail 20\n systemprompt infra \
39 logs view --level error\n systemprompt infra logs view --since 1h"
40 )]
41 View(view::ViewArgs),
42
43 #[command(
44 about = "Search logs by pattern",
45 after_help = "EXAMPLES:\n systemprompt infra logs search \"error\"\n systemprompt infra \
46 logs search \"timeout\" --level error --since 1h"
47 )]
48 Search(search::SearchArgs),
49
50 #[command(
51 about = "Stream logs in real-time (like tail -f)",
52 visible_alias = "follow",
53 after_help = "EXAMPLES:\n systemprompt infra logs stream\n systemprompt infra logs \
54 stream --level error --module agent\n systemprompt infra logs follow"
55 )]
56 Stream(stream::StreamArgs),
57
58 #[command(
59 about = "Export logs to file",
60 after_help = "EXAMPLES:\n systemprompt infra logs export --format json --since 24h\n \
61 systemprompt infra logs export --format csv -o logs.csv"
62 )]
63 Export(export::ExportArgs),
64
65 #[command(about = "Clean up old log entries")]
66 Cleanup(cleanup::CleanupArgs),
67
68 #[command(about = "Delete all log entries")]
69 Delete(delete::DeleteArgs),
70
71 #[command(
72 about = "Show logs summary statistics",
73 after_help = "EXAMPLES:\n systemprompt infra logs summary\n systemprompt infra logs \
74 summary --since 24h"
75 )]
76 Summary(summary::SummaryArgs),
77
78 #[command(
79 about = "Show details of a log entry or all logs for a trace",
80 after_help = "EXAMPLES:\n systemprompt infra logs show log_abc123\n systemprompt infra \
81 logs show trace_def456"
82 )]
83 Show(show::ShowArgs),
84
85 #[command(subcommand, about = "Debug execution traces")]
86 Trace(trace::TraceCommands),
87
88 #[command(subcommand, about = "Inspect AI requests")]
89 Request(request::RequestCommands),
90
91 #[command(subcommand, about = "List and search MCP tool executions")]
92 Tools(tools::ToolsCommands),
93
94 #[command(
95 about = "Full audit of an AI request with all messages and tool calls",
96 after_help = "EXAMPLES:\n systemprompt infra logs audit abc123\n systemprompt infra \
97 logs audit abc123 --full\n systemprompt infra logs audit task-xyz --json"
98 )]
99 Audit(audit::AuditArgs),
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
103pub struct LogEntryRow {
104 pub id: String,
105 pub trace_id: String,
106 pub timestamp: String,
107 pub level: String,
108 pub module: String,
109 pub message: String,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub metadata: Option<serde_json::Value>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
115pub struct LogFilters {
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub level: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub module: Option<String>,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub since: Option<String>,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub pattern: Option<String>,
124 pub tail: i64,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
128pub struct LogViewOutput {
129 pub logs: Vec<LogEntryRow>,
130 pub total: u64,
131 pub filters: LogFilters,
132}
133
134#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
135pub struct LogDeleteOutput {
136 pub deleted_count: u64,
137 pub vacuum_performed: bool,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
141pub struct LogCleanupOutput {
142 pub deleted_count: u64,
143 pub dry_run: bool,
144 pub cutoff_date: String,
145 pub vacuum_performed: bool,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
149pub struct LogExportOutput {
150 pub exported_count: u64,
151 pub format: String,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub file_path: Option<String>,
154}
155
156pub async fn execute(command: LogsCommands, config: &CliConfig) -> Result<()> {
157 match command {
158 LogsCommands::View(args) => {
159 let result = view::execute(args, config).await?;
160 render_result(&result);
161 Ok(())
162 },
163 LogsCommands::Search(args) => {
164 let result = search::execute(args, config).await?;
165 render_result(&result);
166 Ok(())
167 },
168 LogsCommands::Stream(args) => stream::execute(args, config).await,
169 LogsCommands::Export(args) => {
170 let result = export::execute(args, config).await?;
171 render_result(&result);
172 Ok(())
173 },
174 LogsCommands::Cleanup(args) => cleanup::execute(args, config).await,
175 LogsCommands::Delete(args) => delete::execute(args, config).await,
176 LogsCommands::Summary(args) => summary::execute(args, config).await,
177 LogsCommands::Show(args) => show::execute(args, config).await,
178 LogsCommands::Trace(cmd) => trace::execute(cmd, config).await,
179 LogsCommands::Request(cmd) => request::execute(cmd, config).await,
180 LogsCommands::Tools(cmd) => tools::execute(cmd, config).await,
181 LogsCommands::Audit(args) => audit::execute(args, config).await,
182 }
183}
184
185pub async fn execute_with_db(
186 command: LogsCommands,
187 db_ctx: &DatabaseContext,
188 config: &CliConfig,
189) -> Result<()> {
190 match command {
191 LogsCommands::View(args) => {
192 let result = view::execute_with_pool(args, db_ctx, config).await?;
193 render_result(&result);
194 Ok(())
195 },
196 LogsCommands::Search(args) => {
197 let result = search::execute_with_pool(args, db_ctx, config).await?;
198 render_result(&result);
199 Ok(())
200 },
201 LogsCommands::Summary(args) => summary::execute_with_pool(args, db_ctx, config).await,
202 LogsCommands::Export(args) => {
203 let result = export::execute_with_pool(args, db_ctx, config).await?;
204 render_result(&result);
205 Ok(())
206 },
207 LogsCommands::Show(args) => show::execute_with_pool(args, db_ctx, config).await,
208 LogsCommands::Trace(cmd) => trace::execute_with_pool(cmd, db_ctx, config).await,
209 LogsCommands::Request(cmd) => request::execute_with_pool(cmd, db_ctx, config).await,
210 LogsCommands::Tools(cmd) => tools::execute_with_pool(cmd, db_ctx, config).await,
211 LogsCommands::Audit(args) => audit::execute_with_pool(args, db_ctx, config).await,
212 LogsCommands::Stream(_) | LogsCommands::Cleanup(_) | LogsCommands::Delete(_) => {
213 bail!("This logs command requires full profile context")
214 },
215 }
216}