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