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::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}