Skip to main content

systemprompt_cli/commands/infrastructure/logs/
mod.rs

1//! `infra logs` command surface: querying, searching, and tracing the log
2//! store.
3//!
4//! Dispatches the [`LogsCommands`] subcommands (view, search, stream, export,
5//! cleanup, delete, summary, show, trace, request, tools, audit) and defines
6//! the serializable output rows shared across them. Two dispatch paths exist:
7//! [`execute`] boots its own context, while [`execute_with_db`] reuses an
8//! already-open [`DatabaseContext`] for the read-only subcommands.
9
10mod 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}