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