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 serde_json::Value as JsonValue;
22use systemprompt_identifiers::UserId;
23use systemprompt_models::artifacts::{Column, ColumnType, NoticeLine, TableArtifact};
24
25pub use stats::{RequestStatsOutput, build_request_stats};
26
27const REQUEST_ID_DISPLAY_WIDTH: usize = 12;
28
29fn request_list_columns() -> Vec<Column> {
30 vec![
31 Column::new("request_id", ColumnType::String).with_width(REQUEST_ID_DISPLAY_WIDTH),
32 Column::new("timestamp", ColumnType::String),
33 Column::new("user_id", ColumnType::String),
34 Column::new("actor", ColumnType::String),
35 Column::new("client", ColumnType::String),
36 Column::new("attestation", ColumnType::String),
37 Column::new("provider", ColumnType::String),
38 Column::new("model", ColumnType::String),
39 Column::new("tokens", ColumnType::String),
40 Column::new("cost", ColumnType::String),
41 Column::new("latency_ms", ColumnType::Number),
42 Column::new("status", ColumnType::String),
43 ]
44}
45
46#[must_use]
47pub fn build_request_list(rows: &[RequestListRow]) -> CommandOutput {
48 if rows.is_empty() {
49 return CommandOutput::message(vec![NoticeLine::new("info", "No AI requests found")]);
50 }
51 let items: Vec<JsonValue> = rows
52 .iter()
53 .map(|row| serde_json::to_value(row).unwrap_or(JsonValue::Null))
54 .collect();
55 CommandOutput::table_artifact(TableArtifact::new(request_list_columns()).with_rows(items))
56 .with_title("AI Requests")
57}
58
59#[must_use]
60pub fn build_request_show(detail: &RequestShowOutput) -> CommandOutput {
61 CommandOutput::card_value("AI Request Details", detail)
62}
63
64#[must_use]
65pub fn request_show_not_found(request_id: &str) -> CommandOutput {
66 CommandOutput::message(vec![
67 NoticeLine::new("warning", format!("AI request not found: {request_id}")),
68 NoticeLine::new(
69 "info",
70 "Tip: Use 'systemprompt infra logs request list' to see recent requests",
71 ),
72 ])
73}
74
75#[derive(Debug, Subcommand)]
76pub enum RequestCommands {
77 #[command(
78 about = "Operational list of recent AI requests. For dashboard metrics (time range, model filter, CSV export), use `analytics requests list`",
79 after_help = "EXAMPLES:\n systemprompt infra logs request list\n systemprompt infra \
80 logs request list --model gpt-4 --since 1h"
81 )]
82 List(list::ListArgs),
83
84 #[command(
85 about = "Quick single-request view by request id (messages, linked MCP calls, status/error)",
86 after_help = "EXAMPLES:\n systemprompt infra logs request show abc123\n systemprompt \
87 infra logs request show abc123 --messages --tools"
88 )]
89 Show(show::ShowArgs),
90
91 #[command(
92 about = "Operational request aggregate with by-provider / by-model breakdown. For range/model-filtered dashboards with export, use `analytics requests stats`",
93 after_help = "EXAMPLES:\n systemprompt infra logs request stats\n systemprompt infra \
94 logs request stats --since 24h"
95 )]
96 Stats(stats::StatsArgs),
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
100pub struct RequestListRow {
101 pub request_id: String,
102 pub timestamp: String,
103 pub cursor: String,
104 pub user_id: UserId,
105 pub actor: String,
106 pub client: String,
107 pub attestation: String,
108 pub provider: String,
109 pub model: String,
110 pub tokens: String,
111 pub cost: String,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub latency_ms: Option<i64>,
114 pub status: String,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
118pub struct RequestShowOutput {
119 pub request_id: String,
120 pub user_id: UserId,
121 pub actor_kind: String,
122 pub actor_id: String,
123 pub client: String,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub client_evidence: Option<ClientEvidenceOutput>,
126 pub provider: Option<String>,
127 pub model: Option<String>,
128 pub input_tokens: i32,
129 pub output_tokens: i32,
130 pub cost_dollars: f64,
131 pub latency_ms: i64,
132 pub status: String,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub error_message: Option<String>,
135 pub messages: Vec<MessageRow>,
136 pub linked_mcp_calls: Vec<ToolCallRow>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
142pub struct ClientEvidenceOutput {
143 pub kind_source: String,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub attested_host: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub declared_client: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub native_marker: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub ua_product: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub ua_version: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub sdk_lang: Option<String>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub sdk_package_version: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub sdk_runtime: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub sdk_runtime_version: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub sdk_os: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub sdk_arch: Option<String>,
166}
167
168impl From<systemprompt_runtime::AiRequestClientEvidence> for ClientEvidenceOutput {
169 fn from(evidence: systemprompt_runtime::AiRequestClientEvidence) -> Self {
170 Self {
171 kind_source: evidence.kind_source,
172 attested_host: evidence.attested_host,
173 declared_client: evidence.declared_client,
174 native_marker: evidence.native_marker,
175 ua_product: evidence.ua_product,
176 ua_version: evidence.ua_version,
177 sdk_lang: evidence.sdk_lang,
178 sdk_package_version: evidence.sdk_package_version,
179 sdk_runtime: evidence.sdk_runtime,
180 sdk_runtime_version: evidence.sdk_runtime_version,
181 sdk_os: evidence.sdk_os,
182 sdk_arch: evidence.sdk_arch,
183 }
184 }
185}
186
187pub async fn execute(command: RequestCommands, ctx: &CommandContext) -> Result<()> {
188 match command {
189 RequestCommands::List(args) => {
190 let result = list::execute(args, ctx).await?;
191 render_result(&result, &ctx.cli);
192 Ok(())
193 },
194 RequestCommands::Show(args) => {
195 let result = show::execute(args, ctx).await?;
196 render_result(&result, &ctx.cli);
197 Ok(())
198 },
199 RequestCommands::Stats(args) => stats::execute(args, ctx).await,
200 }
201}