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 state = super::super::aggregate_upserts(metrics);
317
318 let mut result = Vec::new();
319 state.visit_metrics(|context, value| {
320 result.push((context.name().to_string(), value.clone()));
321 });
322
323 result.sort_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b));
324
325 result
326 }
327
328 fn assert_counter(value: &AggregatedMetricValue, expected: f64) {
329 match value {
330 AggregatedMetricValue::Counter(v) => assert_eq!(*v, expected),
331 other => panic!("expected counter, got {other:?}"),
332 }
333 }
334
335 fn assert_gauge(value: &AggregatedMetricValue, expected: f64) {
336 match value {
337 AggregatedMetricValue::Gauge(v) => assert_eq!(*v, expected),
338 other => panic!("expected gauge, got {other:?}"),
339 }
340 }
341
342 fn assert_histogram<F: FnOnce(&AggregatedHistogram)>(value: &AggregatedMetricValue, check: F) {
343 match value {
344 AggregatedMetricValue::Histogram(h) => check(h),
345 other => panic!("expected histogram, got {other:?}"),
346 }
347 }
348
349 #[test]
350 fn aggregate_multiple() {
351 let input_metrics = vec![
352 Event::Metric(Metric::counter("counter", 14.0)),
353 Event::Metric(Metric::gauge("gauge", 28.0)),
354 ];
355
356 let aggregated_metrics = process_metrics(input_metrics);
357 assert_eq!(aggregated_metrics.len(), 2);
358 assert_eq!(aggregated_metrics[0].0, "counter");
359 assert_counter(&aggregated_metrics[0].1, 14.0);
360 assert_eq!(aggregated_metrics[1].0, "gauge");
361 assert_gauge(&aggregated_metrics[1].1, 28.0);
362 }
363
364 #[test]
365 fn aggregate_counters() {
366 let input_metrics = vec![
367 Event::Metric(Metric::counter("counter", 14.0)),
368 Event::Metric(Metric::counter("counter", [(123456, 22.0)])),
369 Event::Metric(Metric::counter("counter", [(123456, 67.0), (123457, 44.0)])),
370 ];
371
372 let aggregated_metrics = process_metrics(input_metrics);
373 assert_eq!(aggregated_metrics.len(), 1);
374 assert_eq!(aggregated_metrics[0].0, "counter");
375 assert_counter(&aggregated_metrics[0].1, 147.0);
376 }
377
378 #[test]
379 fn aggregate_gauges() {
380 let input_metrics = vec![
381 Event::Metric(Metric::gauge("gauge", 14.0)),
382 Event::Metric(Metric::gauge("gauge", [(123458, 44.0)])),
383 Event::Metric(Metric::gauge("gauge", [(123455, 67.0), (123457, 88.0)])),
384 ];
385
386 let aggregated_metrics = process_metrics(input_metrics);
387 assert_eq!(aggregated_metrics.len(), 1);
388 assert_gauge(&aggregated_metrics[0].1, 44.0);
389 }
390
391 #[test]
392 fn aggregate_gauges_bias_incoming() {
393 let input_metrics = vec![
394 Event::Metric(Metric::gauge("gauge", [(123456, 33.0)])),
395 Event::Metric(Metric::gauge("gauge", [(123456, 66.0)])),
396 ];
397
398 let aggregated_metrics = process_metrics(input_metrics);
399 assert_eq!(aggregated_metrics.len(), 1);
400 assert_gauge(&aggregated_metrics[0].1, 66.0);
401 }
402
403 #[test]
404 fn aggregate_type_change() {
405 let input_metrics = vec![
408 Event::Metric(Metric::gauge("my_metric", 33.0)),
409 Event::Metric(Metric::counter("my_metric", 42.0)),
410 ];
411
412 let aggregated_metrics = process_metrics(input_metrics);
413 assert_eq!(aggregated_metrics.len(), 1);
414 assert_counter(&aggregated_metrics[0].1, 42.0);
415 }
416
417 #[test]
418 fn aggregate_histograms() {
419 let input_metrics = vec![
421 Event::Metric(Metric::histogram("h", [1.0, 2.0, 3.0])),
422 Event::Metric(Metric::histogram("h", [4.0, 5.0])),
423 ];
424
425 let aggregated_metrics = process_metrics(input_metrics);
426 assert_eq!(aggregated_metrics.len(), 1);
427 assert_histogram(&aggregated_metrics[0].1, |hist| {
428 assert_eq!(hist.count(), 5);
429 assert_eq!(hist.sum(), 15.0);
430 });
431 }
432
433 #[test]
434 fn evict_removes_metric() {
435 let processor = AggregatedMetricsProcessor;
436 let state = processor.build_initial_state();
437
438 let context = Context::from_static_parts("counter", &[]);
439 processor.process(
440 MetricsSnapshot {
441 upserts: vec![Event::Metric(Metric::counter(context.clone(), 14.0))],
442 evictions: Vec::new(),
443 },
444 &state,
445 );
446
447 let count_named = |name: &str| {
448 let mut count = 0;
449 state.visit_metrics(|ctx, _| {
450 if ctx.name() == name {
451 count += 1;
452 }
453 });
454 count
455 };
456
457 assert_eq!(count_named("counter"), 1);
459
460 processor.process(
462 MetricsSnapshot {
463 upserts: Vec::new(),
464 evictions: vec![context],
465 },
466 &state,
467 );
468 assert_eq!(count_named("counter"), 0);
469 }
470}