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