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