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