Skip to main content

dakera_client/
analytics.rs

1//! Analytics operations for the Dakera client.
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Result;
6use crate::DakeraClient;
7
8// ============================================================================
9// Analytics Types
10// ============================================================================
11
12/// Analytics overview response
13#[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/// Latency analytics response
29#[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/// Per-operation latency stats
42#[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/// Throughput analytics response
50#[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/// Storage analytics response
60#[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/// Per-namespace storage stats
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct NamespaceStorage {
72    pub bytes: u64,
73    pub vector_count: u64,
74}
75
76// ============================================================================
77// Analytics Client Methods
78// ============================================================================
79
80impl DakeraClient {
81    /// Get analytics overview
82    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(&params.join("&"));
98        }
99        let response = self.client.get(&url).send().await?;
100        self.handle_response(response).await
101    }
102
103    /// Get latency analytics
104    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(&params.join("&"));
120        }
121        let response = self.client.get(&url).send().await?;
122        self.handle_response(response).await
123    }
124
125    /// Get throughput analytics
126    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(&params.join("&"));
142        }
143        let response = self.client.get(&url).send().await?;
144        self.handle_response(response).await
145    }
146
147    /// Get storage analytics
148    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// ============================================================================
159// Tests
160// ============================================================================
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    // -------------------------------------------------------------------------
167    // AnalyticsOverview
168    // -------------------------------------------------------------------------
169
170    #[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    // -------------------------------------------------------------------------
193    // LatencyAnalytics
194    // -------------------------------------------------------------------------
195
196    #[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    // -------------------------------------------------------------------------
232    // OperationLatency
233    // -------------------------------------------------------------------------
234
235    #[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    // -------------------------------------------------------------------------
244    // ThroughputAnalytics
245    // -------------------------------------------------------------------------
246
247    #[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    // -------------------------------------------------------------------------
260    // StorageAnalytics
261    // -------------------------------------------------------------------------
262
263    #[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    // -------------------------------------------------------------------------
292    // NamespaceStorage
293    // -------------------------------------------------------------------------
294
295    #[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}