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