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