Skip to main content

systemprompt_cli/commands/infrastructure/logs/
mod.rs

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