Skip to main content

systemprompt_cli/commands/infrastructure/logs/trace/
mod.rs

1//! `infra logs trace` subcommands for reconstructing execution traces.
2//!
3//! Exposes [`TraceCommands`] (list, show) and the family of serializable rows
4//! that describe a trace: per-event timeline ([`TraceEventRow`]), the AI / MCP
5//! / step summaries, and the assembled [`TraceViewOutput`] and
6//! [`AiTraceOutput`]. A trace is resolved either from log events or from an AI
7//! task, with the display split across the sibling `display`, `ai_*`,
8//! `summary`, and `json` modules.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13mod ai_artifacts;
14mod ai_display;
15mod ai_mcp;
16mod ai_trace_display;
17mod display;
18mod json;
19mod list;
20mod show;
21mod summary;
22
23pub use summary::{SummaryContext, print_summary};
24
25use anyhow::Result;
26use clap::Subcommand;
27use schemars::JsonSchema;
28use serde::{Deserialize, Serialize};
29use systemprompt_identifiers::{AiRequestId, TraceId};
30
31use super::types::ToolCallRow;
32use crate::context::CommandContext;
33use crate::shared::render_result;
34
35#[derive(Debug, Subcommand)]
36pub enum TraceCommands {
37    #[command(
38        about = "List recent traces",
39        after_help = "EXAMPLES:\n  systemprompt infra logs trace list\n  systemprompt infra logs \
40                      trace list --limit 50 --since 1h\n  systemprompt infra logs trace list \
41                      --agent researcher --status completed"
42    )]
43    List(list::ListArgs),
44
45    #[command(
46        about = "Show trace details",
47        after_help = "EXAMPLES:\n  systemprompt infra logs trace show abc123\n  systemprompt \
48                      infra logs trace show abc123 --verbose\n  systemprompt infra logs trace \
49                      show abc123 --steps --ai --mcp"
50    )]
51    Show(show::ShowArgs),
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
55pub struct TraceEventRow {
56    pub timestamp: String,
57    pub delta_ms: i64,
58    pub event_type: String,
59    pub details: String,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub latency_ms: Option<i64>,
62}
63
64#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
65pub struct AiSummaryRow {
66    pub request_count: i64,
67    pub total_tokens: i64,
68    pub input_tokens: i64,
69    pub output_tokens: i64,
70    pub cost_dollars: f64,
71    pub total_latency_ms: i64,
72}
73
74#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
75pub struct McpSummaryRow {
76    pub execution_count: i64,
77    pub total_execution_time_ms: i64,
78}
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
81pub struct StepSummaryRow {
82    pub total: i64,
83    pub completed: i64,
84    pub failed: i64,
85    pub pending: i64,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
89pub struct TraceViewOutput {
90    pub trace_id: TraceId,
91    pub events: Vec<TraceEventRow>,
92    pub ai_summary: AiSummaryRow,
93    pub mcp_summary: McpSummaryRow,
94    pub step_summary: StepSummaryRow,
95    #[serde(skip_serializing_if = "Option::is_none", rename = "task_id")]
96    pub task: Option<String>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub duration_ms: Option<i64>,
99    pub status: String,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
103pub struct TraceListRow {
104    pub trace_id: TraceId,
105    pub timestamp: String,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub agent: Option<String>,
108    pub status: String,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub duration_ms: Option<i64>,
111    pub ai_requests: i64,
112    pub mcp_calls: i64,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
116pub struct TraceListOutput {
117    pub traces: Vec<TraceListRow>,
118    pub total: u64,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
122pub struct TaskInfoRow {
123    #[serde(rename = "task_id")]
124    pub task: String,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub agent_name: Option<String>,
127    pub status: String,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub started_at: Option<String>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub duration_ms: Option<i64>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
135pub struct StepRow {
136    pub step_number: i32,
137    pub step_type: String,
138    pub title: String,
139    pub status: String,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub duration_ms: Option<i64>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
145pub struct AiRequestRow {
146    pub request_id: AiRequestId,
147    pub model: String,
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub max_tokens: Option<i32>,
150    pub tokens: String,
151    pub cost: String,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub latency_ms: Option<i64>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
157pub struct ArtifactRow {
158    #[serde(rename = "artifact_id")]
159    pub artifact: String,
160    pub artifact_type: String,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub name: Option<String>,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub source: Option<String>,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub tool_name: Option<String>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
170pub struct AiTraceOutput {
171    #[serde(rename = "task_id")]
172    pub task: String,
173    pub task_info: TaskInfoRow,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub user_input: Option<String>,
176    pub execution_steps: Vec<StepRow>,
177    pub ai_requests: Vec<AiRequestRow>,
178    pub mcp_executions: Vec<ToolCallRow>,
179    pub artifacts: Vec<ArtifactRow>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub agent_response: Option<String>,
182}
183
184pub async fn execute(command: TraceCommands, ctx: &CommandContext) -> Result<()> {
185    match command {
186        TraceCommands::List(args) => list::execute(args, ctx).await,
187        TraceCommands::Show(args) => {
188            let result = show::execute(args, ctx).await?;
189            render_result(&result, &ctx.cli);
190            Ok(())
191        },
192    }
193}