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