1use std::{
8 iter::Once,
9 pin::Pin,
10 sync::{Arc, OnceLock},
11};
12
13use futures::{stream::StreamExt as _, Stream};
14use papaya::HashMap;
15use saluki_error::{generic_error, GenericError};
16
17use super::histogram::AggregatedHistogram;
18use super::reflector::{Processor, Reflector, ReflectorWorker};
19use super::{MetricsSnapshot, MetricsStream};
20use crate::data_model::event::metric::{context::Context, MetricValues};
21
22#[derive(Clone, Debug)]
24pub enum AggregatedMetricValue {
25 Counter(f64),
27
28 Gauge(f64),
30
31 Histogram(AggregatedHistogram),
33}
34
35impl AggregatedMetricValue {
36 pub fn value(&self) -> f64 {
40 match self {
41 AggregatedMetricValue::Counter(value) => *value,
42 AggregatedMetricValue::Gauge(value) => *value,
43 AggregatedMetricValue::Histogram(_) => 0.0,
44 }
45 }
46
47 pub fn merge(&mut self, incoming: &AggregatedMetricValue) {
52 match (self, incoming) {
53 (Self::Counter(a), Self::Counter(b)) => {
54 *a += *b;
55 }
56 (Self::Histogram(a), Self::Histogram(b)) => {
57 a.merge(b);
58 }
59 (Self::Gauge(a), Self::Gauge(b)) => *a = *b,
60 (existing, incoming) => *existing = incoming.clone(),
62 }
63 }
64}
65
66#[derive(Clone)]
67pub(crate) struct AggregatedMetric {
68 pub(crate) timestamp: Option<u64>,
69 pub(crate) value: AggregatedMetricValue,
70}
71
72impl AggregatedMetric {
73 fn counter(value: f64) -> Self {
74 Self {
75 timestamp: None,
76 value: AggregatedMetricValue::Counter(value),
77 }
78 }
79
80 fn gauge(timestamp: u64, value: f64) -> Self {
81 Self {
82 timestamp: Some(timestamp),
83 value: AggregatedMetricValue::Gauge(value),
84 }
85 }
86
87 fn histogram(histogram: AggregatedHistogram) -> Self {
88 Self {
89 timestamp: None,
90 value: AggregatedMetricValue::Histogram(histogram),
91 }
92 }
93
94 fn merge(&self, other: Self) -> Self {
95 match (&self.value, other.value) {
96 (AggregatedMetricValue::Counter(a), AggregatedMetricValue::Counter(b)) => Self {
97 timestamp: None,
98 value: AggregatedMetricValue::Counter(a + b),
99 },
100 (AggregatedMetricValue::Gauge(a), AggregatedMetricValue::Gauge(b)) => {
101 let ts_a = self.timestamp.unwrap_or(0);
102 let ts_b = other.timestamp.unwrap_or(0);
103 let (new_ts, new_value) = if ts_a > ts_b { (ts_a, *a) } else { (ts_b, b) };
104
105 Self {
106 timestamp: Some(new_ts),
107 value: AggregatedMetricValue::Gauge(new_value),
108 }
109 }
110 (AggregatedMetricValue::Histogram(a), AggregatedMetricValue::Histogram(b)) => {
111 let mut merged = a.clone();
112 merged.merge(&b);
113 Self {
114 timestamp: None,
115 value: AggregatedMetricValue::Histogram(merged),
116 }
117 }
118 (_, other_value) => Self {
119 timestamp: other.timestamp,
120 value: other_value,
121 },
122 }
123 }
124}
125
126struct Inner {
127 metrics: HashMap<Context, AggregatedMetric>,
128}
129
130pub struct AggregatedMetricsState {
132 inner: Arc<Inner>,
133}
134
135impl AggregatedMetricsState {
136 pub fn visit_metrics<F>(&self, mut visitor: F)
138 where
139 F: FnMut(&Context, &AggregatedMetricValue),
140 {
141 self.inner
142 .metrics
143 .pin()
144 .iter()
145 .for_each(|(context, value)| visitor(context, &value.value));
146 }
147
148 pub fn find_single_with_tags(&self, name: &str, tags: &[&str]) -> Option<f64> {
156 let mut had_existing = false;
157 let mut maybe_metric = None;
158
159 self.visit_metrics(|context, value| {
160 if context.name() == name {
161 for tag in tags {
162 if !context.tags().has_tag(tag) {
163 return;
164 }
165 }
166
167 match value {
168 AggregatedMetricValue::Counter(v) | AggregatedMetricValue::Gauge(v) => {
169 had_existing = maybe_metric.is_some();
170 maybe_metric = Some(*v);
171 }
172 AggregatedMetricValue::Histogram(_) => {}
173 }
174 }
175 });
176
177 if had_existing {
178 None
179 } else {
180 maybe_metric
181 }
182 }
183
184 pub fn get_aggregated_with_tags(&self, name: &str, tags: &[&str]) -> f64 {
194 let mut total = 0.0;
195
196 self.visit_metrics(|context, value| {
197 if context.name() == name {
198 for tag in tags {
199 if !context.tags().has_tag(tag) {
200 return;
201 }
202 }
203
204 if let AggregatedMetricValue::Counter(value) = value {
205 total += *value;
206 }
207 }
208 });
209
210 total
211 }
212}
213
214#[derive(Clone)]
229pub struct AggregatedMetricsProcessor;
230
231impl Processor for AggregatedMetricsProcessor {
232 type Input = MetricsSnapshot;
233 type State = AggregatedMetricsState;
234
235 fn build_initial_state(&self) -> Self::State {
236 AggregatedMetricsState {
237 inner: Arc::new(Inner {
238 metrics: HashMap::new(),
239 }),
240 }
241 }
242
243 fn process(&self, input: Self::Input, state: &Self::State) {
244 let metrics = state.inner.metrics.pin();
245
246 for event in input.upserts {
248 if let Some(metric) = event.try_into_metric() {
249 let (context, values, _) = metric.into_parts();
250 if let Some(agg_metric) = metric_values_to_aggregated(context.name(), values) {
251 metrics.update_or_insert_with(
252 context,
253 |existing| existing.merge(agg_metric.clone()),
254 || agg_metric.clone(),
255 );
256 }
257 }
258 }
259
260 for context in input.evictions {
263 metrics.remove(&context);
264 }
265 }
266}
267
268fn metric_values_to_aggregated(metric_name: &str, values: MetricValues) -> Option<AggregatedMetric> {
269 match values {
270 MetricValues::Counter(points) => {
271 let value = points.into_iter().map(|(_, value)| value).sum();
273 Some(AggregatedMetric::counter(value))
274 }
275 MetricValues::Gauge(points) => {
276 points
278 .into_iter()
279 .last()
280 .map(|(ts, value)| AggregatedMetric::gauge(ts.map(|ts| ts.get()).unwrap_or(0), value))
281 }
282 MetricValues::Histogram(points) => {
283 let mut aggregated = AggregatedHistogram::new(metric_name);
284 for (_, histogram) in points {
285 aggregated.merge_histogram(&histogram);
286 }
287 if aggregated.count() == 0 {
288 None
289 } else {
290 Some(AggregatedMetric::histogram(aggregated))
291 }
292 }
293 _ => None,
294 }
295}
296
297type SharedMetricsSource = Pin<Box<dyn Stream<Item = Once<MetricsSnapshot>> + Send>>;
302
303pub type SharedMetricsWorker = ReflectorWorker<AggregatedMetricsProcessor, SharedMetricsSource>;
305
306static SHARED_METRICS_STATE: OnceLock<Reflector<AggregatedMetricsProcessor>> = OnceLock::new();
307
308pub fn initialize_shared_metrics_state() -> Result<SharedMetricsWorker, GenericError> {
322 let source: SharedMetricsSource =
323 Box::pin(MetricsStream::register().map(Arc::unwrap_or_clone).map(std::iter::once));
324 let (reflector, worker) = Reflector::new(source, AggregatedMetricsProcessor);
325
326 SHARED_METRICS_STATE
327 .set(reflector)
328 .map_err(|_| generic_error!("Shared metrics state was already initialized."))?;
329
330 Ok(worker)
331}
332
333pub fn get_shared_metrics_state() -> Reflector<AggregatedMetricsProcessor> {
339 SHARED_METRICS_STATE
340 .get()
341 .expect("shared metrics state should be initialized before it is accessed")
342 .clone()
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use crate::data_model::event::{
349 metric::{context::Context, Metric},
350 Event,
351 };
352
353 fn process_metrics(metrics: Vec<Event>) -> Vec<(String, AggregatedMetricValue)> {
354 let state = super::super::aggregate_upserts(metrics);
355
356 let mut result = Vec::new();
357 state.visit_metrics(|context, value| {
358 result.push((context.name().to_string(), value.clone()));
359 });
360
361 result.sort_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b));
362
363 result
364 }
365
366 fn assert_counter(value: &AggregatedMetricValue, expected: f64) {
367 match value {
368 AggregatedMetricValue::Counter(v) => assert_eq!(*v, expected),
369 other => panic!("expected counter, got {other:?}"),
370 }
371 }
372
373 fn assert_gauge(value: &AggregatedMetricValue, expected: f64) {
374 match value {
375 AggregatedMetricValue::Gauge(v) => assert_eq!(*v, expected),
376 other => panic!("expected gauge, got {other:?}"),
377 }
378 }
379
380 fn assert_histogram<F: FnOnce(&AggregatedHistogram)>(value: &AggregatedMetricValue, check: F) {
381 match value {
382 AggregatedMetricValue::Histogram(h) => check(h),
383 other => panic!("expected histogram, got {other:?}"),
384 }
385 }
386
387 #[test]
388 fn aggregate_multiple() {
389 let input_metrics = vec![
390 Event::Metric(Metric::counter("counter", 14.0)),
391 Event::Metric(Metric::gauge("gauge", 28.0)),
392 ];
393
394 let aggregated_metrics = process_metrics(input_metrics);
395 assert_eq!(aggregated_metrics.len(), 2);
396 assert_eq!(aggregated_metrics[0].0, "counter");
397 assert_counter(&aggregated_metrics[0].1, 14.0);
398 assert_eq!(aggregated_metrics[1].0, "gauge");
399 assert_gauge(&aggregated_metrics[1].1, 28.0);
400 }
401
402 #[test]
403 fn aggregate_counters() {
404 let input_metrics = vec![
405 Event::Metric(Metric::counter("counter", 14.0)),
406 Event::Metric(Metric::counter("counter", [(123456, 22.0)])),
407 Event::Metric(Metric::counter("counter", [(123456, 67.0), (123457, 44.0)])),
408 ];
409
410 let aggregated_metrics = process_metrics(input_metrics);
411 assert_eq!(aggregated_metrics.len(), 1);
412 assert_eq!(aggregated_metrics[0].0, "counter");
413 assert_counter(&aggregated_metrics[0].1, 147.0);
414 }
415
416 #[test]
417 fn aggregate_gauges() {
418 let input_metrics = vec![
419 Event::Metric(Metric::gauge("gauge", 14.0)),
420 Event::Metric(Metric::gauge("gauge", [(123458, 44.0)])),
421 Event::Metric(Metric::gauge("gauge", [(123455, 67.0), (123457, 88.0)])),
422 ];
423
424 let aggregated_metrics = process_metrics(input_metrics);
425 assert_eq!(aggregated_metrics.len(), 1);
426 assert_gauge(&aggregated_metrics[0].1, 44.0);
427 }
428
429 #[test]
430 fn aggregate_gauges_bias_incoming() {
431 let input_metrics = vec![
432 Event::Metric(Metric::gauge("gauge", [(123456, 33.0)])),
433 Event::Metric(Metric::gauge("gauge", [(123456, 66.0)])),
434 ];
435
436 let aggregated_metrics = process_metrics(input_metrics);
437 assert_eq!(aggregated_metrics.len(), 1);
438 assert_gauge(&aggregated_metrics[0].1, 66.0);
439 }
440
441 #[test]
442 fn aggregate_type_change() {
443 let input_metrics = vec![
446 Event::Metric(Metric::gauge("my_metric", 33.0)),
447 Event::Metric(Metric::counter("my_metric", 42.0)),
448 ];
449
450 let aggregated_metrics = process_metrics(input_metrics);
451 assert_eq!(aggregated_metrics.len(), 1);
452 assert_counter(&aggregated_metrics[0].1, 42.0);
453 }
454
455 #[test]
456 fn aggregate_histograms() {
457 let input_metrics = vec![
459 Event::Metric(Metric::histogram("h", [1.0, 2.0, 3.0])),
460 Event::Metric(Metric::histogram("h", [4.0, 5.0])),
461 ];
462
463 let aggregated_metrics = process_metrics(input_metrics);
464 assert_eq!(aggregated_metrics.len(), 1);
465 assert_histogram(&aggregated_metrics[0].1, |hist| {
466 assert_eq!(hist.count(), 5);
467 assert_eq!(hist.sum(), 15.0);
468 });
469 }
470
471 #[test]
472 fn evict_removes_metric() {
473 let processor = AggregatedMetricsProcessor;
474 let state = processor.build_initial_state();
475
476 let context = Context::from_static_parts("counter", &[]);
477 processor.process(
478 MetricsSnapshot {
479 upserts: vec![Event::Metric(Metric::counter(context.clone(), 14.0))],
480 evictions: Vec::new(),
481 },
482 &state,
483 );
484
485 let count_named = |name: &str| {
486 let mut count = 0;
487 state.visit_metrics(|ctx, _| {
488 if ctx.name() == name {
489 count += 1;
490 }
491 });
492 count
493 };
494
495 assert_eq!(count_named("counter"), 1);
497
498 processor.process(
500 MetricsSnapshot {
501 upserts: Vec::new(),
502 evictions: vec![context],
503 },
504 &state,
505 );
506 assert_eq!(count_named("counter"), 0);
507 }
508}