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