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 resource_accounting::{MemoryBounds, MemoryBoundsBuilder, UsageExpr};
10use saluki_common::time::get_unix_timestamp;
11use saluki_config::GenericConfiguration;
12use saluki_context::Context;
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 saluki_antithesis::always_le!(
576 self.contexts.len(),
577 self.context_limit,
578 "aggregate context map within context_limit",
579 { "len": self.contexts.len(), "limit": self.context_limit }
580 );
581
582 if !self.contexts.contains_key(metric.context()) && self.contexts.len() >= self.context_limit {
584 saluki_antithesis::sometimes!(
586 true,
587 "aggregate context limit breached",
588 { "limit": self.context_limit }
589 );
590
591 self.context_limit_breached = true;
592 return false;
593 }
594
595 let (context, mut values, metadata) = metric.into_parts();
596
597 let bucket_ts = align_to_bucket_start(timestamp, self.bucket_width_secs);
602 values.collapse_non_timestamped(bucket_ts);
603
604 trace!(
605 bucket_ts,
606 kind = values.as_str(),
607 "Inserting metric into aggregation state."
608 );
609
610 match self.contexts.entry(context) {
613 Entry::Occupied(mut entry) => {
614 let aggregated = entry.get_mut();
615
616 aggregated.last_seen = timestamp;
618 aggregated.values.merge(values);
619 }
620 Entry::Vacant(entry) => {
621 self.telemetry.increment_contexts(entry.key(), &values);
622
623 entry.insert(AggregatedMetric {
624 values,
625 metadata,
626 last_seen: timestamp,
627 });
628 }
629 }
630
631 true
632 }
633
634 async fn flush(
635 &mut self, current_time: u64, flush_open_buckets: bool, dispatcher: &mut BufferedDispatcher<'_, EventsBuffer>,
636 ) -> Result<(), GenericError> {
637 let bucket_width_secs = self.bucket_width_secs;
638 let counter_expire_secs = self.counter_expire_secs.map(|d| d.get()).unwrap_or(0);
639
640 let split_timestamp = align_to_bucket_start(current_time, bucket_width_secs).saturating_sub(1);
643
644 let mut zero_value_buckets = SmallVec::<[(u64, MetricValues); 4]>::new();
649 if self.last_flush != 0 {
650 let start = align_to_bucket_start(self.last_flush, bucket_width_secs);
651
652 saluki_antithesis::always_ge!(
657 current_time,
658 self.last_flush,
659 "aggregate flush wall-clock did not move backward",
660 { "current_time": current_time, "last_flush": self.last_flush }
661 );
662 saluki_antithesis::always_le!(
665 current_time.saturating_sub(self.last_flush) / bucket_width_secs.get(),
666 10_000,
667 "aggregate zero-value bucket span bounded across a flush",
668 {
669 "current_time": current_time,
670 "last_flush": self.last_flush,
671 "bucket_width_secs": bucket_width_secs.get()
672 }
673 );
674
675 for bucket_start in (start..current_time).step_by(bucket_width_secs.get() as usize) {
676 if is_bucket_closed(current_time, bucket_start, bucket_width_secs, flush_open_buckets) {
677 zero_value_buckets.push((bucket_start, MetricValues::counter((bucket_start, 0.0))));
678 }
679 }
680
681 saluki_antithesis::sometimes!(
683 !zero_value_buckets.is_empty(),
684 "aggregate flush generated zero-value counter buckets",
685 { "count": zero_value_buckets.len() }
686 );
687 }
688
689 debug!(timestamp = current_time, "Flushing buckets.");
691
692 for (context, am) in self.contexts.iter_mut() {
693 let should_expire_if_empty = match &am.values {
702 MetricValues::Counter(..) => {
703 saluki_antithesis::always_le!(
704 am.last_seen,
705 u64::MAX - counter_expire_secs,
706 "aggregate counter expiry add does not overflow",
707 { "last_seen": am.last_seen, "counter_expire_secs": counter_expire_secs }
708 );
709 counter_expire_secs != 0 && am.last_seen.saturating_add(counter_expire_secs) < current_time
710 }
711 _ => true,
712 };
713
714 if let MetricValues::Counter(..) = &mut am.values {
720 let expires_at = am.last_seen.saturating_add(counter_expire_secs);
721 for (zv_bucket_start, zero_value) in &zero_value_buckets {
722 if expires_at > *zv_bucket_start {
723 am.values.merge(zero_value.clone());
724 } else {
725 break;
728 }
729 }
730 }
731
732 if let Some(closed_bucket_values) = am.values.split_at_timestamp(split_timestamp) {
741 self.telemetry.increment_flushed(&closed_bucket_values);
742
743 transform_and_push_metric(
745 context.clone(),
746 closed_bucket_values,
747 am.metadata.clone(),
748 bucket_width_secs,
749 &self.hist_config,
750 dispatcher,
751 )
752 .await?;
753 }
754
755 if am.values.is_empty() && should_expire_if_empty {
756 self.telemetry.decrement_contexts(context, &am.values);
757 self.contexts_remove_buf.push(context.clone());
758 }
759 }
760
761 let contexts_len_before = self.contexts.len();
763 for context in self.contexts_remove_buf.drain(..) {
764 self.contexts.remove(&context);
765 }
766 let contexts_len_after = self.contexts.len();
767
768 let contexts_delta = contexts_len_before.saturating_sub(contexts_len_after);
769 let target_contexts_capacity = contexts_len_after.saturating_add(contexts_delta / 2);
770 self.contexts.shrink_to(target_contexts_capacity);
771
772 if self.context_limit_breached && self.contexts.len() < self.context_limit {
773 self.context_limit_breached = false;
774 }
775
776 self.last_flush = current_time;
777
778 Ok(())
779 }
780
781 fn context_limit_breached(&self) -> bool {
782 self.context_limit_breached
783 }
784}
785
786async fn transform_and_push_metric(
787 context: Context, mut values: MetricValues, metadata: MetricMetadata, bucket_width_secs: NonZeroU64,
788 hist_config: &HistogramConfiguration, dispatcher: &mut BufferedDispatcher<'_, EventsBuffer>,
789) -> Result<(), GenericError> {
790 let bucket_width = Duration::from_secs(bucket_width_secs.get());
791
792 match values {
793 MetricValues::Histogram(ref mut points) => {
796 if hist_config.copy_to_distribution() {
798 let sketch_points = points
799 .into_iter()
800 .map(|(ts, hist)| {
801 let mut sketch = DDSketch::default();
802 for sample in hist.samples() {
803 sketch.insert_n(sample.value.into_inner(), sample.weight.0 as u64);
804 }
805 (ts, sketch)
806 })
807 .collect::<SketchPoints>();
808 let distribution_values = MetricValues::distribution(sketch_points);
809 let metric_context = if !hist_config.copy_to_distribution_prefix().is_empty() {
810 context.with_name(format!(
811 "{}{}",
812 hist_config.copy_to_distribution_prefix(),
813 context.name()
814 ))
815 } else {
816 context.clone()
817 };
818 let new_metric = Metric::from_parts(metric_context, distribution_values, metadata.clone());
819 dispatcher.push(Event::Metric(new_metric)).await?;
820 }
821 let mut sorted_points = Vec::new();
826 for (ts, h) in points {
827 sorted_points.push((ts, h.summary_view()));
828 }
829
830 for statistic in hist_config.statistics() {
831 let new_points = sorted_points
832 .iter()
833 .map(|(ts, hs)| (*ts, statistic.value_from_histogram(hs)))
834 .collect::<ScalarPoints>();
835
836 let new_values = if statistic.is_rate_statistic() {
837 MetricValues::rate(new_points, bucket_width)
838 } else {
839 MetricValues::gauge(new_points)
840 };
841
842 let new_metadata = if matches!(statistic, HistogramStatistic::Count) {
844 metadata.clone().with_unit(MetaString::empty())
845 } else {
846 metadata.clone()
847 };
848
849 let new_context = context.with_name(format!("{}.{}", context.name(), statistic.suffix()));
850 let new_metric = Metric::from_parts(new_context, new_values, new_metadata);
851 dispatcher.push(Event::Metric(new_metric)).await?;
852 }
853
854 Ok(())
855 }
856
857 values => {
860 let adjusted_values = counter_values_to_rate(values, bucket_width_secs);
861
862 let metric = Metric::from_parts(context, adjusted_values, metadata);
863 dispatcher.push(Event::Metric(metric)).await
864 }
865 }
866}
867
868fn counter_values_to_rate(values: MetricValues, interval_secs: NonZeroU64) -> MetricValues {
869 match values {
870 MetricValues::Counter(points) => MetricValues::rate(points, Duration::from_secs(interval_secs.get())),
871 values => values,
872 }
873}
874
875const fn align_to_bucket_start(timestamp: u64, bucket_width_secs: NonZeroU64) -> u64 {
876 timestamp - (timestamp % bucket_width_secs.get())
877}
878
879const fn is_bucket_closed(
880 current_time: u64, bucket_start: u64, bucket_width_secs: NonZeroU64, flush_open_buckets: bool,
881) -> bool {
882 (bucket_start + bucket_width_secs.get() - 1) < current_time || flush_open_buckets
901}
902
903#[cfg(test)]
907mod tests {
908 use float_cmp::ApproxEqRatio as _;
909 use saluki_core::{
910 components::ComponentContext,
911 topology::{interconnect::Dispatcher, ComponentId, OutputName},
912 };
913 use saluki_metrics::test::TestRecorder;
914 use stringtheory::MetaString;
915 use tokio::sync::mpsc;
916
917 use super::config::HistogramStatistic;
918 use super::*;
919
920 const BUCKET_WIDTH_SECS: NonZeroU64 = NonZeroU64::new(10).expect("not zero");
921 const BUCKET_WIDTH: Duration = Duration::from_secs(BUCKET_WIDTH_SECS.get());
922 const COUNTER_EXPIRE_SECS: u64 = 20;
923 const COUNTER_EXPIRE: Option<Duration> = Some(Duration::from_secs(COUNTER_EXPIRE_SECS));
924
925 const fn bucket_ts(step: u64) -> u64 {
927 align_to_bucket_start(insert_ts(step), BUCKET_WIDTH_SECS)
928 }
929
930 const fn insert_ts(step: u64) -> u64 {
932 (BUCKET_WIDTH_SECS.get() * (step + 1)) - 2
933 }
934
935 const fn flush_ts(step: u64) -> u64 {
937 BUCKET_WIDTH_SECS.get() * (step + 1)
938 }
939
940 struct DispatcherReceiver {
941 receiver: mpsc::Receiver<EventsBuffer>,
942 }
943
944 impl DispatcherReceiver {
945 fn collect_next(&mut self) -> Vec<Metric> {
946 match self.receiver.try_recv() {
947 Ok(event_buffer) => {
948 let mut metrics = event_buffer
949 .into_iter()
950 .filter_map(|event| event.try_into_metric())
951 .collect::<Vec<Metric>>();
952
953 metrics.sort_by(|a, b| a.context().name().cmp(b.context().name()));
954 metrics
955 }
956 Err(_) => Vec::new(),
957 }
958 }
959 }
960
961 fn build_basic_dispatcher() -> (EventsDispatcher, DispatcherReceiver) {
963 let component_id = ComponentId::try_from("test").expect("should not fail to create component ID");
964 let mut dispatcher = Dispatcher::new(ComponentContext::transform(component_id));
965
966 let (buffer_tx, buffer_rx) = mpsc::channel(1);
967 dispatcher.add_output(OutputName::Default).unwrap();
968 dispatcher
969 .attach_sender_to_output(&OutputName::Default, buffer_tx)
970 .unwrap();
971
972 (dispatcher, DispatcherReceiver { receiver: buffer_rx })
973 }
974
975 async fn get_flushed_metrics(timestamp: u64, state: &mut AggregationState) -> Vec<Metric> {
976 let (dispatcher, mut dispatcher_receiver) = build_basic_dispatcher();
977 let mut buffered_dispatcher = dispatcher.buffered().expect("default output should always exist");
978
979 state
981 .flush(timestamp, true, &mut buffered_dispatcher)
982 .await
983 .expect("should not fail to flush aggregation state");
984
985 buffered_dispatcher
988 .flush()
989 .await
990 .expect("should not fail to flush buffered sender");
991
992 dispatcher_receiver.collect_next()
993 }
994
995 macro_rules! compare_points {
996 (scalar, $expected:expr, $actual:expr, $error_ratio:literal) => {
997 for (idx, (expected_value, actual_value)) in $expected.into_iter().zip($actual.into_iter()).enumerate() {
998 let (expected_ts, expected_point) = expected_value;
999 let (actual_ts, actual_point) = actual_value;
1000
1001 assert_eq!(
1002 expected_ts, actual_ts,
1003 "timestamp for value #{} does not match: {:?} (expected) vs {:?} (actual)",
1004 idx, expected_ts, actual_ts
1005 );
1006 assert!(
1007 expected_point.approx_eq_ratio(&actual_point, $error_ratio),
1008 "point for value #{} does not match: {} (expected) vs {} (actual)",
1009 idx,
1010 expected_point,
1011 actual_point
1012 );
1013 }
1014 };
1015 (distribution, $expected:expr, $actual:expr) => {
1016 for (idx, (expected_value, actual_value)) in $expected.into_iter().zip($actual.into_iter()).enumerate() {
1017 let (expected_ts, expected_sketch) = expected_value;
1018 let (actual_ts, actual_sketch) = actual_value;
1019
1020 assert_eq!(
1021 expected_ts, actual_ts,
1022 "timestamp for value #{} does not match: {:?} (expected) vs {:?} (actual)",
1023 idx, expected_ts, actual_ts
1024 );
1025 assert_eq!(
1026 expected_sketch, actual_sketch,
1027 "sketch for value #{} does not match: {:?} (expected) vs {:?} (actual)",
1028 idx, expected_sketch, actual_sketch
1029 );
1030 }
1031 };
1032 }
1033
1034 macro_rules! assert_flushed_scalar_metric {
1035 ($original:expr, $actual:expr, [$($ts:expr => $value:expr),+]) => {
1036 assert_flushed_scalar_metric!($original, $actual, [$($ts => $value),+], error_ratio => 0.000001);
1037 };
1038 ($original:expr, $actual:expr, [$($ts:expr => $value:expr),+], error_ratio => $error_ratio:literal) => {
1039 let actual_metric = $actual;
1040
1041 assert_eq!($original.context(), actual_metric.context(), "expected context ({}) and actual context ({}) do not match", $original.context(), actual_metric.context());
1042
1043 let expected_points = ScalarPoints::from([$(($ts, $value)),+]);
1044
1045 match actual_metric.values() {
1046 MetricValues::Counter(ref actual_points) | MetricValues::Gauge(ref actual_points) | MetricValues::Rate(ref actual_points, _) => {
1047 assert_eq!(expected_points.len(), actual_points.len(), "expected and actual values have different number of points");
1048 compare_points!(scalar, expected_points, actual_points, $error_ratio);
1049 },
1050 _ => panic!("only counters, rates, and gauges are supported in assert_flushed_scalar_metric"),
1051 }
1052 };
1053 }
1054
1055 macro_rules! assert_flushed_distribution_metric {
1056 ($original:expr, $actual:expr, [$($ts:expr => $value:expr),+]) => {
1057 assert_flushed_distribution_metric!($original, $actual, [$($ts => $value),+], error_ratio => 0.000001);
1058 };
1059 ($original:expr, $actual:expr, [$($ts:expr => $value:expr),+], error_ratio => $error_ratio:literal) => {
1060 let actual_metric = $actual;
1061
1062 assert_eq!($original.context(), actual_metric.context());
1063
1064 match actual_metric.values() {
1065 MetricValues::Distribution(ref actual_points) => {
1066 let expected_points = SketchPoints::from([$(($ts, $value)),+]);
1067 assert_eq!(expected_points.len(), actual_points.len(), "expected and actual values have different number of points");
1068
1069 compare_points!(distribution, &expected_points, actual_points);
1070 },
1071 _ => panic!("only distributions are supported in assert_flushed_distribution_metric"),
1072 }
1073 };
1074 }
1075
1076 #[test]
1077 fn bucket_is_closed() {
1078 let cases = [
1081 (1000, 995, BUCKET_WIDTH_SECS, false, false),
1083 (1000, 995, BUCKET_WIDTH_SECS, true, true),
1084 (1000, 1000, BUCKET_WIDTH_SECS, false, false),
1086 (1000, 1000, BUCKET_WIDTH_SECS, true, true),
1087 (1010, 1000, BUCKET_WIDTH_SECS, false, true),
1089 (1010, 1000, BUCKET_WIDTH_SECS, true, true),
1090 ];
1091
1092 for (current_time, bucket_start, bucket_width_secs, flush_open_buckets, expected) in cases {
1093 let expected_reason = if expected {
1094 "closed, was open"
1095 } else {
1096 "open, was closed"
1097 };
1098
1099 assert_eq!(
1100 is_bucket_closed(current_time, bucket_start, bucket_width_secs, flush_open_buckets),
1101 expected,
1102 "expected bucket to be {} (current_time={}, bucket_start={}, bucket_width={}, flush_open_buckets={})",
1103 expected_reason,
1104 current_time,
1105 bucket_start,
1106 bucket_width_secs,
1107 flush_open_buckets
1108 );
1109 }
1110 }
1111
1112 #[tokio::test]
1113 async fn context_limit() {
1114 let mut state = AggregationState::new(
1116 BUCKET_WIDTH_SECS,
1117 2,
1118 COUNTER_EXPIRE,
1119 HistogramConfiguration::default(),
1120 Telemetry::noop(),
1121 );
1122
1123 let input_metrics = [
1126 Metric::gauge("metric1", 1.0),
1127 Metric::gauge("metric2", 2.0),
1128 Metric::gauge("metric3", 3.0),
1129 Metric::gauge("metric4", 4.0),
1130 ];
1131
1132 assert!(!state.context_limit_breached());
1133
1134 assert!(state.insert(insert_ts(1), input_metrics[0].clone()));
1135 assert!(state.insert(insert_ts(1), input_metrics[1].clone()));
1136 assert!(!state.context_limit_breached());
1137
1138 assert!(!state.insert(insert_ts(1), input_metrics[2].clone()));
1139 assert!(state.context_limit_breached());
1140 assert!(!state.insert(insert_ts(1), input_metrics[3].clone()));
1141 assert!(state.context_limit_breached());
1142
1143 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1146 assert_eq!(flushed_metrics.len(), 2);
1147 assert_eq!(input_metrics[0].context(), flushed_metrics[0].context());
1148 assert_eq!(input_metrics[1].context(), flushed_metrics[1].context());
1149 assert!(!state.context_limit_breached());
1150
1151 assert!(state.insert(insert_ts(2), input_metrics[2].clone()));
1154 assert!(state.insert(insert_ts(2), input_metrics[3].clone()));
1155
1156 let flushed_metrics = get_flushed_metrics(flush_ts(2), &mut state).await;
1157 assert_eq!(flushed_metrics.len(), 2);
1158 assert_eq!(input_metrics[2].context(), flushed_metrics[0].context());
1159 assert_eq!(input_metrics[3].context(), flushed_metrics[1].context());
1160 }
1161
1162 #[tokio::test]
1163 async fn context_limit_with_zero_value_counters() {
1164 let mut state = AggregationState::new(
1166 BUCKET_WIDTH_SECS,
1167 2,
1168 COUNTER_EXPIRE,
1169 HistogramConfiguration::default(),
1170 Telemetry::noop(),
1171 );
1172
1173 let input_metrics = [
1175 Metric::counter("metric1", 1.0),
1176 Metric::counter("metric2", 2.0),
1177 Metric::counter("metric3", 3.0),
1178 ];
1179
1180 assert!(state.insert(insert_ts(1), input_metrics[0].clone()));
1181 assert!(state.insert(insert_ts(1), input_metrics[1].clone()));
1182
1183 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1185 assert_eq!(flushed_metrics.len(), 2);
1186 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(1) => 1.0]);
1187 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(1) => 2.0]);
1188
1189 let flushed_metrics = get_flushed_metrics(flush_ts(2), &mut state).await;
1191 assert_eq!(flushed_metrics.len(), 2);
1192 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(2) => 0.0]);
1193 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(2) => 0.0]);
1194
1195 assert!(!state.insert(insert_ts(3), input_metrics[2].clone()));
1197
1198 let flushed_metrics = get_flushed_metrics(flush_ts(3), &mut state).await;
1200 assert_eq!(flushed_metrics.len(), 2);
1201 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(3) => 0.0]);
1202 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(3) => 0.0]);
1203
1204 let flushed_metrics = get_flushed_metrics(flush_ts(4), &mut state).await;
1207 assert_eq!(flushed_metrics.len(), 0);
1208
1209 assert!(state.insert(insert_ts(5), input_metrics[2].clone()));
1211
1212 let flushed_metrics = get_flushed_metrics(flush_ts(5), &mut state).await;
1213 assert_eq!(flushed_metrics.len(), 1);
1214 assert_flushed_scalar_metric!(&input_metrics[2], &flushed_metrics[0], [bucket_ts(5) => 3.0]);
1215 }
1216
1217 #[tokio::test]
1218 async fn zero_value_counters() {
1219 let mut state = AggregationState::new(
1221 BUCKET_WIDTH_SECS,
1222 10,
1223 COUNTER_EXPIRE,
1224 HistogramConfiguration::default(),
1225 Telemetry::noop(),
1226 );
1227
1228 let input_metrics = [Metric::counter("metric1", 1.0), Metric::counter("metric2", 2.0)];
1230
1231 assert!(state.insert(insert_ts(1), input_metrics[0].clone()));
1232 assert!(state.insert(insert_ts(1), input_metrics[1].clone()));
1233
1234 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1236 assert_eq!(flushed_metrics.len(), 2);
1237 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(1) => 1.0]);
1238 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(1) => 2.0]);
1239
1240 let flushed_metrics = get_flushed_metrics(flush_ts(2), &mut state).await;
1242 assert_eq!(flushed_metrics.len(), 2);
1243 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(2) => 0.0]);
1244 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(2) => 0.0]);
1245
1246 assert!(state.insert(insert_ts(4), input_metrics[0].clone()));
1248 assert!(state.insert(insert_ts(4), input_metrics[1].clone()));
1249
1250 let flushed_metrics = get_flushed_metrics(flush_ts(4), &mut state).await;
1253 assert_eq!(flushed_metrics.len(), 2);
1254 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(3) => 0.0, bucket_ts(4) => 1.0]);
1255 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(3) => 0.0, bucket_ts(4) => 2.0]);
1256
1257 let flushed_metrics = get_flushed_metrics(flush_ts(7), &mut state).await;
1261 assert_eq!(flushed_metrics.len(), 2);
1262 assert_flushed_scalar_metric!(&input_metrics[0], &flushed_metrics[0], [bucket_ts(5) => 0.0, bucket_ts(6) => 0.0]);
1263 assert_flushed_scalar_metric!(&input_metrics[1], &flushed_metrics[1], [bucket_ts(5) => 0.0, bucket_ts(6) => 0.0]);
1264 }
1265
1266 #[tokio::test]
1267 async fn merge_identical_timestamped_values_on_flush() {
1268 let mut state = AggregationState::new(
1270 BUCKET_WIDTH_SECS,
1271 10,
1272 COUNTER_EXPIRE,
1273 HistogramConfiguration::default(),
1274 Telemetry::noop(),
1275 );
1276
1277 let input_metric = Metric::counter("metric1", [1.0, 2.0, 3.0, 4.0, 5.0]);
1279
1280 assert!(state.insert(insert_ts(1), input_metric.clone()));
1281
1282 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1285 assert_eq!(flushed_metrics.len(), 1);
1286 assert_flushed_scalar_metric!(&input_metric, &flushed_metrics[0], [bucket_ts(1) => 15.0]);
1287 }
1288
1289 #[tokio::test]
1290 async fn histogram_statistics() {
1291 let hist_config = HistogramConfiguration::from_statistics(
1293 &[
1294 HistogramStatistic::Count,
1295 HistogramStatistic::Sum,
1296 HistogramStatistic::Percentile {
1297 q: 0.5,
1298 suffix: "p50".into(),
1299 },
1300 ],
1301 false,
1302 "".into(),
1303 );
1304 let mut state = AggregationState::new(BUCKET_WIDTH_SECS, 10, COUNTER_EXPIRE, hist_config, Telemetry::noop());
1305
1306 let input_metric = Metric::histogram("metric1", [1.0, 2.0, 3.0, 4.0, 5.0]);
1308 assert!(state.insert(insert_ts(1), input_metric.clone()));
1309
1310 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1313 assert_eq!(flushed_metrics.len(), 3);
1314
1315 let count_metric = Metric::rate("metric1.count", 0.0, Duration::from_secs(BUCKET_WIDTH_SECS.get()));
1318 let sum_metric = Metric::gauge("metric1.sum", 0.0);
1319 let p50_metric = Metric::gauge("metric1.p50", 0.0);
1320
1321 assert_flushed_scalar_metric!(count_metric, &flushed_metrics[0], [bucket_ts(1) => 5.0]);
1324 assert_flushed_scalar_metric!(p50_metric, &flushed_metrics[1], [bucket_ts(1) => 3.0], error_ratio => 0.0025);
1325 assert_flushed_scalar_metric!(sum_metric, &flushed_metrics[2], [bucket_ts(1) => 15.0]);
1326 }
1327
1328 #[tokio::test]
1329 async fn histogram_statistics_unit_propagation() {
1330 let hist_config = HistogramConfiguration::from_statistics(
1332 &[
1333 HistogramStatistic::Count,
1334 HistogramStatistic::Sum,
1335 HistogramStatistic::Percentile {
1336 q: 0.5,
1337 suffix: "p50".into(),
1338 },
1339 ],
1340 false,
1341 "".into(),
1342 );
1343 let mut state = AggregationState::new(BUCKET_WIDTH_SECS, 10, COUNTER_EXPIRE, hist_config, Telemetry::noop());
1344
1345 let context = Context::from_static_parts("metric1", &[]);
1347 let metadata = MetricMetadata::default().with_unit(MetaString::from_static("millisecond"));
1348 let input_metric = Metric::from_parts(
1349 context,
1350 MetricValues::histogram([1.0_f64, 2.0, 3.0, 4.0, 5.0]),
1351 metadata,
1352 );
1353 assert!(state.insert(insert_ts(1), input_metric));
1354
1355 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1356 assert_eq!(flushed_metrics.len(), 3);
1357
1358 for metric in &flushed_metrics {
1361 let name = metric.context().name();
1362 if name.ends_with(".count") {
1363 assert_eq!(
1364 metric.metadata().unit(),
1365 None,
1366 "flushed metric '{}' should be dimensionless",
1367 name
1368 );
1369 } else {
1370 assert_eq!(
1371 metric.metadata().unit(),
1372 Some("millisecond"),
1373 "flushed metric '{}' should carry unit='millisecond'",
1374 name
1375 );
1376 }
1377 }
1378 }
1379
1380 #[tokio::test]
1381 async fn distributions() {
1382 let mut state = AggregationState::new(
1384 BUCKET_WIDTH_SECS,
1385 10,
1386 COUNTER_EXPIRE,
1387 HistogramConfiguration::default(),
1388 Telemetry::noop(),
1389 );
1390
1391 let values = [1.0, 2.0, 3.0, 4.0, 5.0];
1393 let input_metric = Metric::distribution("metric1", &values[..]);
1394
1395 assert!(state.insert(insert_ts(1), input_metric.clone()));
1396
1397 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1399 assert_eq!(flushed_metrics.len(), 1);
1400
1401 assert_flushed_distribution_metric!(&input_metric, &flushed_metrics[0], [bucket_ts(1) => &values[..]]);
1402 }
1403
1404 #[tokio::test]
1405 async fn histogram_copy_to_distribution() {
1406 let hist_config = HistogramConfiguration::from_statistics(
1407 &[
1408 HistogramStatistic::Count,
1409 HistogramStatistic::Sum,
1410 HistogramStatistic::Percentile {
1411 q: 0.5,
1412 suffix: "p50".into(),
1413 },
1414 ],
1415 true,
1416 "dist_prefix.".into(),
1417 );
1418 let mut state = AggregationState::new(BUCKET_WIDTH_SECS, 10, COUNTER_EXPIRE, hist_config, Telemetry::noop());
1419
1420 let values = [1.0, 2.0, 3.0, 4.0, 5.0];
1422 let input_metric = Metric::histogram("metric1", values);
1423 assert!(state.insert(insert_ts(1), input_metric.clone()));
1424
1425 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1428 assert_eq!(flushed_metrics.len(), 4);
1429
1430 let count_metric = Metric::rate("metric1.count", 0.0, BUCKET_WIDTH);
1433 let sum_metric = Metric::gauge("metric1.sum", 0.0);
1434 let p50_metric = Metric::gauge("metric1.p50", 0.0);
1435 let expected_distribution = Metric::distribution("dist_prefix.metric1", &values[..]);
1436
1437 assert_flushed_distribution_metric!(expected_distribution, &flushed_metrics[0], [bucket_ts(1) => &values[..]]);
1440 assert_flushed_scalar_metric!(count_metric, &flushed_metrics[1], [bucket_ts(1) => 5.0]);
1441 assert_flushed_scalar_metric!(p50_metric, &flushed_metrics[2], [bucket_ts(1) => 3.0], error_ratio => 0.0025);
1442 assert_flushed_scalar_metric!(sum_metric, &flushed_metrics[3], [bucket_ts(1) => 15.0]);
1443 }
1444
1445 #[tokio::test]
1446 async fn nonaggregated_counters_to_rate() {
1447 let counter_value = 42.0;
1448
1449 let mut state = AggregationState::new(
1451 BUCKET_WIDTH_SECS,
1452 10,
1453 COUNTER_EXPIRE,
1454 HistogramConfiguration::default(),
1455 Telemetry::noop(),
1456 );
1457
1458 let input_metric = Metric::counter("metric1", counter_value);
1460 assert!(state.insert(insert_ts(1), input_metric.clone()));
1461
1462 let flushed_metrics = get_flushed_metrics(flush_ts(1), &mut state).await;
1465 assert_eq!(flushed_metrics.len(), 1);
1466 let flushed_metric = &flushed_metrics[0];
1467
1468 assert_flushed_scalar_metric!(&input_metric, flushed_metric, [bucket_ts(1) => counter_value]);
1469 assert_eq!(flushed_metric.values().as_str(), "rate");
1470 }
1471
1472 #[tokio::test]
1473 async fn preaggregated_counters_to_rate() {
1474 let counter_value = 42.0;
1475 let timestamp = 123456;
1476
1477 let mut batcher = PassthroughBatcher::new(Duration::from_nanos(1), BUCKET_WIDTH_SECS, Telemetry::noop()).await;
1479 let (dispatcher, mut dispatcher_receiver) = build_basic_dispatcher();
1480
1481 let input_metric = Metric::counter("metric1", (timestamp, counter_value));
1483 batcher.push_metric(input_metric.clone(), &dispatcher).await;
1484
1485 batcher.try_flush(&dispatcher).await;
1488
1489 let mut flushed_metrics = dispatcher_receiver.collect_next();
1490 assert_eq!(flushed_metrics.len(), 1);
1491 assert_eq!(
1492 Metric::rate("metric1", (timestamp, counter_value), BUCKET_WIDTH),
1493 flushed_metrics.remove(0)
1494 );
1495 }
1496
1497 #[tokio::test]
1498 async fn telemetry() {
1499 let recorder = TestRecorder::default();
1506 let _local = metrics::set_default_local_recorder(&recorder);
1507
1508 let builder = MetricsBuilder::default();
1509 let telemetry = Telemetry::new(&builder);
1510
1511 let mut state = AggregationState::new(
1512 BUCKET_WIDTH_SECS,
1513 2,
1514 COUNTER_EXPIRE,
1515 HistogramConfiguration::default(),
1516 telemetry,
1517 );
1518
1519 assert_eq!(recorder.gauge("aggregate_active_contexts"), Some(0.0));
1521 assert_eq!(recorder.counter("aggregate_passthrough_metrics_total"), Some(0));
1522 assert_eq!(
1523 recorder.counter(("component_events_dropped_total", &[("intentional", "true")])),
1524 Some(0)
1525 );
1526 for metric_type in &["counter", "gauge", "rate", "set", "histogram", "distribution"] {
1527 assert_eq!(
1528 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", *metric_type)])),
1529 Some(0.0)
1530 );
1531 }
1532
1533 assert!(state.insert(insert_ts(1), Metric::counter("metric1", 42.0)));
1535 assert_eq!(recorder.gauge("aggregate_active_contexts"), Some(1.0));
1536 assert_eq!(
1537 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", "counter")])),
1538 Some(1.0)
1539 );
1540 assert_eq!(recorder.counter("aggregate_passthrough_metrics_total"), Some(0));
1541
1542 assert!(state.insert(insert_ts(1), Metric::gauge("metric2", (insert_ts(1), 42.0))));
1544 assert_eq!(recorder.gauge("aggregate_active_contexts"), Some(2.0));
1545 assert_eq!(
1546 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", "gauge")])),
1547 Some(1.0)
1548 );
1549
1550 assert!(!state.insert(insert_ts(1), Metric::counter("metric3", 42.0)));
1552 assert_eq!(recorder.gauge("aggregate_active_contexts"), Some(2.0));
1553 assert_eq!(
1554 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", "counter")])),
1555 Some(1.0)
1556 );
1557
1558 let _ = get_flushed_metrics(flush_ts(1), &mut state).await;
1561 assert_eq!(recorder.gauge("aggregate_active_contexts"), Some(1.0));
1562 assert_eq!(
1563 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", "counter")])),
1564 Some(1.0)
1565 );
1566 assert_eq!(
1567 recorder.gauge(("aggregate_active_contexts_by_type", &[("metric_type", "gauge")])),
1568 Some(0.0)
1569 );
1570 }
1571}
1572
1573#[cfg(test)]
1574mod config_smoke {
1575 use datadog_agent_config_testing::config_registry::structs;
1576 use datadog_agent_config_testing::run_config_smoke_tests;
1577 use serde_json::json;
1578
1579 use super::AggregateConfiguration;
1580 use crate::config::{DatadogRemapper, KEY_ALIASES};
1581
1582 #[tokio::test]
1583 async fn smoke_test() {
1584 run_config_smoke_tests(
1587 structs::AGGREGATE_CONFIGURATION,
1588 &[
1589 "aggregate_flush_interval.nanos",
1590 "aggregate_passthrough_idle_flush_timeout.nanos",
1591 ],
1592 json!({}),
1593 |cfg| {
1594 cfg.as_typed::<AggregateConfiguration>()
1595 .expect("AggregateConfiguration should deserialize")
1596 },
1597 KEY_ALIASES,
1598 DatadogRemapper::new,
1599 )
1600 .await
1601 }
1602}