1use anyhow::anyhow;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug)]
34pub struct CompositeAggregator {
35 aggregations: Vec<AggregationSpec>,
36}
37
38#[derive(Debug, Clone)]
39enum AggregationSpec {
40 Count {
41 name: String,
42 count: u64,
43 },
44 Sum {
45 name: String,
46 sum: f64,
47 count: u64,
48 },
49 Avg {
50 name: String,
51 sum: f64,
52 count: u64,
53 },
54 Min {
55 name: String,
56 min: Option<f64>,
57 count: u64,
58 },
59 Max {
60 name: String,
61 max: Option<f64>,
62 count: u64,
63 },
64 Percentile {
65 name: String,
66 values: Vec<f64>,
67 percentile: f64,
68 },
69 StdDev {
70 name: String,
71 count: u64,
72 mean: f64,
73 m2: f64,
74 },
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct AggregationResult {
80 counts: HashMap<String, u64>,
81 values: HashMap<String, f64>,
82}
83
84impl AggregationResult {
85 pub fn new() -> Self {
86 Self {
87 counts: HashMap::new(),
88 values: HashMap::new(),
89 }
90 }
91
92 pub fn set_count(&mut self, name: impl Into<String>, count: u64) {
94 self.counts.insert(name.into(), count);
95 }
96
97 pub fn set_value(&mut self, name: impl Into<String>, value: f64) {
99 self.values.insert(name.into(), value);
100 }
101
102 pub fn get_count(&self, name: &str) -> Option<u64> {
104 self.counts.get(name).copied()
105 }
106
107 pub fn get_value(&self, name: &str) -> Option<f64> {
109 self.values.get(name).copied()
110 }
111
112 pub fn counts(&self) -> &HashMap<String, u64> {
114 &self.counts
115 }
116
117 pub fn values(&self) -> &HashMap<String, f64> {
119 &self.values
120 }
121}
122
123impl CompositeAggregator {
124 pub fn new() -> Self {
126 Self {
127 aggregations: Vec::new(),
128 }
129 }
130
131 pub fn add_count(&mut self, name: impl Into<String>) -> &mut Self {
133 self.aggregations.push(AggregationSpec::Count {
134 name: name.into(),
135 count: 0,
136 });
137 self
138 }
139
140 pub fn add_sum(&mut self, name: impl Into<String>) -> &mut Self {
142 self.aggregations.push(AggregationSpec::Sum {
143 name: name.into(),
144 sum: 0.0,
145 count: 0,
146 });
147 self
148 }
149
150 pub fn add_avg(&mut self, name: impl Into<String>) -> &mut Self {
152 self.aggregations.push(AggregationSpec::Avg {
153 name: name.into(),
154 sum: 0.0,
155 count: 0,
156 });
157 self
158 }
159
160 pub fn add_min(&mut self, name: impl Into<String>) -> &mut Self {
162 self.aggregations.push(AggregationSpec::Min {
163 name: name.into(),
164 min: None,
165 count: 0,
166 });
167 self
168 }
169
170 pub fn add_max(&mut self, name: impl Into<String>) -> &mut Self {
172 self.aggregations.push(AggregationSpec::Max {
173 name: name.into(),
174 max: None,
175 count: 0,
176 });
177 self
178 }
179
180 pub fn add_percentile(&mut self, name: impl Into<String>, percentile: f64) -> anyhow::Result<&mut Self> {
182 if !(0.0..=100.0).contains(&percentile) {
183 return Err(anyhow!("Percentile must be between 0 and 100"));
184 }
185 self.aggregations.push(AggregationSpec::Percentile {
186 name: name.into(),
187 values: Vec::new(),
188 percentile,
189 });
190 Ok(self)
191 }
192
193 pub fn add_p50(&mut self, name: impl Into<String>) -> &mut Self {
195 self.aggregations.push(AggregationSpec::Percentile {
196 name: name.into(),
197 values: Vec::new(),
198 percentile: 50.0,
199 });
200 self
201 }
202
203 pub fn add_p95(&mut self, name: impl Into<String>) -> &mut Self {
205 self.aggregations.push(AggregationSpec::Percentile {
206 name: name.into(),
207 values: Vec::new(),
208 percentile: 95.0,
209 });
210 self
211 }
212
213 pub fn add_p99(&mut self, name: impl Into<String>) -> &mut Self {
215 self.aggregations.push(AggregationSpec::Percentile {
216 name: name.into(),
217 values: Vec::new(),
218 percentile: 99.0,
219 });
220 self
221 }
222
223 pub fn add_stddev(&mut self, name: impl Into<String>) -> &mut Self {
225 self.aggregations.push(AggregationSpec::StdDev {
226 name: name.into(),
227 count: 0,
228 mean: 0.0,
229 m2: 0.0,
230 });
231 self
232 }
233
234 pub fn update(&mut self, value: f64) -> anyhow::Result<()> {
236 for agg in &mut self.aggregations {
237 match agg {
238 AggregationSpec::Count { count, .. } => {
239 *count += 1;
240 }
241 AggregationSpec::Sum { sum, count, .. } => {
242 *sum += value;
243 *count += 1;
244 }
245 AggregationSpec::Avg { sum, count, .. } => {
246 *sum += value;
247 *count += 1;
248 }
249 AggregationSpec::Min { min, count, .. } => {
250 *min = Some(match *min {
251 Some(current) => current.min(value),
252 None => value,
253 });
254 *count += 1;
255 }
256 AggregationSpec::Max { max, count, .. } => {
257 *max = Some(match *max {
258 Some(current) => current.max(value),
259 None => value,
260 });
261 *count += 1;
262 }
263 AggregationSpec::Percentile { values, .. } => {
264 values.push(value);
265 }
266 AggregationSpec::StdDev { count, mean, m2, .. } => {
267 *count += 1;
268 let delta = value - *mean;
269 *mean += delta / *count as f64;
270 let delta2 = value - *mean;
271 *m2 += delta * delta2;
272 }
273 }
274 }
275 Ok(())
276 }
277
278 pub fn update_batch(&mut self, values: &[f64]) -> anyhow::Result<()> {
280 for value in values {
281 self.update(*value)?;
282 }
283 Ok(())
284 }
285
286 pub fn finalize(&self) -> anyhow::Result<AggregationResult> {
288 let mut result = AggregationResult::new();
289
290 for agg in &self.aggregations {
291 match agg {
292 AggregationSpec::Count { name, count } => {
293 result.counts.insert(name.clone(), *count);
294 }
295 AggregationSpec::Sum { name, sum, .. } => {
296 result.values.insert(name.clone(), *sum);
297 }
298 AggregationSpec::Avg { name, sum, count } => {
299 if *count == 0 {
300 return Err(anyhow!("Cannot compute average '{}' of zero values", name));
301 }
302 result.values.insert(name.clone(), *sum / *count as f64);
303 }
304 AggregationSpec::Min { name, min, .. } => {
305 let value = min.ok_or_else(|| anyhow!("Cannot compute min '{}' of zero values", name))?;
306 result.values.insert(name.clone(), value);
307 }
308 AggregationSpec::Max { name, max, .. } => {
309 let value = max.ok_or_else(|| anyhow!("Cannot compute max '{}' of zero values", name))?;
310 result.values.insert(name.clone(), value);
311 }
312 AggregationSpec::Percentile { name, values, percentile } => {
313 if values.is_empty() {
314 return Err(anyhow!("Cannot compute percentile '{}' of zero values", name));
315 }
316 let mut sorted = values.clone();
317 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
318 let value = calculate_percentile(&sorted, *percentile)
319 .ok_or_else(|| anyhow!("Failed to calculate percentile '{}'", name))?;
320 result.values.insert(name.clone(), value);
321 }
322 AggregationSpec::StdDev { name, count, m2, .. } => {
323 if *count < 2 {
324 return Err(anyhow!("Cannot compute stddev '{}' with less than 2 values", name));
325 }
326 let variance = *m2 / (*count - 1) as f64;
327 result.values.insert(name.clone(), variance.sqrt());
328 }
329 }
330 }
331
332 Ok(result)
333 }
334
335 pub fn reset(&mut self) {
337 for agg in &mut self.aggregations {
338 match agg {
339 AggregationSpec::Count { count, .. } => {
340 *count = 0;
341 }
342 AggregationSpec::Sum { sum, count, .. } => {
343 *sum = 0.0;
344 *count = 0;
345 }
346 AggregationSpec::Avg { sum, count, .. } => {
347 *sum = 0.0;
348 *count = 0;
349 }
350 AggregationSpec::Min { min, count, .. } => {
351 *min = None;
352 *count = 0;
353 }
354 AggregationSpec::Max { max, count, .. } => {
355 *max = None;
356 *count = 0;
357 }
358 AggregationSpec::Percentile { values, .. } => {
359 values.clear();
360 }
361 AggregationSpec::StdDev { count, mean, m2, .. } => {
362 *count = 0;
363 *mean = 0.0;
364 *m2 = 0.0;
365 }
366 }
367 }
368 }
369
370 pub fn len(&self) -> usize {
372 self.aggregations.len()
373 }
374
375 pub fn is_empty(&self) -> bool {
377 self.aggregations.is_empty()
378 }
379}
380
381impl Default for CompositeAggregator {
382 fn default() -> Self {
383 Self::new()
384 }
385}
386
387fn calculate_percentile(sorted_values: &[f64], percentile: f64) -> Option<f64> {
388 if sorted_values.is_empty() {
389 return None;
390 }
391
392 if sorted_values.len() == 1 {
393 return Some(sorted_values[0]);
394 }
395
396 let rank = (percentile / 100.0) * (sorted_values.len() - 1) as f64;
397 let lower_idx = rank.floor() as usize;
398 let upper_idx = rank.ceil() as usize;
399
400 if lower_idx == upper_idx {
401 Some(sorted_values[lower_idx])
402 } else {
403 let fraction = rank - lower_idx as f64;
404 Some(sorted_values[lower_idx] * (1.0 - fraction)
405 + sorted_values[upper_idx] * fraction)
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_composite_basic() {
415 let mut composite = CompositeAggregator::new();
416 composite
417 .add_count("count")
418 .add_sum("sum")
419 .add_avg("avg")
420 .add_min("min")
421 .add_max("max");
422
423 composite.update(10.0).unwrap();
424 composite.update(20.0).unwrap();
425 composite.update(30.0).unwrap();
426
427 let results = composite.finalize().unwrap();
428
429 assert_eq!(results.get_count("count").unwrap(), 3);
430 assert_eq!(results.get_value("sum").unwrap(), 60.0);
431 assert_eq!(results.get_value("avg").unwrap(), 20.0);
432 assert_eq!(results.get_value("min").unwrap(), 10.0);
433 assert_eq!(results.get_value("max").unwrap(), 30.0);
434 }
435
436 #[test]
437 fn test_composite_percentiles() {
438 let mut composite = CompositeAggregator::new();
439 composite
440 .add_p50("p50")
441 .add_p95("p95")
442 .add_p99("p99");
443
444 for i in 1..=100 {
445 composite.update(i as f64).unwrap();
446 }
447
448 let results = composite.finalize().unwrap();
449
450 assert!((results.get_value("p50").unwrap() - 50.5).abs() < 0.1);
451 assert!((results.get_value("p95").unwrap() - 95.05).abs() < 0.1);
452 assert!((results.get_value("p99").unwrap() - 99.01).abs() < 0.1);
453 }
454
455 #[test]
456 fn test_composite_stddev() {
457 let mut composite = CompositeAggregator::new();
458 composite
459 .add_avg("avg")
460 .add_stddev("stddev");
461
462 composite.update(10.0).unwrap();
463 composite.update(12.0).unwrap();
464 composite.update(14.0).unwrap();
465 composite.update(16.0).unwrap();
466 composite.update(18.0).unwrap();
467
468 let results = composite.finalize().unwrap();
469
470 assert!((results.get_value("avg").unwrap() - 14.0).abs() < 0.01);
471 assert!((results.get_value("stddev").unwrap() - 3.162).abs() < 0.01);
472 }
473
474 #[test]
475 fn test_composite_batch() {
476 let mut composite = CompositeAggregator::new();
477 composite
478 .add_count("count")
479 .add_avg("avg");
480
481 composite.update_batch(&[10.0, 20.0, 30.0, 40.0, 50.0]).unwrap();
482
483 let results = composite.finalize().unwrap();
484
485 assert_eq!(results.get_count("count").unwrap(), 5);
486 assert_eq!(results.get_value("avg").unwrap(), 30.0);
487 }
488
489 #[test]
490 fn test_composite_reset() {
491 let mut composite = CompositeAggregator::new();
492 composite.add_count("count").add_sum("sum");
493
494 composite.update(10.0).unwrap();
495 composite.update(20.0).unwrap();
496
497 composite.reset();
498
499 composite.update(5.0).unwrap();
500
501 let results = composite.finalize().unwrap();
502
503 assert_eq!(results.get_count("count").unwrap(), 1);
504 assert_eq!(results.get_value("sum").unwrap(), 5.0);
505 }
506
507 #[test]
508 fn test_composite_empty() {
509 let composite = CompositeAggregator::new();
510 assert!(composite.is_empty());
511 assert_eq!(composite.len(), 0);
512 }
513
514 #[test]
515 fn test_composite_all_aggregations() {
516 let mut composite = CompositeAggregator::new();
517 composite
518 .add_count("count")
519 .add_sum("sum")
520 .add_avg("avg")
521 .add_min("min")
522 .add_max("max")
523 .add_p50("p50")
524 .add_p95("p95")
525 .add_stddev("stddev");
526
527 for i in 1..=100 {
528 composite.update(i as f64).unwrap();
529 }
530
531 let results = composite.finalize().unwrap();
532
533 assert_eq!(results.get_count("count").unwrap(), 100);
534 assert_eq!(results.get_value("sum").unwrap(), 5050.0);
535 assert_eq!(results.get_value("avg").unwrap(), 50.5);
536 assert_eq!(results.get_value("min").unwrap(), 1.0);
537 assert_eq!(results.get_value("max").unwrap(), 100.0);
538 assert!((results.get_value("p50").unwrap() - 50.5).abs() < 0.1);
539 assert!((results.get_value("p95").unwrap() - 95.05).abs() < 0.1);
540 assert!((results.get_value("stddev").unwrap() - 29.01).abs() < 0.5);
541 }
542
543 #[test]
544 fn test_composite_serialization() {
545 let mut composite = CompositeAggregator::new();
546 composite.add_count("count").add_avg("avg");
547
548 composite.update(10.0).unwrap();
549 composite.update(20.0).unwrap();
550
551 let results = composite.finalize().unwrap();
552 let serialized = serde_json::to_string(&results).unwrap();
553 let deserialized: AggregationResult = serde_json::from_str(&serialized).unwrap();
554
555 assert_eq!(deserialized.get_count("count").unwrap(), 2);
556 assert_eq!(deserialized.get_value("avg").unwrap(), 15.0);
557 }
558
559 #[test]
560 fn test_composite_custom_percentile() {
561 let mut composite = CompositeAggregator::new();
562 composite.add_percentile("p75", 75.0).unwrap();
563
564 for i in 1..=100 {
565 composite.update(i as f64).unwrap();
566 }
567
568 let results = composite.finalize().unwrap();
569 assert!((results.get_value("p75").unwrap() - 75.25).abs() < 0.5);
570 }
571
572 #[test]
573 fn test_composite_invalid_percentile() {
574 let mut composite = CompositeAggregator::new();
575 assert!(composite.add_percentile("invalid", 150.0).is_err());
576 }
577
578 #[test]
579 fn test_real_world_latency_tracking() {
580 let mut composite = CompositeAggregator::new();
582 composite
583 .add_count("request_count")
584 .add_avg("avg_latency_ms")
585 .add_min("min_latency_ms")
586 .add_max("max_latency_ms")
587 .add_p50("p50_latency_ms")
588 .add_p95("p95_latency_ms")
589 .add_p99("p99_latency_ms")
590 .add_stddev("stddev_latency_ms");
591
592 let latencies = vec![
594 45.0, 52.0, 48.0, 51.0, 49.0, 150.0, 200.0, 46.0, 50.0, 47.0, 500.0, ];
599
600 for latency in latencies {
601 composite.update(latency).unwrap();
602 }
603
604 let results = composite.finalize().unwrap();
605
606 println!("Latency Statistics:");
607 println!(" Total Requests: {}", results.get_count("request_count").unwrap());
608 println!(" Avg: {:.2}ms", results.get_value("avg_latency_ms").unwrap());
609 println!(" Min: {:.2}ms", results.get_value("min_latency_ms").unwrap());
610 println!(" Max: {:.2}ms", results.get_value("max_latency_ms").unwrap());
611 println!(" P50: {:.2}ms", results.get_value("p50_latency_ms").unwrap());
612 println!(" P95: {:.2}ms", results.get_value("p95_latency_ms").unwrap());
613 println!(" P99: {:.2}ms", results.get_value("p99_latency_ms").unwrap());
614 println!(" StdDev: {:.2}ms", results.get_value("stddev_latency_ms").unwrap());
615
616 assert_eq!(results.get_count("request_count").unwrap(), 11);
617 assert!(results.get_value("avg_latency_ms").unwrap() > 50.0);
618 assert_eq!(results.get_value("min_latency_ms").unwrap(), 45.0);
619 assert_eq!(results.get_value("max_latency_ms").unwrap(), 500.0);
620 }
621}