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