systemprompt_analytics/repository/
requests.rs1use crate::Result;
12use chrono::{DateTime, Utc};
13use sqlx::PgPool;
14use std::sync::Arc;
15use systemprompt_database::DbPool;
16use systemprompt_identifiers::UserId;
17
18use crate::models::reporting::{
19 ModelUsageRow, RequestListFilter, RequestListRow, RequestStatsRow, RequestTrendRow,
20};
21
22#[derive(Debug)]
23pub struct RequestAnalyticsRepository {
24 pool: Arc<PgPool>,
25}
26
27impl RequestAnalyticsRepository {
28 pub fn new(db: &DbPool) -> Result<Self> {
29 let pool = db.pool_arc()?;
30 Ok(Self { pool })
31 }
32
33 pub async fn get_stats(
34 &self,
35 start: DateTime<Utc>,
36 end: DateTime<Utc>,
37 model_filter: Option<&str>,
38 ) -> Result<RequestStatsRow> {
39 if let Some(model) = model_filter {
40 let pattern = format!("%{model}%");
41 sqlx::query_as!(
42 RequestStatsRow,
43 r#"
44 SELECT
45 COUNT(*)::bigint as "total!",
46 SUM(tokens_used)::bigint as "total_tokens",
47 SUM(input_tokens)::bigint as "input_tokens",
48 SUM(output_tokens)::bigint as "output_tokens",
49 SUM(reasoning_tokens)::bigint as "reasoning_tokens",
50 SUM(cache_read_tokens)::bigint as "cache_read_tokens",
51 SUM(cache_creation_tokens)::bigint as "cache_creation_tokens",
52 SUM(cost_microdollars)::bigint as "cost",
53 AVG(latency_ms)::float8 as "avg_latency",
54 COUNT(*) FILTER (WHERE cache_hit = true)::bigint as "cache_hits!"
55 FROM analytics_report_ai_requests
56 WHERE created_at >= $1 AND created_at < $2
57 AND model ILIKE $3
58 "#,
59 start,
60 end,
61 pattern
62 )
63 .fetch_one(&*self.pool)
64 .await
65 .map_err(Into::into)
66 } else {
67 sqlx::query_as!(
68 RequestStatsRow,
69 r#"
70 SELECT
71 COUNT(*)::bigint as "total!",
72 SUM(tokens_used)::bigint as "total_tokens",
73 SUM(input_tokens)::bigint as "input_tokens",
74 SUM(output_tokens)::bigint as "output_tokens",
75 SUM(reasoning_tokens)::bigint as "reasoning_tokens",
76 SUM(cache_read_tokens)::bigint as "cache_read_tokens",
77 SUM(cache_creation_tokens)::bigint as "cache_creation_tokens",
78 SUM(cost_microdollars)::bigint as "cost",
79 AVG(latency_ms)::float8 as "avg_latency",
80 COUNT(*) FILTER (WHERE cache_hit = true)::bigint as "cache_hits!"
81 FROM analytics_report_ai_requests
82 WHERE created_at >= $1 AND created_at < $2
83 "#,
84 start,
85 end
86 )
87 .fetch_one(&*self.pool)
88 .await
89 .map_err(Into::into)
90 }
91 }
92
93 pub async fn list_models(
94 &self,
95 start: DateTime<Utc>,
96 end: DateTime<Utc>,
97 limit: i64,
98 ) -> Result<Vec<ModelUsageRow>> {
99 sqlx::query_as!(
100 ModelUsageRow,
101 r#"
102 SELECT
103 provider as "provider!",
104 model as "model!",
105 COUNT(*)::bigint as "request_count!",
106 SUM(tokens_used)::bigint as "total_tokens",
107 SUM(cost_microdollars)::bigint as "total_cost",
108 AVG(latency_ms)::float8 as "avg_latency"
109 FROM analytics_report_ai_requests
110 WHERE created_at >= $1 AND created_at < $2
111 AND provider IS NOT NULL AND model IS NOT NULL
112 GROUP BY provider, model
113 ORDER BY COUNT(*) DESC
114 LIMIT $3
115 "#,
116 start,
117 end,
118 limit
119 )
120 .fetch_all(&*self.pool)
121 .await
122 .map_err(Into::into)
123 }
124
125 pub async fn get_requests_for_trends(
126 &self,
127 start: DateTime<Utc>,
128 end: DateTime<Utc>,
129 ) -> Result<Vec<RequestTrendRow>> {
130 sqlx::query_as!(
131 RequestTrendRow,
132 r#"
133 SELECT
134 created_at as "created_at!",
135 tokens_used,
136 cost_microdollars,
137 latency_ms
138 FROM analytics_report_ai_requests
139 WHERE created_at >= $1 AND created_at < $2
140 ORDER BY created_at
141 "#,
142 start,
143 end
144 )
145 .fetch_all(&*self.pool)
146 .await
147 .map_err(Into::into)
148 }
149
150 pub async fn list_requests(
151 &self,
152 start: DateTime<Utc>,
153 end: DateTime<Utc>,
154 filter: &RequestListFilter,
155 ) -> Result<Vec<RequestListRow>> {
156 let pattern = filter.model.as_deref().map(|model| format!("%{model}%"));
157 let user = filter.user.as_ref().map(UserId::as_str);
158 sqlx::query_as!(
159 RequestListRow,
160 r#"
161 SELECT
162 id as "id!",
163 provider,
164 model,
165 input_tokens,
166 output_tokens,
167 cost_microdollars,
168 latency_ms,
169 cache_hit,
170 created_at as "created_at!",
171 status as "status!",
172 error_message,
173 user_id as "user_id!: UserId"
174 FROM analytics_report_ai_requests
175 WHERE created_at >= $1 AND created_at < $2
176 AND ($3::text IS NULL OR model ILIKE $3)
177 AND ($4::text IS NULL OR user_id = $4)
178 ORDER BY created_at DESC, id DESC
179 LIMIT $5 OFFSET $6
180 "#,
181 start,
182 end,
183 pattern,
184 user,
185 filter.limit,
186 filter.offset
187 )
188 .fetch_all(&*self.pool)
189 .await
190 .map_err(Into::into)
191 }
192}