Skip to main content

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

1//! Trace summary rendering: event counts, AI and MCP rollups.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use chrono::{DateTime, Utc};
7use std::collections::HashMap;
8use systemprompt_identifiers::TaskId;
9use systemprompt_logging::{
10    AiRequestSummary, CliService, ExecutionStepSummary, McpExecutionSummary, TraceEvent,
11};
12
13#[derive(Debug)]
14pub struct SummaryContext<'a> {
15    pub events: &'a [TraceEvent],
16    pub first: Option<DateTime<Utc>>,
17    pub last: Option<DateTime<Utc>>,
18    pub task_id: Option<&'a TaskId>,
19    pub ai_summary: &'a AiRequestSummary,
20    pub mcp_summary: &'a McpExecutionSummary,
21    pub step_summary: &'a ExecutionStepSummary,
22}
23
24pub fn print_summary(ctx: &SummaryContext<'_>) {
25    CliService::section("Summary");
26
27    if let (Some(first), Some(last)) = (ctx.first, ctx.last) {
28        let duration = last.signed_duration_since(first);
29        CliService::key_value("  Duration", &format!("{}ms", duration.num_milliseconds()));
30    }
31
32    print_event_counts(ctx.events);
33    print_ai_summary(ctx.ai_summary);
34    print_mcp_summary(ctx.mcp_summary);
35    print_step_summary(ctx.step_summary);
36    print_trace_context(ctx.events, ctx.task_id);
37    print_status(ctx.events);
38}
39
40fn print_event_counts(events: &[TraceEvent]) {
41    let mut event_counts: HashMap<String, usize> = HashMap::new();
42    for event in events {
43        *event_counts.entry(event.event_type.clone()).or_insert(0) += 1;
44    }
45
46    let mut count_vec: Vec<_> = event_counts.iter().collect();
47    count_vec.sort_by_key(|&(k, _)| k);
48
49    let event_parts: Vec<String> = count_vec
50        .iter()
51        .map(|(k, v)| format!("{} {}", v, k))
52        .collect();
53    CliService::key_value(
54        "  Events",
55        &format!("{} ({})", events.len(), event_parts.join(", ")),
56    );
57}
58
59fn print_ai_summary(ai_summary: &AiRequestSummary) {
60    if ai_summary.request_count > 0 {
61        CliService::info("  AI Requests:");
62        CliService::key_value("     Requests", &ai_summary.request_count.to_string());
63        CliService::key_value(
64            "     Tokens",
65            &format!(
66                "{} (in: {}, out: {})",
67                ai_summary.total_tokens,
68                ai_summary.total_input_tokens,
69                ai_summary.total_output_tokens
70            ),
71        );
72        let dollars = ai_summary.total_cost_microdollars as f64 / 1_000_000.0;
73        CliService::key_value("     Cost", &format!("${dollars:.6}"));
74        CliService::key_value(
75            "     Total Latency",
76            &format!("{}ms", ai_summary.total_latency_ms),
77        );
78        if ai_summary.request_count > 0 {
79            let avg_latency = ai_summary.total_latency_ms / ai_summary.request_count;
80            CliService::key_value("     Avg Latency", &format!("{avg_latency}ms"));
81        }
82    }
83}
84
85fn print_mcp_summary(mcp_summary: &McpExecutionSummary) {
86    if mcp_summary.execution_count > 0 {
87        CliService::info("  MCP Tool Executions:");
88        CliService::key_value("     Executions", &mcp_summary.execution_count.to_string());
89        CliService::key_value(
90            "     Total Time",
91            &format!("{}ms", mcp_summary.total_execution_time_ms),
92        );
93        if mcp_summary.execution_count > 0 {
94            let avg_time = mcp_summary.total_execution_time_ms / mcp_summary.execution_count;
95            CliService::key_value("     Avg Time", &format!("{avg_time}ms"));
96        }
97    }
98}
99
100fn print_step_summary(step_summary: &ExecutionStepSummary) {
101    if step_summary.total > 0 {
102        CliService::info("  Execution Steps:");
103        CliService::key_value(
104            "     Steps",
105            &format!(
106                "{} ({} completed, {} failed, {} pending)",
107                step_summary.total,
108                step_summary.completed,
109                step_summary.failed,
110                step_summary.pending
111            ),
112        );
113    }
114}
115
116fn print_trace_context(events: &[TraceEvent], task_id: Option<&TaskId>) {
117    if let Some(task_id) = task_id {
118        CliService::key_value(
119            "  Task",
120            &format!("{} (use: just ai-trace <task_id>)", task_id.as_str()),
121        );
122    }
123
124    if let Some(session_id) = events.first().and_then(|e| e.session_id.as_ref()) {
125        CliService::key_value("  Session", session_id.as_str());
126    }
127
128    if let Some(user_id) = events.first().and_then(|e| e.user_id.as_ref()) {
129        CliService::key_value("  User", user_id.as_str());
130    }
131}
132
133fn print_status(events: &[TraceEvent]) {
134    let has_errors = events.iter().any(|e| {
135        e.details.contains("ERROR")
136            || e.details.contains("failed")
137            || e.details.contains("(failed)")
138    });
139
140    if has_errors {
141        CliService::error("  Status: [FAILED]");
142    } else {
143        CliService::success("  Status: [OK]");
144    }
145}