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("provider", ColumnType::String),
36 Column::new("model", ColumnType::String),
37 Column::new("tokens", ColumnType::String),
38 Column::new("cost", ColumnType::String),
39 Column::new("latency_ms", ColumnType::Number),
40 Column::new("status", ColumnType::String),
41 ]
42}
43
44#[must_use]
45pub fn build_request_list(rows: &[RequestListRow]) -> CommandOutput {
46 if rows.is_empty() {
47 return CommandOutput::message(vec![NoticeLine::new("info", "No AI requests found")]);
48 }
49 let items: Vec<JsonValue> = rows
50 .iter()
51 .map(|row| serde_json::to_value(row).unwrap_or(JsonValue::Null))
52 .collect();
53 CommandOutput::table_artifact(TableArtifact::new(request_list_columns()).with_rows(items))
54 .with_title("AI Requests")
55}
56
57#[must_use]
58pub fn build_request_show(detail: &RequestShowOutput) -> CommandOutput {
59 CommandOutput::card_value("AI Request Details", detail)
60}
61
62#[must_use]
63pub fn request_show_not_found(request_id: &str) -> CommandOutput {
64 CommandOutput::message(vec![
65 NoticeLine::new("warning", format!("AI request not found: {request_id}")),
66 NoticeLine::new(
67 "info",
68 "Tip: Use 'systemprompt infra logs request list' to see recent requests",
69 ),
70 ])
71}
72
73#[derive(Debug, Subcommand)]
74pub enum RequestCommands {
75 #[command(
76 about = "Operational list of recent AI requests. For dashboard metrics (time range, model filter, CSV export), use `analytics requests list`",
77 after_help = "EXAMPLES:\n systemprompt infra logs request list\n systemprompt infra \
78 logs request list --model gpt-4 --since 1h"
79 )]
80 List(list::ListArgs),
81
82 #[command(
83 about = "Quick single-request view by request id (messages, linked MCP calls, status/error)",
84 after_help = "EXAMPLES:\n systemprompt infra logs request show abc123\n systemprompt \
85 infra logs request show abc123 --messages --tools"
86 )]
87 Show(show::ShowArgs),
88
89 #[command(
90 about = "Operational request aggregate with by-provider / by-model breakdown. For range/model-filtered dashboards with export, use `analytics requests stats`",
91 after_help = "EXAMPLES:\n systemprompt infra logs request stats\n systemprompt infra \
92 logs request stats --since 24h"
93 )]
94 Stats(stats::StatsArgs),
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
98pub struct RequestListRow {
99 pub request_id: String,
100 pub timestamp: String,
101 pub user_id: UserId,
102 pub actor: String,
103 pub provider: String,
104 pub model: String,
105 pub tokens: String,
106 pub cost: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub latency_ms: Option<i64>,
109 pub status: String,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
113pub struct RequestShowOutput {
114 pub request_id: String,
115 pub user_id: UserId,
116 pub actor_kind: String,
117 pub actor_id: String,
118 pub provider: Option<String>,
119 pub model: Option<String>,
120 pub input_tokens: i32,
121 pub output_tokens: i32,
122 pub cost_dollars: f64,
123 pub latency_ms: i64,
124 pub status: String,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub error_message: Option<String>,
127 pub messages: Vec<MessageRow>,
128 pub linked_mcp_calls: Vec<ToolCallRow>,
129}
130
131pub async fn execute(command: RequestCommands, ctx: &CommandContext) -> Result<()> {
132 match command {
133 RequestCommands::List(args) => {
134 let result = list::execute(args, ctx).await?;
135 render_result(&result, &ctx.cli);
136 Ok(())
137 },
138 RequestCommands::Show(args) => {
139 let result = show::execute(args, ctx).await?;
140 render_result(&result, &ctx.cli);
141 Ok(())
142 },
143 RequestCommands::Stats(args) => stats::execute(args, ctx).await,
144 }
145}