Skip to main content

systemprompt_cli/commands/infrastructure/logs/request/
stats.rs

1//! `infra logs request stats`: aggregate AI request counts, token usage, cost,
2//! and latency, broken down by provider and model.
3
4use 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}