systemprompt_cli/commands/infrastructure/logs/request/
mod.rs1mod list;
10mod show;
11mod stats;
12
13use anyhow::Result;
14use clap::Subcommand;
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17
18use super::types::{MessageRow, ToolCallRow};
19use crate::context::CommandContext;
20use crate::shared::{CommandOutput, render_result};
21use systemprompt_identifiers::UserId;
22use systemprompt_models::artifacts::NoticeLine;
23
24pub use stats::{RequestStatsOutput, build_request_stats};
25
26const REQUEST_LIST_COLUMNS: [&str; 10] = [
27 "request_id",
28 "timestamp",
29 "user_id",
30 "actor",
31 "provider",
32 "model",
33 "tokens",
34 "cost",
35 "latency_ms",
36 "status",
37];
38
39#[must_use]
40pub fn build_request_list(rows: &[RequestListRow]) -> CommandOutput {
41 if rows.is_empty() {
42 return CommandOutput::message(vec![NoticeLine::new("info", "No AI requests found")]);
43 }
44 CommandOutput::table_of(REQUEST_LIST_COLUMNS.to_vec(), rows).with_title("AI Requests")
45}
46
47#[must_use]
48pub fn build_request_show(detail: &RequestShowOutput) -> CommandOutput {
49 CommandOutput::card_value("AI Request Details", detail)
50}
51
52#[must_use]
53pub fn request_show_not_found(request_id: &str) -> CommandOutput {
54 CommandOutput::message(vec![
55 NoticeLine::new("warning", format!("AI request not found: {request_id}")),
56 NoticeLine::new(
57 "info",
58 "Tip: Use 'systemprompt infra logs request list' to see recent requests",
59 ),
60 ])
61}
62
63#[derive(Debug, Subcommand)]
64pub enum RequestCommands {
65 #[command(
66 about = "Operational list of recent AI requests. For dashboard metrics (time range, model filter, CSV export), use `analytics requests list`",
67 after_help = "EXAMPLES:\n systemprompt infra logs request list\n systemprompt infra \
68 logs request list --model gpt-4 --since 1h"
69 )]
70 List(list::ListArgs),
71
72 #[command(
73 about = "Quick single-request view by request id (messages, linked MCP calls, status/error)",
74 after_help = "EXAMPLES:\n systemprompt infra logs request show abc123\n systemprompt \
75 infra logs request show abc123 --messages --tools"
76 )]
77 Show(show::ShowArgs),
78
79 #[command(
80 about = "Operational request aggregate with by-provider / by-model breakdown. For range/model-filtered dashboards with export, use `analytics requests stats`",
81 after_help = "EXAMPLES:\n systemprompt infra logs request stats\n systemprompt infra \
82 logs request stats --since 24h"
83 )]
84 Stats(stats::StatsArgs),
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
88pub struct RequestListRow {
89 pub request_id: String,
90 pub timestamp: String,
91 pub user_id: UserId,
92 pub actor: String,
93 pub provider: String,
94 pub model: String,
95 pub tokens: String,
96 pub cost: String,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub latency_ms: Option<i64>,
99 pub status: String,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
103pub struct RequestShowOutput {
104 pub request_id: String,
105 pub user_id: UserId,
106 pub actor_kind: String,
107 pub actor_id: String,
108 pub provider: Option<String>,
109 pub model: Option<String>,
110 pub input_tokens: i32,
111 pub output_tokens: i32,
112 pub cost_dollars: f64,
113 pub latency_ms: i64,
114 pub status: String,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub error_message: Option<String>,
117 pub messages: Vec<MessageRow>,
118 pub linked_mcp_calls: Vec<ToolCallRow>,
119}
120
121pub async fn execute(command: RequestCommands, ctx: &CommandContext) -> Result<()> {
122 match command {
123 RequestCommands::List(args) => {
124 let result = list::execute(args, ctx).await?;
125 render_result(&result, &ctx.cli);
126 Ok(())
127 },
128 RequestCommands::Show(args) => {
129 let result = show::execute(args, ctx).await?;
130 render_result(&result, &ctx.cli);
131 Ok(())
132 },
133 RequestCommands::Stats(args) => stats::execute(args, ctx).await,
134 }
135}