1use std::{
2 num::NonZeroU64,
3 time::{Duration, Instant},
4};
5
6use async_trait::async_trait;
7use ddsketch::DDSketch;
8use hashbrown::{hash_map::Entry, HashMap};
9use saluki_common::time::get_unix_timestamp;
10use saluki_config::GenericConfiguration;
11use saluki_context::Context;
12use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder, UsageExpr};
13use saluki_core::{
14 components::{transforms::*, ComponentContext},
15 data_model::event::{metric::*, Event, EventType},
16 observability::ComponentMetricsExt as _,
17 topology::{interconnect::BufferedDispatcher, OutputDefinition},
18 topology::{EventsBuffer, EventsDispatcher},
19};
20use saluki_error::GenericError;
21use saluki_metrics::MetricsBuilder;
22use serde::Deserialize;
23use smallvec::SmallVec;
24use stringtheory::MetaString;
25use tokio::{
26 pin, select,
27 time::{interval, interval_at},
28};
29use tracing::{debug, error, info, trace, warn};
30
31mod telemetry;
32use self::telemetry::Telemetry;
33
34mod config;
35use self::config::{HistogramConfiguration, HistogramStatistic};
36
37const PASSTHROUGH_IDLE_FLUSH_CHECK_INTERVAL: Duration = Duration::from_secs(2);
38
39const fn default_window_duration_seconds() -> NonZeroU64 {
40 NonZeroU64::new(10).expect("not zero")
41}
42
43const fn default_primary_flush_interval() -> Duration {
44 Duration::from_secs(15)
45}
46
47const fn default_context_limit() -> usize {
48 1_000_000
49}
50
51const fn default_counter_expiry_seconds() -> Option<u64> {
52 Some(300)
53}
54
55const fn default_passthrough_timestamped_metrics() -> bool {
56 true
57}
58
59const fn default_passthrough_idle_flush_timeout() -> Duration {
60 Duration::from_secs(1)
61}
62
63#[derive(Deserialize)]
83#[cfg_attr(test, derive(Debug, PartialEq, serde::Serialize))]
84pub struct AggregateConfiguration {
85 #[serde(
95 rename = "aggregate_window_duration_seconds",
96 default = "default_window_duration_seconds"
97 )]
98 window_duration_seconds: NonZeroU64,
99
100 #[serde(rename = "aggregate_flush_interval", default = "default_primary_flush_interval")]
107 primary_flush_interval: Duration,
108
109 #[serde(rename = "aggregate_context_limit", default = "default_context_limit")]
120 context_limit: usize,
121
122 #[serde(
134 rename = "aggregate_flush_open_windows",
135 alias = "dogstatsd_flush_incomplete_buckets",
136 default
137 )]
138 flush_open_windows: bool,
139
140 #[serde(alias = "dogstatsd_expiry_seconds", default = "default_counter_expiry_seconds")]
153 counter_expiry_seconds: Option<u64>,
154
155 #[serde(
164 rename = "dogstatsd_no_aggregation_pipeline",
165 default = "default_passthrough_timestamped_metrics"
166 )]
167 passthrough_timestamped_metrics: bool,
168
169 #[serde(
177 rename = "aggregate_passthrough_idle_flush_timeout",
178 default = "default_passthrough_idle_flush_timeout"
179 )]
180 passthrough_idle_flush_timeout: Duration,
181
182 #[serde(flatten)]
187 hist_config: HistogramConfiguration,
188}
189
190impl AggregateConfiguration {
191 pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
193 Ok(config.as_typed()?)
194 }
195
196 pub fn with_defaults() -> Self {
198 Self {
199 window_duration_seconds: default_window_duration_seconds(),
200 primary_flush_interval: default_primary_flush_interval(),
201 context_limit: default_context_limit(),
202 flush_open_windows: false,
203 counter_expiry_seconds: default_counter_expiry_seconds(),
204 passthrough_timestamped_metrics: default_passthrough_timestamped_metrics(),
205 passthrough_idle_flush_timeout: default_passthrough_idle_flush_timeout(),
206 hist_config: HistogramConfiguration::default(),
207 }
208 }
209}
210
211#[async_trait]
212impl TransformBuilder for AggregateConfiguration {
213 async fn build(&self, context: ComponentContext) -> Result<Box<dyn Transform + Send>, GenericError> {
214 let metrics_builder = MetricsBuilder::from_component_context(&context);
215 let telemetry = Telemetry::new(&metrics_builder);
216
217 let state = AggregationState::new(
218 self.window_duration_seconds,
219 self.context_limit,
220 self.counter_expiry_seconds.filter(|s| *s != 0).map(Duration::from_secs),
221 self.hist_config.clone(),
222 telemetry.clone(),
223 );
224
225 let passthrough_batcher = PassthroughBatcher::new(
226 self.passthrough_idle_flush_timeout,
227 self.window_duration_seconds,
228 telemetry.clone(),
229 )
230 .await;
231
232 Ok(Box::new(Aggregate {
233 state,
234 telemetry,
235 primary_flush_interval: self.primary_flush_interval,
236 flush_open_windows: self.flush_open_windows,
237 passthrough_batcher,
238 passthrough_timestamped_metrics: self.passthrough_timestamped_metrics,
239 }))
240 }
241
242 fn input_event_type(&self) -> EventType {
243 EventType::Metric
244 }
245
246 fn outputs(&self) -> &[OutputDefinition<EventType>] {
247 static OUTPUTS: &[OutputDefinition<EventType>] = &[OutputDefinition::default_output(EventType::Metric)];
248 OUTPUTS
249 }
250}
251
252impl MemoryBounds for AggregateConfiguration {
253 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
254 builder
263 .minimum()
264 .with_single_value::<Aggregate>("component struct");
266 builder
267 .firm()
268 .with_expr(UsageExpr::product(
270 "aggregation state map",
271 UsageExpr::sum(
272 "context map entry",
273 UsageExpr::struct_size::<Context>("context"),
274 UsageExpr::struct_size::<AggregatedMetric>("aggregated metric"),
275 ),
276 UsageExpr::config("aggregate_context_limit", self.context_limit),
277 ));
278 }
279}
280
281pub struct Aggregate {
282 state: AggregationState,
283 telemetry: Telemetry,
284 primary_flush_interval: Duration,
285 flush_open_windows: bool,
286 passthrough_batcher: PassthroughBatcher,
287 passthrough_timestamped_metrics: bool,
288}
289
290#[async_trait]
291impl Transform for Aggregate {
292 async fn run(mut self: Box<Self>, mut context: TransformContext) -> Result<(), GenericError> {
293 let mut health = context.take_health_handle();
294
295 let mut primary_flush = interval_at(
296 tokio::time::Instant::now() + self.primary_flush_interval,
297 self.primary_flush_interval,
298 );
299 let mut final_primary_flush = false;
300
301 let passthrough_flush = interval(PASSTHROUGH_IDLE_FLUSH_CHECK_INTERVAL);
302
303 health.mark_ready();
304 debug!("Aggregation transform started.");
305
306 pin!(passthrough_flush);
307
308 loop {
309 select! {
310 _ = health.live() => continue,
311 _ = primary_flush.tick() => {
312 if !self.state.is_empty() {
316 debug!("Flushing aggregated metrics...");
317
318 let should_flush_open_windows = final_primary_flush && self.flush_open_windows;
319
320 let was_breached = self.state.context_limit_breached();
322
323 let mut dispatcher = context.dispatcher().buffered().expect("default output should always exist");
324 if let Err(e) = self.state.flush(get_unix_timestamp(), should_flush_open_windows, &mut dispatcher).await {
325 error!(error = %e, "Failed to flush aggregation state.");
326 }
327
328 self.telemetry.increment_flushes();
329
330 if was_breached && !self.state.context_limit_breached() {
332 info!("Context limit no longer exceeded, metrics are being accepted again.");
333 }
334
335 match dispatcher.flush().await {
336 Ok(aggregated_events) => debug!(aggregated_events, "Dispatched events."),
337 Err(e) => error!(error = %e, "Failed to flush aggregated events."),
338 }
339 }
340
341 if final_primary_flush {
343 debug!("All aggregation complete.");
344 break
345 }
346 },
347 _ = passthrough_flush.tick() => self.passthrough_batcher.try_flush(context.dispatcher()).await,
348 maybe_events = context.events().next(), if !final_primary_flush => match maybe_events {
349 Some(events) => {
350 trace!(events_len = events.len(), "Received events.");
351
352 let current_time = get_unix_timestamp();
353 let mut processed_passthrough_metrics = false;
354
355 for event in events {
356 if let Some(metric) = event.try_into_metric() {
357 let metric = if self.passthrough_timestamped_metrics {
358 let (maybe_timestamped_metric, maybe_nontimestamped_metric) = try_split_timestamped_values(metric);
362
363 if let Some(timestamped_metric) = maybe_timestamped_metric {
365 self.passthrough_batcher.push_metric(timestamped_metric, context.dispatcher()).await;
366 processed_passthrough_metrics = true;
367 }
368
369 match maybe_nontimestamped_metric {
373 Some(metric) => metric,
374 None => continue,
375 }
376 } else {
377 metric
378 };
379
380 let was_breached = self.state.context_limit_breached();
381 if !self.state.insert(current_time, metric) {
382 trace!("Dropping metric due to context limit.");
383 if !was_breached {
384 warn!(context_limit = self.state.context_limit, "Context limit reached, \
386 dropping metrics. Consider increasing `aggregate_context_limit`.");
387 }
388 self.telemetry.increment_events_dropped();
389 }
390 }
391 }
392
393 if processed_passthrough_metrics {
394 self.passthrough_batcher.update_last_processed_at();
395 }
396 },
397 None => {
398 final_primary_flush = true;
401 primary_flush.reset_immediately();
402
403 debug!("Aggregation transform stopping...");
404 }
405 },
406 }
407 }
408
409 self.passthrough_batcher.try_flush(context.dispatcher()).await;
411
412 debug!("Aggregation transform stopped.");
413
414 Ok(())
415 }
416}
417
418fn try_split_timestamped_values(mut metric: Metric) -> (Option<Metric>, Option<Metric>) {
419 if metric.values().all_timestamped() {
420 (Some(metric), None)
421 } else if metric.values().any_timestamped() {
422 let new_metric_values = metric.values_mut().split_timestamped();
424 let new_metric = Metric::from_parts(metric.context().clone(), new_metric_values, metric.metadata().clone());
425
426 (Some(new_metric), Some(metric))
427 } else {
428 (None, Some(metric))
430 }
431}
432
433struct PassthroughBatcher {
434 active_buffer: EventsBuffer,
435 active_buffer_start: Instant,
436 last_processed_at: Instant,
437 idle_flush_timeout: Duration,
438 bucket_width_secs: NonZeroU64,
439 telemetry: Telemetry,
440}
441
442impl PassthroughBatcher {
443 async fn new(idle_flush_timeout: Duration, bucket_width_secs: NonZeroU64, telemetry: Telemetry) -> Self {
444 let active_buffer = EventsBuffer::default();
445
446 Self {
447 active_buffer,
448 active_buffer_start: Instant::now(),
449 last_processed_at: Instant::now(),
450 idle_flush_timeout,
451 bucket_width_secs,
452 telemetry,
453 }
454 }
455
456 async fn push_metric(&mut self, metric: Metric, dispatcher: &EventsDispatcher) {
457 let (context, values, metadata) = metric.into_parts();
463 let adjusted_values = counter_values_to_rate(values, self.bucket_width_secs);
464 let metric = Metric::from_parts(context, adjusted_values, metadata);
465
466 if let Some(event) = self.active_buffer.try_push(Event::Metric(metric)) {
470 debug!("Passthrough event buffer was full. Flushing...");
471 self.dispatch_events(dispatcher).await;
472
473 if self.active_buffer.try_push(event).is_some() {
474 error!("Event buffer is full even after dispatching events. Dropping event.");
475 self.telemetry.increment_events_dropped();
476 return;
477 }
478 }
479
480 if self.active_buffer.len() == 1 {
482 self.active_buffer_start = Instant::now();
483 }
484
485 self.telemetry.increment_passthrough_metrics();
486 }
487
488 fn update_last_processed_at(&mut self) {
489 self.last_processed_at = Instant::now();
493 }
494
495 async fn try_flush(&mut self, dispatcher: &EventsDispatcher) {
496 if !self.active_buffer.is_empty() && self.last_processed_at.elapsed() >= self.idle_flush_timeout {
498 debug!("Passthrough processing exceeded idle flush timeout. Flushing...");
499
500 self.dispatch_events(dispatcher).await;
501 }
502 }
503
504 async fn dispatch_events(&mut self, dispatcher: &EventsDispatcher) {
505 if !self.active_buffer.is_empty() {
506 let unaggregated_events = self.active_buffer.len();
507
508 let batch_duration = self.active_buffer_start.elapsed();
510 self.telemetry.record_passthrough_batch_duration(batch_duration);
511
512 self.telemetry.increment_passthrough_flushes();
513
514 let new_active_buffer = EventsBuffer::default();
516 let old_active_buffer = std::mem::replace(&mut self.active_buffer, new_active_buffer);
517
518 match dispatcher.dispatch(old_active_buffer).await {
519 Ok(()) => debug!(unaggregated_events, "Dispatched events."),
520 Err(e) => error!(error = %e, "Failed to flush unaggregated events."),
521 }
522 }
523 }
524}
525
526#[derive(Clone)]
527struct AggregatedMetric {
528 values: MetricValues,
529 metadata: MetricMetadata,
530 last_seen: u64,
531}
532
533struct AggregationState {
534 contexts: HashMap<Context, AggregatedMetric, foldhash::quality::RandomState>,
535 contexts_remove_buf: Vec<Context>,
536 context_limit: usize,
537 bucket_width_secs: NonZeroU64,
538 counter_expire_secs: Option<NonZeroU64>,
539 last_flush: u64,
540 hist_config: HistogramConfiguration,
541 telemetry: Telemetry,
542 context_limit_breached: bool,
545}
546
547impl AggregationState {
548 fn new(
549 bucket_width_secs: NonZeroU64, context_limit: usize, counter_expiration: Option<Duration>,
550 hist_config: HistogramConfiguration, telemetry: Telemetry,
551 ) -> Self {
552 let counter_expire_secs = counter_expiration.map(|d| d.as_secs()).and_then(NonZeroU64::new);
553
554 Self {
555 contexts: HashMap::default(),
556 contexts_remove_buf: Vec::new(),
557 context_limit,
558 bucket_width_secs,
559 counter_expire_secs,
560 last_flush: 0,
561 hist_config,
562 telemetry,
563 context_limit_breached: false,
564 }
565 }
566
567 fn is_empty(&self) -> bool {
568 self.contexts.is_empty()
569 }
570
571 fn insert(&mut self, timestamp: u64, metric: Metric) -> bool {
572 if !self.contexts.contains_key(metric.context()) && self.contexts.len() >= self.context_limit {
574 self.context_limit_breached = true;
575 return false;
576 }
577
578 let (context, mut values, metadata) = metric.into_parts();
579
580 let bucket_ts = align_to_bucket_start(timestamp, self.bucket_width_secs);
585 values.collapse_non_timestamped(bucket_ts);
586
587 trace!(
588 bucket_ts,
589 kind = values.as_str(),
590 "Inserting metric into aggregation state."
591 );
592
593 match self.contexts.entry(context) {
596 Entry::Occupied(mut entry) => {
597 let aggregated = entry.get_mut();
598
599 aggregated.last_seen = timestamp;
601 aggregated.values.merge(values);
602 }
603 Entry::Vacant(entry) => {
604 self.telemetry.increment_contexts(entry.key(), &values);
605
606 entry.insert(AggregatedMetric {
607 values,
608 metadata,
609 last_seen: timestamp,
610 });
611
612 saluki_antithesis::always_le!(
613 self.contexts.len(),
614 self.context_limit,
615 "aggregate context map within context_limit",
616 { "len": self.contexts.len(), "limit": self.context_limit }
617 );
618 }
619 }
620
621 true
622 }
623
624 async fn flush(
625 &mut self, current_time: u64, flush_open_buckets: bool, dispatcher: &mut BufferedDispatcher<'_, EventsBuffer>,
626 ) -> Result<(), GenericError> {
627 let bucket_width_secs = self.bucket_width_secs;
628 let counter_expire_secs = self.counter_expire_secs.map(|d| d.get()).unwrap_or(0);
629
630 let split_timestamp = align_to_bucket_start(current_time, bucket_width_secs).saturating_sub(1);
633
634 let mut zero_value_buckets = SmallVec::<[(u64, MetricValues); 4]>::new();
639 if self.last_flush != 0 {
640 let start = align_to_bucket_start(self.last_flush, bucket_width_secs);
641
642 saluki_antithesis::always_ge!(
647 current_time,
648 self.last_flush,
649 "aggregate flush wall-clock did not move backward",
650 { "current_time": current_time, "last_flush": self.last_flush }
651 );
652 saluki_antithesis::always_le!(
655 current_time.saturating_sub(self.last_flush) / bucket_width_secs.get(),
656 10_000,
657 "aggregate zero-value bucket span bounded across a flush",
658 {
659 "current_time": current_time,
660 "last_flush": self.last_flush,
661 "bucket_width_secs": bucket_width_secs.get()
662 }
663 );
664
665 for bucket_start in (start..current_time).step_by(bucket_width_secs.get() as usize) {
666 if is_bucket_closed(current_time, bucket_start, bucket_width_secs, flush_open_buckets) {
667 zero_value_buckets.push((bucket_start, MetricValues::counter((bucket_start, 0.0))));
668 }
669 }
670
671 saluki_antithesis::sometimes!(
673 !zero_value_buckets.is_empty(),
674 "aggregate flush generated zero-value counter buckets",
675 { "count": zero_value_buckets.len() }
676 );
677 }
678
679 debug!(timestamp = current_time, "Flushing buckets.");
681
682 for (context, am) in self.contexts.iter_mut() {
683 let should_expire_if_empty = match &am.values {
692 MetricValues::Counter(..) => {
693 saluki_antithesis::always_le!(
694 am.last_seen,
695 u64::MAX - counter_expire_secs,
696 "aggregate counter expiry add does not overflow",
697 { "last_seen": am.last_seen, "counter_expire_secs": counter_expire_secs }
698 );
699 counter_expire_secs != 0 && am.last_seen.saturating_add(counter_expire_secs) < current_time
700 }
701 _ => true,
702 };
703
704 if let MetricValues::Counter(..) = &mut am.values {
710 let expires_at = am.last_seen.saturating_add(counter_expire_secs);
711 for (zv_bucket_start, zero_value) in &zero_value_buckets {
712 if expires_at > *zv_bucket_start {
713 am.values.merge(zero_value.clone());
714 } else {
715 break;
718 }
719 }
720 }
721
722 if let Some(closed_bucket_values) = am.values.split_at_timestamp(split_timestamp) {
731 self.telemetry.increment_flushed(&closed_bucket_values);
732
733 transform_and_push_metric(
735 context.clone(),
736 closed_bucket_values,
737 am.metadata.clone(),
738 bucket_width_secs,
739 &self.hist_config,
740 dispatcher,
741 )
742 .await?;
743 }
744
745 if am.values.is_empty() && should_expire_if_empty {
746 self.telemetry.decrement_contexts(context, &am.values);
747 self.contexts_remove_buf.push(context.clone());
748 }
749 }
750
751 let contexts_len_before = self.contexts.len();
753 for context in self.contexts_remove_buf.drain(..) {
754 self.contexts.remove(&context);
755 }
756 let contexts_len_after = self.contexts.len();
757
758 let contexts_delta = contexts_len_before.saturating_sub(contexts_len_after);
759 let target_contexts_capacity = contexts_len_after.saturating_add(contexts_delta / 2);
760 self.contexts.shrink_to(target_contexts_capacity);
761
762 if self.context_limit_breached && self.contexts.len() < self.context_limit {
763 self.context_limit_breached = false;
764 }
765
766 self.last_flush = current_time;
767
768 Ok(())
769 }
770
771 fn context_limit_breached(&self) -> bool {
772 self.context_limit_breached
773 }
774}
775
776async fn transform_and_push_metric(
777 context: Context, mut values: MetricValues, metadata: MetricMetadata, bucket_width_secs: NonZeroU64,
778 hist_config: &HistogramConfiguration, dispatcher: &mut BufferedDispatcher<'_, EventsBuffer>,
779) -> Result<(), GenericError> {
780 let bucket_width = Duration::from_secs(bucket_width_secs.get());
781
782 match values {
783 MetricValues::Histogram(ref mut points) => {
786 if hist_config.copy_to_distribution() {
788 let sketch_points = points
789 .into_iter()
790 .map(|(ts, hist)| {
791 let mut sketch = DDSketch::default();
792 for sample in hist.samples() {
793 sketch.insert_n(sample.value.into_inner(), sample.weight.0 as u64);
794 }
795 (ts, sketch)
796 })
797 .collect::<SketchPoints>();
798 let distribution_values = MetricValues::distribution(sketch_points);
799 let metric_context = if !hist_config.copy_to_distribution_prefix().is_empty() {
800 context.with_name(format!(
801 "{}{}",
802 hist_config.copy_to_distribution_prefix(),
803 context.name()
804 ))
805 } else {
806 context.clone()
807 };
808 let new_metric = Metric::from_parts(metric_context, distribution_values, metadata.clone());
809 dispatcher.push(Event::Metric(new_metric)).await?;
810 }
811 let mut sorted_points = Vec::new();
816 for (ts, h) in points {
817 sorted_points.push((ts, h.summary_view()));
818 }
819
820 for statistic in hist_config.statistics() {
821 let new_points = sorted_points
822 .iter()
823 .map(|(ts, hs)| (*ts, statistic.value_from_histogram(hs)))
824 .collect::<ScalarPoints>();
825
826 let new_values = if statistic.is_rate_statistic() {
827 MetricValues::rate(new_points, bucket_width)
828 } else {
829 MetricValues::gauge(new_points)
830 };
831
832 let new_metadata = if matches!(statistic, HistogramStatistic::Count) {
834 metadata.clone().with_unit(MetaString::empty())
835 } else {
836 metadata.clone()
837 };
838
839 let new_context = context.with_name(format!("{}.{}", context.name(), statistic.suffix()));
840 let new_metric = Metric::from_parts(new_context, new_values, new_metadata);
841 dispatcher.push(Event::Metric(new_metric)).await?;
842 }
843
844 Ok(())
845 }
846
847 values => {
850 let adjusted_values = counter_values_to_rate(values, bucket_width_secs);
851
852 let metric = Metric::from_parts(context, adjusted_values, metadata);
853 dispatcher.push(Event::Metric(metric)).await
854 }
855 }
856}
857
858fn counter_values_to_rate(values: MetricValues, interval_secs: NonZeroU64) -> MetricValues {
859 match values {
860 MetricValues::Counter(points) => MetricValues::rate(points, Duration::from_secs(interval_secs.get())),
861 values => values,
862 }
863}
864
865const fn align_to_bucket_start(timestamp: u64, bucket_width_secs: NonZeroU64) -> u64 {
866 timestamp - (timestamp % bucket_width_secs.get())
867}
868
869const fn is_bucket_closed(
870 current_time: u64, bucket_start: u64, bucket_width_secs: NonZeroU64, flush_open_buckets: bool,
871) -> bool {
872 (bucket_start + bucket_width_secs.get() - 1) < current_time || flush_open_buckets
891}
892
893#[cfg(test)]
897mod tests {
898 use float_cmp::ApproxEqRatio as _;
899 use saluki_core::{
900 components::ComponentContext,
901 topology::{interconnect::Dispatcher, OutputName},
902 };
903 use saluki_metrics::test::TestRecorder;
904 use stringtheory::MetaString;
905 use tokio::sync::mpsc;
906
907 use super::config::HistogramStatistic;
908 use super::*;
909
910 const BUCKET_WIDTH_SECS: NonZeroU64 = NonZeroU64::new(10).expect("not zero");
911 const BUCKET_WIDTH: Duration = Duration::from_secs(BUCKET_WIDTH_SECS.get());
912 const COUNTER_EXPIRE_SECS: u64 = 20;
913 const COUNTER_EXPIRE: Option<Duration> = Some(Duration::from_secs(COUNTER_EXPIRE_SECS));
914
915 const fn bucket_ts(step: u64) -> u64 {
917 align_to_bucket_start(insert_ts(step), BUCKET_WIDTH_SECS)
918 }
919
920 const fn insert_ts(step: u64) -> u64 {
922 (BUCKET_WIDTH_SECS.get() * (step + 1)) - 2
923 }
924
925 const fn flush_ts(step: u64) -> u64 {
927 BUCKET_WIDTH_SECS.get() * (step + 1)
928 }
929
930 struct DispatcherReceiver {
931 receiver: mpsc::Receiver<EventsBuffer>,
932 }
933
934 impl DispatcherReceiver {
935 fn collect_next(&mut self) -> Vec<Metric> {
936 match self.receiver.try_recv() {
937 Ok(event_buffer) => {
938 let mut metrics = event_buffer
939 .into_iter()
940 .filter_map(|event| event.try_into_metric())
941 .collect::<Vec<Metric>>();
942
943 metrics.sort_by(|a, b| a.context().name().cmp(b.context().name()));
944 metrics
945 }
946 Err(_) => Vec::new(),
947 }
948 }
949 }
950
951 fn build_basic_dispatcher() -> (EventsDispatcher, DispatcherReceiver) {
953 let context = ComponentContext::test_transform("test");
954 let mut dispatcher = Dispatcher::new(context);
955
956 let (buffer_tx, buffer_rx) = mpsc::channel(1);
957 dispatcher.add_output(OutputName::Default).unwrap();
958 dispatcher
959 .attach_sender_to_output(&OutputName::Default, buffer_tx)
960 .unwrap();
961
962 (dispatcher, DispatcherReceiver { receiver: buffer_rx })
963 }
964
965 async fn get_flushed_metrics(timestamp: u64, state: &mut AggregationState) -> Vec<Metric> {
966 let (dispatcher, mut dispatcher_receiver) = build_basic_dispatcher();
967 let mut buffered_dispatcher = dispatcher.buffered().expect("default output should always exist");
968
969 state
971 .flush(timestamp, true, &mut buffered_dispatcher)
972 .await
973 .expect("should not fail to flush aggregation state");
974
975 buffered_dispatcher
978 .flush()
979 .await
980 .expect("should not fail to flush buffered sender");
981
982 dispatcher_receiver.collect_next()
983 }
984
985 macro_rules! compare_points {
986 (scalar, $expected:expr, $actual:expr, $error_ratio:literal) => {
987 for (idx, (expected_value, actual_value)) in $expected.into_iter().zip($actual.into_iter()).enumerate() {
988 let (expected_ts, expected_point) = expected_value;
989 let (actual_ts, actual_point) = actual_value;
990
991 assert_eq!(
992 expected_ts, actual_ts,
993 "timestamp for value #{} does not match: {:?} (expected) vs {:?} (actual)",
994 idx, expected_ts, actual_ts
995 );
996 assert!(
997 expected_point.approx_eq_ratio(&actual_point, $error_ratio),
998 "point for value #{} does not match: {} (expected) vs {} (actual)",
999 idx,
1000 expected_point,
1001 actual_point
1002 );
1003 }
1004 };
1005 (distribution, $expected:expr, $actual:expr) => {
1006 for (idx, (expected_value, actual_value)) in $expected.into_iter().zip($actual.into_iter()).enumerate() {
1007 let (expected_ts, expected_sketch) = expected_value;
1008 let (actual_ts, actual_sketch) = actual_value;
1009
1010 assert_eq!(
1011 expected_ts, actual_ts,
1012 "timestamp for value #{} does not match: {:?} (expected) vs {:?} (actual)",
1013 idx, expected_ts, actual_ts
1014 );
1015 assert_eq!(
1016 expected_sketch, actual_sketch,
1017 "sketch for value #{} does not match: {:?} (expected) vs {:?} (actual)",
1018 idx, expected_sketch, actual_sketch
1019 );
1020 }
1021 };
1022 }
1023
1024 macro_rules! assert_flushed_scalar_metric {
1025 ($original:expr, $actual:expr, [$($ts:expr => $value:expr),+]) => {
1026 assert_flushed_scalar_metric!($original, $actual, [$($ts => $value),+], error_ratio => 0.000001);
1027 };
1028 ($original:expr, $actual:expr, [$($ts:expr => $value:expr),+], error_ratio => $error_ratio:literal) => {
1029 let actual_metric = $actual;
1030
1031 assert_eq!($original.context(), actual_metric.context(), "expected context ({}) and actual context ({}) do not match", $original.context(), actual_metric.context());
1032
1033 let expected_points = ScalarPoints::from([$(($ts, $value)),+]);
1034
1035 match actual_metric.values() {
1036 MetricValues::Counter(ref actual_points) | MetricValues::Gauge(ref actual_points) | MetricValues::Rate(ref actual_points, _) => {
1037 assert_eq!(expected_points.len(), actual_points.len(), "expected and actual values have different number of points");
1038 compare_points!(scalar, expected_points, actual_points, $error_ratio);
1039 },
1040 _ => panic!("only counters, rates, and gauges are supported in assert_flushed_scalar_metric"),
1041 }
1042 };
1043 }
1044
1045 macro_rules! assert_flushed_distribution_metric {
1046 ($original:expr, $actual:expr, [$($ts:expr => $value:expr),+]) => {
1047 assert_flushed_distribution_metric!($original, $actual, [$($ts => $value),+], error_ratio => 0.000001);
1048 };
1049 ($original:expr, $actual:expr, [$($ts:expr => $value:expr),+], error_ratio => $error_ratio:literal) => {
1050 let actual_metric = $actual;
1051
1052 assert_eq!($original.context(), actual_metric.context());
1053
1054 match actual_metric.values() {
1055 MetricValues::Distribution(ref actual_points) => {
1056 let expected_points = SketchPoints::from([$(($ts, $value)),+]);
1057 assert_eq!(expected_points.len(), actual_points.len(), "expected and actual values have different number of points");
1058
1059 compare_points!(distribution, &expected_points, actual_points);
1060 },
1061 _ => panic!("only distributions are supported in assert_flushed_distribution_metric"),
1062 }
1063 };
1064 }
1065
1066 #[test]
1067 fn bucket_is_closed() {
1068 let cases = [
1071 (1000, 995, BUCKET_WIDTH_SECS, false, false),
1073 (1000, 995, BUCKET_WIDTH_SECS, true, true),
1074 (1000, 1000, BUCKET_WIDTH_SECS, false, false),
1076 (1000, 1000, BUCKET_WIDTH_SECS, true, true),
1077 (1010, 1000, BUCKET_WIDTH_SECS, false, true),
1079 (1010, 1000, BUCKET_WIDTH_SECS, true, true),
1080 ];
1081
1082 for (current_time, bucket_start, bucket_width_secs, flush_open_buckets, expected) in cases {
1083 let expected_reason = if expected {
1084 "closed, was open"
1085 } else {
1086 "open, was closed"
1087 };
1088
1089 assert_eq!(
1090 is_bucket_closed(current_time, bucket_start, bucket_width_secs, flush_open_buckets),
1091 expected,
1092 "expected bucket to be {} (current_time={}, bucket_start={}, bucket_width={}, flush_open_buckets={})",
1093 expected_reason,
1094 current_time,
1095 bucket_start,
1096 bucket_width_secs,
1097 flush_open_buckets
1098 );
1099 }
1100 }
1101
1102 #[tokio::test]
1103 async fn context_limit() {
1104 let mut state = AggregationState::new(
1106 BUCKET_WIDTH_SECS,
1107 2,
1108 COUNTER_EXPIRE,
1109 HistogramConfiguration::default(),
1110 Telemetry::noop(),
1111 );
1112
1113 let input_metrics = [
1116 Metric::gauge("metric1", 1.0),
1117 Metric::gauge("metric2", 2.0),
1118 Metric::gauge("metric3", 3.0),
1119 Metric::gauge("metric4", 4.0),
1120 ];
1121
1122 assert!(!state.context_limit_breached());
1123
1124 assert!(state.insert(insert_ts(1), input_metrics[0].clone()));
1125 assert!(state.insert(insert_ts(1), input_metrics[1].clone()));
1126 assert!(!state.context_limit_breached());
1127
1128 assert!(!state.insert(insert_ts(1), input_metrics[2].clone()));
1129 assert!(state.context_limit_breached());
1130 assert!(!state.insert(insert_ts(1), input_metrics[3].clone()));
1131 assert!(state.context_limit_breached());
1132
1133 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1136 assert_eq!(flushed_metrics.len(), 2);
1137 assert_eq!(input_metrics[0].context(), flushed_metrics[0].context());
1138 assert_eq!(input_metrics[1].context(), flushed_metrics[1].context());
1139 assert!(!state.context_limit_breached());
1140
1141 assert!(state.insert(insert_ts(2), input_metrics[2].clone()));
1144 assert!(state.insert(insert_ts(2), input_metrics[3].clone()));
1145
1146 let flushed_metrics = get_flushed_metrics(flush_ts(2), &mut state).await;
1147 assert_eq!(flushed_metrics.len(), 2);
1148 assert_eq!(input_metrics[2].context(), flushed_metrics[0].context());
1149 assert_eq!(input_metrics[3].context(), flushed_metrics[1].context());
1150 }
1151
1152 #[tokio::test]
1153 async fn context_limit_with_zero_value_counters() {
1154 let mut state = AggregationState::new(
1156 BUCKET_WIDTH_SECS,
1157 2,
1158 COUNTER_EXPIRE,
1159 HistogramConfiguration::default(),
1160 Telemetry::noop(),
1161 );
1162
1163 let input_metrics = [
1165 Metric::counter("metric1", 1.0),
1166 Metric::counter("metric2", 2.0),
1167 Metric::counter("metric3", 3.0),
1168 ];
1169
1170 assert!(state.insert(insert_ts(1), input_metrics[0].clone()));
1171 assert!(state.insert(insert_ts(1), input_metrics[1].clone()));
1172
1173 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1175 assert_eq!(flushed_metrics.len(), 2);
1176 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(1) => 1.0]);
1177 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(1) => 2.0]);
1178
1179 let flushed_metrics = get_flushed_metrics(flush_ts(2), &mut state).await;
1181 assert_eq!(flushed_metrics.len(), 2);
1182 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(2) => 0.0]);
1183 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(2) => 0.0]);
1184
1185 assert!(!state.insert(insert_ts(3), input_metrics[2].clone()));
1187
1188 let flushed_metrics = get_flushed_metrics(flush_ts(3), &mut state).await;
1190 assert_eq!(flushed_metrics.len(), 2);
1191 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(3) => 0.0]);
1192 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(3) => 0.0]);
1193
1194 let flushed_metrics = get_flushed_metrics(flush_ts(4), &mut state).await;
1197 assert_eq!(flushed_metrics.len(), 0);
1198
1199 assert!(state.insert(insert_ts(5), input_metrics[2].clone()));
1201
1202 let flushed_metrics = get_flushed_metrics(flush_ts(5), &mut state).await;
1203 assert_eq!(flushed_metrics.len(), 1);
1204 assert_flushed_scalar_metric!(&input_metrics[2], &flushed_metrics[0], [bucket_ts(5) => 3.0]);
1205 }
1206
1207 #[tokio::test]
1208 async fn zero_value_counters() {
1209 let mut state = AggregationState::new(
1211 BUCKET_WIDTH_SECS,
1212 10,
1213 COUNTER_EXPIRE,
1214 HistogramConfiguration::default(),
1215 Telemetry::noop(),
1216 );
1217
1218 let input_metrics = [Metric::counter("metric1", 1.0), Metric::counter("metric2", 2.0)];
1220
1221 assert!(state.insert(insert_ts(1), input_metrics[0].clone()));
1222 assert!(state.insert(insert_ts(1), input_metrics[1].clone()));
1223
1224 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1226 assert_eq!(flushed_metrics.len(), 2);
1227 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(1) => 1.0]);
1228 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(1) => 2.0]);
1229
1230 let flushed_metrics = get_flushed_metrics(flush_ts(2), &mut state).await;
1232 assert_eq!(flushed_metrics.len(), 2);
1233 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(2) => 0.0]);
1234 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(2) => 0.0]);
1235
1236 assert!(state.insert(insert_ts(4), input_metrics[0].clone()));
1238 assert!(state.insert(insert_ts(4), input_metrics[1].clone()));
1239
1240 let flushed_metrics = get_flushed_metrics(flush_ts(4), &mut state).await;
1243 assert_eq!(flushed_metrics.len(), 2);
1244 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(3) => 0.0, bucket_ts(4) => 1.0]);
1245 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(3) => 0.0, bucket_ts(4) => 2.0]);
1246
1247 let flushed_metrics = get_flushed_metrics(flush_ts(7), &mut state).await;
1251 assert_eq!(flushed_metrics.len(), 2);
1252 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(5) => 0.0, bucket_ts(6) => 0.0]);
1253 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(5) => 0.0, bucket_ts(6) => 0.0]);
1254 }
1255
1256 #[tokio::test]
1257 async fn merge_identical_timestamped_values_on_flush() {
1258 let mut state = AggregationState::new(
1260 BUCKET_WIDTH_SECS,
1261 10,
1262 COUNTER_EXPIRE,
1263 HistogramConfiguration::default(),
1264 Telemetry::noop(),
1265 );
1266
1267 let input_metric = Metric::counter("metric1", [1.0, 2.0, 3.0, 4.0, 5.0]);
1269
1270 assert!(state.insert(insert_ts(1), input_metric.clone()));
1271
1272 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1275 assert_eq!(flushed_metrics.len(), 1);
1276 assert_flushed_scalar_metric!(&input_metric, &flushed_metrics[0], [bucket_ts(1) => 15.0]);
1277 }
1278
1279 #[tokio::test]
1280 async fn histogram_statistics() {
1281 let hist_config = HistogramConfiguration::from_statistics(
1283 &[
1284 HistogramStatistic::Count,
1285 HistogramStatistic::Sum,
1286 HistogramStatistic::Percentile {
1287 q: 0.5,
1288 suffix: "p50".into(),
1289 },
1290 ],
1291 false,
1292 "".into(),
1293 );
1294 let mut state = AggregationState::new(BUCKET_WIDTH_SECS, 10, COUNTER_EXPIRE, hist_config, Telemetry::noop());
1295
1296 let input_metric = Metric::histogram("metric1", [1.0, 2.0, 3.0, 4.0, 5.0]);
1298 assert!(state.insert(insert_ts(1), input_metric.clone()));
1299
1300 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1303 assert_eq!(flushed_metrics.len(), 3);
1304
1305 let count_metric = Metric::rate("metric1.count", 0.0, Duration::from_secs(BUCKET_WIDTH_SECS.get()));
1308 let sum_metric = Metric::gauge("metric1.sum", 0.0);
1309 let p50_metric = Metric::gauge("metric1.p50", 0.0);
1310
1311 assert_flushed_scalar_metric!(count_metric, &flushed_metrics[0], [bucket_ts(1) => 5.0]);
1314 assert_flushed_scalar_metric!(p50_metric, &flushed_metrics[1], [bucket_ts(1) => 3.0], error_ratio => 0.0025);
1315 assert_flushed_scalar_metric!(sum_metric, &flushed_metrics[2], [bucket_ts(1) => 15.0]);
1316 }
1317
1318 #[tokio::test]
1319 async fn histogram_statistics_unit_propagation() {
1320 let hist_config = HistogramConfiguration::from_statistics(
1322 &[
1323 HistogramStatistic::Count,
1324 HistogramStatistic::Sum,
1325 HistogramStatistic::Percentile {
1326 q: 0.5,
1327 suffix: "p50".into(),
1328 },
1329 ],
1330 false,
1331 "".into(),
1332 );
1333 let mut state = AggregationState::new(BUCKET_WIDTH_SECS, 10, COUNTER_EXPIRE, hist_config, Telemetry::noop());
1334
1335 let context = Context::from_static_parts("metric1", &[]);
1337 let metadata = MetricMetadata::default().with_unit(MetaString::from_static("millisecond"));
1338 let input_metric = Metric::from_parts(
1339 context,
1340 MetricValues::histogram([1.0_f64, 2.0, 3.0, 4.0, 5.0]),
1341 metadata,
1342 );
1343 assert!(state.insert(insert_ts(1), input_metric));
1344
1345 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1346 assert_eq!(flushed_metrics.len(), 3);
1347
1348 for metric in &flushed_metrics {
1351 let name = metric.context().name();
1352 if name.ends_with(".count") {
1353 assert_eq!(
1354 metric.metadata().unit(),
1355 None,
1356 "flushed metric '{}' should be dimensionless",
1357 name
1358 );
1359 } else {
1360 assert_eq!(
1361 metric.metadata().unit(),
1362 Some("millisecond"),
1363 "flushed metric '{}' should carry unit='millisecond'",
1364 name
1365 );
1366 }
1367 }
1368 }
1369
1370 #[tokio::test]
1371 async fn distributions() {
1372 let mut state = AggregationState::new(
1374 BUCKET_WIDTH_SECS,
1375 10,
1376 COUNTER_EXPIRE,
1377 HistogramConfiguration::default(),
1378 Telemetry::noop(),
1379 );
1380
1381 let values = [1.0, 2.0, 3.0, 4.0, 5.0];
1383 let input_metric = Metric::distribution("metric1", &values[..]);
1384
1385 assert!(state.insert(insert_ts(1), input_metric.clone()));
1386
1387 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1389 assert_eq!(flushed_metrics.len(), 1);
1390
1391 assert_flushed_distribution_metric!(&input_metric, &flushed_metrics[0], [bucket_ts(1) => &values[..]]);
1392 }
1393
1394 #[tokio::test]
1395 async fn histogram_copy_to_distribution() {
1396 let hist_config = HistogramConfiguration::from_statistics(
1397 &[
1398 HistogramStatistic::Count,
1399 HistogramStatistic::Sum,
1400 HistogramStatistic::Percentile {
1401 q: 0.5,
1402 suffix: "p50".into(),
1403 },
1404 ],
1405 true,
1406 "dist_prefix.".into(),
1407 );
1408 let mut state = AggregationState::new(BUCKET_WIDTH_SECS, 10, COUNTER_EXPIRE, hist_config, Telemetry::noop());
1409
1410 let values = [1.0, 2.0, 3.0, 4.0, 5.0];
1412 let input_metric = Metric::histogram("metric1", values);
1413 assert!(state.insert(insert_ts(1), input_metric.clone()));
1414
1415 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1418 assert_eq!(flushed_metrics.len(), 4);
1419
1420 let count_metric = Metric::rate("metric1.count", 0.0, BUCKET_WIDTH);
1423 let sum_metric = Metric::gauge("metric1.sum", 0.0);
1424 let p50_metric = Metric::gauge("metric1.p50", 0.0);
1425 let expected_distribution = Metric::distribution("dist_prefix.metric1", &values[..]);
1426
1427 assert_flushed_distribution_metric!(expected_distribution, &flushed_metrics[0], [bucket_ts(1) => &values[..]]);
1430 assert_flushed_scalar_metric!(count_metric, &flushed_metrics[1], [bucket_ts(1) => 5.0]);
1431 assert_flushed_scalar_metric!(p50_metric, &flushed_metrics[2], [bucket_ts(1) => 3.0], error_ratio => 0.0025);
1432 assert_flushed_scalar_metric!(sum_metric, &flushed_metrics[3], [bucket_ts(1) => 15.0]);
1433 }
1434
1435 #[tokio::test]
1436 async fn nonaggregated_counters_to_rate() {
1437 let counter_value = 42.0;
1438
1439 let mut state = AggregationState::new(
1441 BUCKET_WIDTH_SECS,
1442 10,
1443 COUNTER_EXPIRE,
1444 HistogramConfiguration::default(),
1445 Telemetry::noop(),
1446 );
1447
1448 let input_metric = Metric::counter("metric1", counter_value);
1450 assert!(state.insert(insert_ts(1), input_metric.clone()));
1451
1452 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1455 assert_eq!(flushed_metrics.len(), 1);
1456 let flushed_metric = &flushed_metrics[0];
1457
1458 assert_flushed_scalar_metric!(&input_metric, flushed_metric, [bucket_ts(1) => counter_value]);
1459 assert_eq!(flushed_metric.values().as_str(), "rate");
1460 }
1461
1462 #[tokio::test]
1463 async fn preaggregated_counters_to_rate() {
1464 let counter_value = 42.0;
1465 let timestamp = 123456;
1466
1467 let mut batcher = PassthroughBatcher::new(Duration::from_nanos(1), BUCKET_WIDTH_SECS, Telemetry::noop()).await;
1469 let (dispatcher, mut dispatcher_receiver) = build_basic_dispatcher();
1470
1471 let input_metric = Metric::counter("metric1", (timestamp, counter_value));
1473 batcher.push_metric(input_metric.clone(), &dispatcher).await;
1474
1475 batcher.try_flush(&dispatcher).await;
1478
1479 let mut flushed_metrics = dispatcher_receiver.collect_next();
1480 assert_eq!(flushed_metrics.len(), 1);
1481 assert_eq!(
1482 Metric::rate("metric1", (timestamp, counter_value), BUCKET_WIDTH),
1483 flushed_metrics.remove(0)
1484 );
1485 }
1486
1487 #[tokio::test]
1488 async fn telemetry() {
1489 let recorder = TestRecorder::default();
1496 let _local = metrics::set_default_local_recorder(&recorder);
1497
1498 let builder = MetricsBuilder::default();
1499 let telemetry = Telemetry::new(&builder);
1500
1501 let mut state = AggregationState::new(
1502 BUCKET_WIDTH_SECS,
1503 2,
1504 COUNTER_EXPIRE,
1505 HistogramConfiguration::default(),
1506 telemetry,
1507 );
1508
1509 assert_eq!(recorder.gauge("aggregate_active_contexts"), Some(0.0));
1511 assert_eq!(recorder.counter("aggregate_passthrough_metrics_total"), Some(0));
1512 assert_eq!(
1513 recorder.counter(("component_events_dropped_total", &[("intentional", "true")])),
1514 Some(0)
1515 );
1516 for metric_type in &["counter", "gauge", "rate", "set", "histogram", "distribution"] {
1517 assert_eq!(
1518 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", *metric_type)])),
1519 Some(0.0)
1520 );
1521 }
1522
1523 assert!(state.insert(insert_ts(1), Metric::counter("metric1", 42.0)));
1525 assert_eq!(recorder.gauge("aggregate_active_contexts"), Some(1.0));
1526 assert_eq!(
1527 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", "counter")])),
1528 Some(1.0)
1529 );
1530 assert_eq!(recorder.counter("aggregate_passthrough_metrics_total"), Some(0));
1531
1532 assert!(state.insert(insert_ts(1), Metric::gauge("metric2", (insert_ts(1), 42.0))));
1534 assert_eq!(recorder.gauge("aggregate_active_contexts"), Some(2.0));
1535 assert_eq!(
1536 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", "gauge")])),
1537 Some(1.0)
1538 );
1539
1540 assert!(!state.insert(insert_ts(1), Metric::counter("metric3", 42.0)));
1542 assert_eq!(recorder.gauge("aggregate_active_contexts"), Some(2.0));
1543 assert_eq!(
1544 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", "counter")])),
1545 Some(1.0)
1546 );
1547
1548 let _ = get_flushed_metrics(flush_ts(1), &mut state).await;
1551 assert_eq!(recorder.gauge("aggregate_active_contexts"), Some(1.0));
1552 assert_eq!(
1553 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", "counter")])),
1554 Some(1.0)
1555 );
1556 assert_eq!(
1557 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", "gauge")])),
1558 Some(0.0)
1559 );
1560 }
1561}
1562
1563#[cfg(test)]
1564mod config_smoke {
1565 use datadog_agent_config_testing::config_registry::structs;
1566 use datadog_agent_config_testing::run_config_smoke_tests;
1567 use serde_json::json;
1568
1569 use super::AggregateConfiguration;
1570 use crate::config::{DatadogRemapper, KEY_ALIASES};
1571
1572 #[tokio::test]
1573 async fn smoke_test() {
1574 run_config_smoke_tests(
1577 structs::AGGREGATE_CONFIGURATION,
1578 &[
1579 "aggregate_flush_interval.nanos",
1580 "aggregate_passthrough_idle_flush_timeout.nanos",
1581 ],
1582 json!({}),
1583 |cfg| {
1584 cfg.as_typed::<AggregateConfiguration>()
1585 .expect("AggregateConfiguration should deserialize")
1586 },
1587 KEY_ALIASES,
1588 DatadogRemapper::from_env_vars,
1589 )
1590 .await
1591 }
1592}