1use std::sync::Arc;
8
9use futures::stream::StreamExt as _;
10use papaya::HashMap;
11use saluki_context::Context;
12use tokio::sync::OnceCell;
13
14use super::histogram::AggregatedHistogram;
15use super::reflector::{Processor, Reflector};
16use super::{MetricsSnapshot, MetricsStream};
17use crate::data_model::event::metric::MetricValues;
18
19#[derive(Clone, Debug)]
21pub enum AggregatedMetricValue {
22 Counter(f64),
24
25 Gauge(f64),
27
28 Histogram(AggregatedHistogram),
30}
31
32impl AggregatedMetricValue {
33 pub fn value(&self) -> f64 {
37 match self {
38 AggregatedMetricValue::Counter(value) => *value,
39 AggregatedMetricValue::Gauge(value) => *value,
40 AggregatedMetricValue::Histogram(_) => 0.0,
41 }
42 }
43
44 pub fn merge(&mut self, incoming: &AggregatedMetricValue) {
49 match (self, incoming) {
50 (Self::Counter(a), Self::Counter(b)) => {
51 *a += *b;
52 }
53 (Self::Histogram(a), Self::Histogram(b)) => {
54 a.merge(b);
55 }
56 (Self::Gauge(a), Self::Gauge(b)) => *a = *b,
57 (existing, incoming) => *existing = incoming.clone(),
59 }
60 }
61}
62
63#[derive(Clone)]
64pub(crate) struct AggregatedMetric {
65 pub(crate) timestamp: Option<u64>,
66 pub(crate) value: AggregatedMetricValue,
67}
68
69impl AggregatedMetric {
70 fn counter(value: f64) -> Self {
71 Self {
72 timestamp: None,
73 value: AggregatedMetricValue::Counter(value),
74 }
75 }
76
77 fn gauge(timestamp: u64, value: f64) -> Self {
78 Self {
79 timestamp: Some(timestamp),
80 value: AggregatedMetricValue::Gauge(value),
81 }
82 }
83
84 fn histogram(histogram: AggregatedHistogram) -> Self {
85 Self {
86 timestamp: None,
87 value: AggregatedMetricValue::Histogram(histogram),
88 }
89 }
90
91 fn merge(&self, other: Self) -> Self {
92 match (&self.value, other.value) {
93 (AggregatedMetricValue::Counter(a), AggregatedMetricValue::Counter(b)) => Self {
94 timestamp: None,
95 value: AggregatedMetricValue::Counter(a + b),
96 },
97 (AggregatedMetricValue::Gauge(a), AggregatedMetricValue::Gauge(b)) => {
98 let ts_a = self.timestamp.unwrap_or(0);
99 let ts_b = other.timestamp.unwrap_or(0);
100 let (new_ts, new_value) = if ts_a > ts_b { (ts_a, *a) } else { (ts_b, b) };
101
102 Self {
103 timestamp: Some(new_ts),
104 value: AggregatedMetricValue::Gauge(new_value),
105 }
106 }
107 (AggregatedMetricValue::Histogram(a), AggregatedMetricValue::Histogram(b)) => {
108 let mut merged = a.clone();
109 merged.merge(&b);
110 Self {
111 timestamp: None,
112 value: AggregatedMetricValue::Histogram(merged),
113 }
114 }
115 (_, other_value) => Self {
116 timestamp: other.timestamp,
117 value: other_value,
118 },
119 }
120 }
121}
122
123struct Inner {
124 metrics: HashMap<Context, AggregatedMetric>,
125}
126
127pub struct AggregatedMetricsState {
129 inner: Arc<Inner>,
130}
131
132impl AggregatedMetricsState {
133 pub fn visit_metrics<F>(&self, mut visitor: F)
135 where
136 F: FnMut(&Context, &AggregatedMetricValue),
137 {
138 self.inner
139 .metrics
140 .pin()
141 .iter()
142 .for_each(|(context, value)| visitor(context, &value.value));
143 }
144
145 pub fn find_single_with_tags(&self, name: &str, tags: &[&str]) -> Option<f64> {
153 let mut had_existing = false;
154 let mut maybe_metric = None;
155
156 self.visit_metrics(|context, value| {
157 if context.name() == name {
158 for tag in tags {
159 if !context.tags().has_tag(tag) {
160 return;
161 }
162 }
163
164 match value {
165 AggregatedMetricValue::Counter(v) | AggregatedMetricValue::Gauge(v) => {
166 had_existing = maybe_metric.is_some();
167 maybe_metric = Some(*v);
168 }
169 AggregatedMetricValue::Histogram(_) => {}
170 }
171 }
172 });
173
174 if had_existing {
175 None
176 } else {
177 maybe_metric
178 }
179 }
180
181 pub fn get_aggregated_with_tags(&self, name: &str, tags: &[&str]) -> f64 {
191 let mut total = 0.0;
192
193 self.visit_metrics(|context, value| {
194 if context.name() == name {
195 for tag in tags {
196 if !context.tags().has_tag(tag) {
197 return;
198 }
199 }
200
201 if let AggregatedMetricValue::Counter(value) = value {
202 total += *value;
203 }
204 }
205 });
206
207 total
208 }
209}
210
211#[derive(Clone)]
226pub struct AggregatedMetricsProcessor;
227
228impl Processor for AggregatedMetricsProcessor {
229 type Input = MetricsSnapshot;
230 type State = AggregatedMetricsState;
231
232 fn build_initial_state(&self) -> Self::State {
233 AggregatedMetricsState {
234 inner: Arc::new(Inner {
235 metrics: HashMap::new(),
236 }),
237 }
238 }
239
240 fn process(&self, input: Self::Input, state: &Self::State) {
241 let metrics = state.inner.metrics.pin();
242
243 for event in input.upserts {
245 if let Some(metric) = event.try_into_metric() {
246 let (context, values, _) = metric.into_parts();
247 if let Some(agg_metric) = metric_values_to_aggregated(context.name(), values) {
248 metrics.update_or_insert_with(
249 context,
250 |existing| existing.merge(agg_metric.clone()),
251 || agg_metric.clone(),
252 );
253 }
254 }
255 }
256
257 for context in input.evictions {
260 metrics.remove(&context);
261 }
262 }
263}
264
265fn metric_values_to_aggregated(metric_name: &str, values: MetricValues) -> Option<AggregatedMetric> {
266 match values {
267 MetricValues::Counter(points) => {
268 let value = points.into_iter().map(|(_, value)| value).sum();
270 Some(AggregatedMetric::counter(value))
271 }
272 MetricValues::Gauge(points) => {
273 points
275 .into_iter()
276 .last()
277 .map(|(ts, value)| AggregatedMetric::gauge(ts.map(|ts| ts.get()).unwrap_or(0), value))
278 }
279 MetricValues::Histogram(points) => {
280 let mut aggregated = AggregatedHistogram::new(metric_name);
281 for (_, histogram) in points {
282 aggregated.merge_histogram(&histogram);
283 }
284 if aggregated.count() == 0 {
285 None
286 } else {
287 Some(AggregatedMetric::histogram(aggregated))
288 }
289 }
290 _ => None,
291 }
292}
293
294pub async fn get_shared_metrics_state() -> Reflector<AggregatedMetricsProcessor> {
298 static REFLECTOR: OnceCell<Reflector<AggregatedMetricsProcessor>> = OnceCell::const_new();
299 REFLECTOR
300 .get_or_init(|| async {
301 let metrics_stream = MetricsStream::register().map(Arc::unwrap_or_clone).map(std::iter::once);
302 Reflector::new(metrics_stream, AggregatedMetricsProcessor).await
303 })
304 .await
305 .clone()
306}
307
308#[cfg(test)]
309mod tests {
310 use saluki_context::Context;
311
312 use super::*;
313 use crate::data_model::event::{metric::Metric, Event};
314
315 fn process_metrics(metrics: Vec<Event>) -> Vec<(String, AggregatedMetricValue)> {
316 let processor = AggregatedMetricsProcessor;
317 let state = processor.build_initial_state();
318
319 processor.process(
320 MetricsSnapshot {
321 upserts: metrics,
322 evictions: Vec::new(),
323 },
324 &state,
325 );
326
327 let mut result = Vec::new();
328 state.visit_metrics(|context, value| {
329 result.push((context.name().to_string(), value.clone()));
330 });
331
332 result.sort_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b));
333
334 result
335 }
336
337 fn assert_counter(value: &AggregatedMetricValue, expected: f64) {
338 match value {
339 AggregatedMetricValue::Counter(v) => assert_eq!(*v, expected),
340 other => panic!("expected counter, got {other:?}"),
341 }
342 }
343
344 fn assert_gauge(value: &AggregatedMetricValue, expected: f64) {
345 match value {
346 AggregatedMetricValue::Gauge(v) => assert_eq!(*v, expected),
347 other => panic!("expected gauge, got {other:?}"),
348 }
349 }
350
351 fn assert_histogram<F: FnOnce(&AggregatedHistogram)>(value: &AggregatedMetricValue, check: F) {
352 match value {
353 AggregatedMetricValue::Histogram(h) => check(h),
354 other => panic!("expected histogram, got {other:?}"),
355 }
356 }
357
358 #[test]
359 fn test_aggregate_multiple() {
360 let input_metrics = vec![
361 Event::Metric(Metric::counter("counter", 14.0)),
362 Event::Metric(Metric::gauge("gauge", 28.0)),
363 ];
364
365 let aggregated_metrics = process_metrics(input_metrics);
366 assert_eq!(aggregated_metrics.len(), 2);
367 assert_eq!(aggregated_metrics[0].0, "counter");
368 assert_counter(&aggregated_metrics[0].1, 14.0);
369 assert_eq!(aggregated_metrics[1].0, "gauge");
370 assert_gauge(&aggregated_metrics[1].1, 28.0);
371 }
372
373 #[test]
374 fn test_aggregate_counters() {
375 let input_metrics = vec![
376 Event::Metric(Metric::counter("counter", 14.0)),
377 Event::Metric(Metric::counter("counter", [(123456, 22.0)])),
378 Event::Metric(Metric::counter("counter", [(123456, 67.0), (123457, 44.0)])),
379 ];
380
381 let aggregated_metrics = process_metrics(input_metrics);
382 assert_eq!(aggregated_metrics.len(), 1);
383 assert_eq!(aggregated_metrics[0].0, "counter");
384 assert_counter(&aggregated_metrics[0].1, 147.0);
385 }
386
387 #[test]
388 fn test_aggregate_gauges() {
389 let input_metrics = vec![
390 Event::Metric(Metric::gauge("gauge", 14.0)),
391 Event::Metric(Metric::gauge("gauge", [(123458, 44.0)])),
392 Event::Metric(Metric::gauge("gauge", [(123455, 67.0), (123457, 88.0)])),
393 ];
394
395 let aggregated_metrics = process_metrics(input_metrics);
396 assert_eq!(aggregated_metrics.len(), 1);
397 assert_gauge(&aggregated_metrics[0].1, 44.0);
398 }
399
400 #[test]
401 fn test_aggregate_gauges_bias_incoming() {
402 let input_metrics = vec![
403 Event::Metric(Metric::gauge("gauge", [(123456, 33.0)])),
404 Event::Metric(Metric::gauge("gauge", [(123456, 66.0)])),
405 ];
406
407 let aggregated_metrics = process_metrics(input_metrics);
408 assert_eq!(aggregated_metrics.len(), 1);
409 assert_gauge(&aggregated_metrics[0].1, 66.0);
410 }
411
412 #[test]
413 fn test_aggregate_type_change() {
414 let input_metrics = vec![
417 Event::Metric(Metric::gauge("my_metric", 33.0)),
418 Event::Metric(Metric::counter("my_metric", 42.0)),
419 ];
420
421 let aggregated_metrics = process_metrics(input_metrics);
422 assert_eq!(aggregated_metrics.len(), 1);
423 assert_counter(&aggregated_metrics[0].1, 42.0);
424 }
425
426 #[test]
427 fn test_aggregate_histograms() {
428 let input_metrics = vec![
430 Event::Metric(Metric::histogram("h", [1.0, 2.0, 3.0])),
431 Event::Metric(Metric::histogram("h", [4.0, 5.0])),
432 ];
433
434 let aggregated_metrics = process_metrics(input_metrics);
435 assert_eq!(aggregated_metrics.len(), 1);
436 assert_histogram(&aggregated_metrics[0].1, |hist| {
437 assert_eq!(hist.count(), 5);
438 assert_eq!(hist.sum(), 15.0);
439 });
440 }
441
442 #[test]
443 fn test_evict_removes_metric() {
444 let processor = AggregatedMetricsProcessor;
445 let state = processor.build_initial_state();
446
447 let context = Context::from_static_parts("counter", &[]);
448 processor.process(
449 MetricsSnapshot {
450 upserts: vec![Event::Metric(Metric::counter(context.clone(), 14.0))],
451 evictions: Vec::new(),
452 },
453 &state,
454 );
455
456 let count_named = |name: &str| {
457 let mut count = 0;
458 state.visit_metrics(|ctx, _| {
459 if ctx.name() == name {
460 count += 1;
461 }
462 });
463 count
464 };
465
466 assert_eq!(count_named("counter"), 1);
468
469 processor.process(
471 MetricsSnapshot {
472 upserts: Vec::new(),
473 evictions: vec![context],
474 },
475 &state,
476 );
477 assert_eq!(count_named("counter"), 0);
478 }
479}