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;
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}