1use serde::{Deserialize, Serialize};
4
5use crate::error::Result;
6use crate::DakeraClient;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct AnalyticsOverview {
15 pub total_queries: u64,
16 pub avg_latency_ms: f64,
17 pub p95_latency_ms: f64,
18 pub p99_latency_ms: f64,
19 pub queries_per_second: f64,
20 pub error_rate: f64,
21 pub cache_hit_rate: f64,
22 pub storage_used_bytes: u64,
23 pub total_vectors: u64,
24 pub total_namespaces: u64,
25 pub uptime_seconds: u64,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct LatencyAnalytics {
31 pub period: String,
32 pub avg_ms: f64,
33 pub p50_ms: f64,
34 pub p95_ms: f64,
35 pub p99_ms: f64,
36 pub max_ms: f64,
37 #[serde(default)]
38 pub by_operation: std::collections::HashMap<String, OperationLatency>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct OperationLatency {
44 pub avg_ms: f64,
45 pub p95_ms: f64,
46 pub count: u64,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ThroughputAnalytics {
52 pub period: String,
53 pub total_operations: u64,
54 pub operations_per_second: f64,
55 #[serde(default)]
56 pub by_operation: std::collections::HashMap<String, u64>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct StorageAnalytics {
62 pub total_bytes: u64,
63 pub index_bytes: u64,
64 pub data_bytes: u64,
65 #[serde(default)]
66 pub by_namespace: std::collections::HashMap<String, NamespaceStorage>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct NamespaceStorage {
72 pub bytes: u64,
73 pub vector_count: u64,
74}
75
76impl DakeraClient {
81 pub async fn analytics_overview(
83 &self,
84 period: Option<&str>,
85 namespace: Option<&str>,
86 ) -> Result<AnalyticsOverview> {
87 let mut url = format!("{}/v1/analytics/overview", self.base_url);
88 let mut params = Vec::new();
89 if let Some(p) = period {
90 params.push(format!("period={}", p));
91 }
92 if let Some(ns) = namespace {
93 params.push(format!("namespace={}", ns));
94 }
95 if !params.is_empty() {
96 url.push('?');
97 url.push_str(¶ms.join("&"));
98 }
99 let response = self.client.get(&url).send().await?;
100 self.handle_response(response).await
101 }
102
103 pub async fn analytics_latency(
105 &self,
106 period: Option<&str>,
107 namespace: Option<&str>,
108 ) -> Result<LatencyAnalytics> {
109 let mut url = format!("{}/v1/analytics/latency", self.base_url);
110 let mut params = Vec::new();
111 if let Some(p) = period {
112 params.push(format!("period={}", p));
113 }
114 if let Some(ns) = namespace {
115 params.push(format!("namespace={}", ns));
116 }
117 if !params.is_empty() {
118 url.push('?');
119 url.push_str(¶ms.join("&"));
120 }
121 let response = self.client.get(&url).send().await?;
122 self.handle_response(response).await
123 }
124
125 pub async fn analytics_throughput(
127 &self,
128 period: Option<&str>,
129 namespace: Option<&str>,
130 ) -> Result<ThroughputAnalytics> {
131 let mut url = format!("{}/v1/analytics/throughput", self.base_url);
132 let mut params = Vec::new();
133 if let Some(p) = period {
134 params.push(format!("period={}", p));
135 }
136 if let Some(ns) = namespace {
137 params.push(format!("namespace={}", ns));
138 }
139 if !params.is_empty() {
140 url.push('?');
141 url.push_str(¶ms.join("&"));
142 }
143 let response = self.client.get(&url).send().await?;
144 self.handle_response(response).await
145 }
146
147 pub async fn analytics_storage(&self, namespace: Option<&str>) -> Result<StorageAnalytics> {
149 let mut url = format!("{}/v1/analytics/storage", self.base_url);
150 if let Some(ns) = namespace {
151 url.push_str(&format!("?namespace={}", ns));
152 }
153 let response = self.client.get(&url).send().await?;
154 self.handle_response(response).await
155 }
156}
157
158#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
171 fn test_analytics_overview_deserializes_all_numeric_fields() {
172 let json = r#"{
173 "total_queries": 100,
174 "avg_latency_ms": 12.5,
175 "p95_latency_ms": 30.0,
176 "p99_latency_ms": 60.0,
177 "queries_per_second": 5.0,
178 "error_rate": 0.01,
179 "cache_hit_rate": 0.87,
180 "storage_used_bytes": 1048576,
181 "total_vectors": 95,
182 "total_namespaces": 3,
183 "uptime_seconds": 86400
184 }"#;
185 let overview: AnalyticsOverview = serde_json::from_str(json).unwrap();
186 assert_eq!(overview.total_queries, 100);
187 assert!((overview.avg_latency_ms - 12.5).abs() < 1e-6);
188 assert!((overview.cache_hit_rate - 0.87).abs() < 1e-6);
189 assert_eq!(overview.uptime_seconds, 86400);
190 }
191
192 #[test]
197 fn test_latency_analytics_by_operation_defaults_empty() {
198 let json = r#"{
199 "period": "1h",
200 "avg_ms": 10.0,
201 "p50_ms": 8.0,
202 "p95_ms": 25.0,
203 "p99_ms": 50.0,
204 "max_ms": 120.0
205 }"#;
206 let la: LatencyAnalytics = serde_json::from_str(json).unwrap();
207 assert_eq!(la.period, "1h");
208 assert!(la.by_operation.is_empty());
209 }
210
211 #[test]
212 fn test_latency_analytics_with_by_operation() {
213 let json = r#"{
214 "period": "24h",
215 "avg_ms": 15.0,
216 "p50_ms": 12.0,
217 "p95_ms": 40.0,
218 "p99_ms": 80.0,
219 "max_ms": 200.0,
220 "by_operation": {
221 "recall": {"avg_ms": 18.0, "p95_ms": 45.0, "count": 3000},
222 "store": {"avg_ms": 10.0, "p95_ms": 30.0, "count": 2000}
223 }
224 }"#;
225 let la: LatencyAnalytics = serde_json::from_str(json).unwrap();
226 assert_eq!(la.by_operation.len(), 2);
227 assert!((la.by_operation["recall"].avg_ms - 18.0).abs() < 1e-6);
228 assert_eq!(la.by_operation["store"].count, 2000);
229 }
230
231 #[test]
236 fn test_operation_latency_deserializes() {
237 let json = r#"{"avg_ms": 9.5, "p95_ms": 22.0, "count": 100}"#;
238 let op: OperationLatency = serde_json::from_str(json).unwrap();
239 assert!((op.avg_ms - 9.5).abs() < 1e-6);
240 assert_eq!(op.count, 100);
241 }
242
243 #[test]
248 fn test_throughput_analytics_by_operation_defaults_empty() {
249 let json = r#"{
250 "period": "1h",
251 "operations_per_second": 42.5,
252 "total_operations": 153000
253 }"#;
254 let ta: ThroughputAnalytics = serde_json::from_str(json).unwrap();
255 assert!((ta.operations_per_second - 42.5).abs() < 1e-6);
256 assert!(ta.by_operation.is_empty());
257 }
258
259 #[test]
264 fn test_storage_analytics_by_namespace_defaults_empty() {
265 let json = r#"{
266 "total_bytes": 2097152,
267 "index_bytes": 512000,
268 "data_bytes": 1585152
269 }"#;
270 let sa: StorageAnalytics = serde_json::from_str(json).unwrap();
271 assert_eq!(sa.total_bytes, 2097152);
272 assert!(sa.by_namespace.is_empty());
273 }
274
275 #[test]
276 fn test_storage_analytics_with_namespaces() {
277 let json = r#"{
278 "total_bytes": 4194304,
279 "index_bytes": 1048576,
280 "data_bytes": 3145728,
281 "by_namespace": {
282 "default": {"bytes": 2097152, "vector_count": 500},
283 "archive": {"bytes": 2097152, "vector_count": 500}
284 }
285 }"#;
286 let sa: StorageAnalytics = serde_json::from_str(json).unwrap();
287 assert_eq!(sa.by_namespace.len(), 2);
288 assert_eq!(sa.by_namespace["default"].vector_count, 500);
289 }
290
291 #[test]
296 fn test_namespace_storage_deserializes() {
297 let json = r#"{"bytes": 1048576, "vector_count": 256}"#;
298 let ns: NamespaceStorage = serde_json::from_str(json).unwrap();
299 assert_eq!(ns.bytes, 1048576);
300 assert_eq!(ns.vector_count, 256);
301 }
302}