systemprompt_cli/commands/infrastructure/logs/request/
stats.rs1use anyhow::Result;
5use clap::Args;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9use systemprompt_logging::TraceQueryService;
10
11use crate::commands::infrastructure::logs::duration::parse_since;
12use crate::shared::{CommandOutput, render_result};
13
14#[derive(Debug, Args)]
15pub struct StatsArgs {
16 #[arg(
17 long,
18 help = "Only include requests since this duration (e.g., '1h', '24h', '7d')"
19 )]
20 pub since: Option<String>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
24pub struct RequestStatsOutput {
25 pub total_requests: i64,
26 pub total_tokens: TokenStats,
27 pub total_cost_dollars: f64,
28 pub average_latency_ms: i64,
29 pub by_provider: Vec<ProviderStats>,
30 pub by_model: Vec<ModelStats>,
31}
32
33#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
34pub struct TokenStats {
35 pub input: i64,
36 pub output: i64,
37 pub total: i64,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
41pub struct ProviderStats {
42 pub provider: String,
43 pub request_count: i64,
44 pub total_tokens: i64,
45 pub total_cost_dollars: f64,
46 pub avg_latency_ms: i64,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
50pub struct ModelStats {
51 pub model: String,
52 pub provider: String,
53 pub request_count: i64,
54 pub total_tokens: i64,
55 pub total_cost_dollars: f64,
56 pub avg_latency_ms: i64,
57}
58
59crate::define_pool_command!(StatsArgs => (), no_config);
60
61async fn execute_with_pool_inner(args: StatsArgs, pool: &Arc<sqlx::PgPool>) -> Result<()> {
62 let since_timestamp = parse_since(args.since.as_ref())?;
63
64 let service = TraceQueryService::new(Arc::clone(pool));
65 let stats = service.get_ai_request_stats(since_timestamp).await?;
66
67 let input_tokens = stats.total_input_tokens;
68 let output_tokens = stats.total_output_tokens;
69
70 let output = RequestStatsOutput {
71 total_requests: stats.total_requests,
72 total_tokens: TokenStats {
73 input: input_tokens,
74 output: output_tokens,
75 total: input_tokens + output_tokens,
76 },
77 total_cost_dollars: f64::from(stats.total_cost_microdollars as i32) / 1_000_000.0,
78 average_latency_ms: stats.avg_latency_ms,
79 by_provider: stats
80 .by_provider
81 .into_iter()
82 .map(|r| ProviderStats {
83 provider: r.provider,
84 request_count: r.request_count,
85 total_tokens: r.total_tokens,
86 total_cost_dollars: f64::from(r.total_cost_microdollars as i32) / 1_000_000.0,
87 avg_latency_ms: r.avg_latency_ms,
88 })
89 .collect(),
90 by_model: stats
91 .by_model
92 .into_iter()
93 .map(|r| ModelStats {
94 model: r.model,
95 provider: r.provider,
96 request_count: r.request_count,
97 total_tokens: r.total_tokens,
98 total_cost_dollars: f64::from(r.total_cost_microdollars as i32) / 1_000_000.0,
99 avg_latency_ms: r.avg_latency_ms,
100 })
101 .collect(),
102 };
103
104 render_result(&build_request_stats(&output));
105
106 Ok(())
107}
108
109#[must_use]
110pub fn build_request_stats(output: &RequestStatsOutput) -> CommandOutput {
111 CommandOutput::card_value("AI Request Statistics", output)
112}