1use std::{collections::HashMap, collections::VecDeque, ops::Range, sync::Arc, time::Duration};
2
3use agent_data_plane_config::shared::{SharedConfiguration, V3SeriesMode};
4use async_trait::async_trait;
5use ddsketch::DDSketch;
6use http::{HeaderValue, Method, Request};
7use protobuf::{rt::WireType, CodedOutputStream};
8use saluki_common::{
9 buf::{ChunkedBytesBuffer, FrozenChunkedBytesBuffer},
10 iter::ReusableDeduplicator,
11};
12use saluki_core::{
13 accounting::{MemoryBounds, MemoryBoundsBuilder},
14 components::{encoders::*, BuildContext},
15 data_model::{
16 event::{
17 metric::{Metric, MetricOrigin, MetricValues},
18 EventType,
19 },
20 payload::{HttpPayload, Payload, PayloadMetadata, PayloadType},
21 tags::{SharedTagSet, Tag},
22 },
23 observability::ComponentMetricsExt as _,
24 runtime,
25 topology::{EventsBuffer, PayloadsBuffer, PayloadsDispatcher},
26};
27use saluki_error::{generic_error, ErrorContext as _, GenericError};
28use saluki_io::compression::{CompressionScheme, Compressor};
29use saluki_metrics::MetricsBuilder;
30use tokio::{io::AsyncWriteExt as _, select, sync::mpsc, time::sleep};
31use tracing::{debug, error, warn};
32use url::Url;
33
34use self::v3::{
35 V3EncodedMetrics, V3EncodedRequest, V3EncoderStats, V3MetricType, V3PayloadLimits, V3PayloadRequest,
36 V3PayloadSplitReason, V3SerializerTelemetry, V3Writer,
37};
38use crate::{
39 common::datadog::{
40 clamp_payload_limits,
41 config::OpwMetricsConfiguration,
42 endpoints::{
43 resolve_additional_endpoints, series_v3_config_can_enable_v3, EndpointV3Settings, ResolvedEndpoint,
44 V3EndpointConfig,
45 },
46 io::RB_BUFFER_CHUNK_SIZE,
47 protocol::{MetricsPayloadInfo, UseV3ApiConfig, UseV3ApiSeriesConfig, V3ApiConfig},
48 request_builder::{RequestBuilder, RequestBuilderError},
49 routing::{MetricsRoutingTargets, RoutingTargetCatalog, RoutingTargetKind, RoutingTargetSet},
50 telemetry::ComponentTelemetry,
51 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT, DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT, METRICS_SERIES_V3_PATH,
52 METRICS_SKETCHES_V3_PATH,
53 },
54 encoders::datadog::metrics::v2::MetricsEndpointEncoder,
55};
56
57mod endpoint;
58use self::endpoint::{EndpointConfiguration, MetricsEndpoint};
59
60mod v1;
61mod v2;
62mod v3;
63
64const V3_SKETCHES_ENDPOINT_URI: &str = METRICS_SKETCHES_V3_PATH;
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68enum MetricsEncoderMode {
69 V2Only,
71 V3Enabled,
74}
75
76impl MetricsEncoderMode {
77 fn from_config(use_v3: bool) -> Self {
78 match use_v3 {
79 false => Self::V2Only,
80 true => Self::V3Enabled,
81 }
82 }
83
84 fn needs_v3(self) -> bool {
85 matches!(self, Self::V3Enabled)
86 }
87
88 fn needs_tagging(self) -> bool {
89 matches!(self, Self::V3Enabled)
90 }
91}
92
93fn metrics_encoder_mode_for_config(use_v3: bool, metrics_v3_disabled_by_compressor: bool) -> MetricsEncoderMode {
94 let use_v3 = use_v3 && !metrics_v3_disabled_by_compressor;
95 MetricsEncoderMode::from_config(use_v3)
96}
97
98fn selected_metrics_primary_v3_override(opw_metrics: &OpwMetricsConfiguration) -> Option<bool> {
99 selected_metrics_primary_endpoint(opw_metrics).map(|(_, use_v3_series)| use_v3_series)
100}
101
102fn selected_metrics_primary_endpoint(opw_metrics: &OpwMetricsConfiguration) -> Option<(&str, bool)> {
103 let selected = opw_metrics.selected_endpoint()?;
104 let url = selected.url.trim();
105 metrics_primary_url_can_resolve(url).then_some((url, selected.use_v3_series))
106}
107
108fn metrics_primary_url_can_resolve(url: &str) -> bool {
109 if url.is_empty() {
110 return false;
111 }
112 if url.starts_with("http://") || url.starts_with("https://") {
113 Url::parse(url).is_ok_and(|url| url.host_str().is_some())
114 } else {
115 Url::parse(&format!("https://{url}")).is_ok_and(|url| url.host_str().is_some())
116 }
117}
118
119fn series_v3_can_be_enabled_for_config(
120 use_v2_api_series: bool, metrics_primary_v3_override: Option<bool>, has_additional_endpoints: bool,
121 series_config: &UseV3ApiSeriesConfig,
122) -> bool {
123 use_v2_api_series
124 && (metrics_primary_v3_override == Some(true)
125 || ((metrics_primary_v3_override != Some(false) || has_additional_endpoints)
126 && series_v3_config_can_enable_v3(series_config)))
127}
128
129#[derive(Clone)]
133#[cfg_attr(test, derive(Debug, PartialEq))]
134pub struct DatadogMetricsConfiguration {
135 max_metrics_per_payload: usize,
139
140 max_payload_size: usize,
148
149 max_uncompressed_payload_size: usize,
157
158 max_series_payload_size: usize,
165
166 max_series_uncompressed_payload_size: usize,
174
175 max_series_points_per_payload: usize,
182
183 flush_timeout: Duration,
191
192 compressor_kind: String,
194
195 zstd_compressor_level: i32,
197
198 use_v2_series_api: bool,
204
205 log_payloads: bool,
209
210 additional_tags: Option<SharedTagSet>,
212
213 v3_api: V3ApiConfig,
217
218 use_v3_api: UseV3ApiConfig,
220
221 opw_metrics: OpwMetricsConfiguration,
223
224 primary_endpoint: String,
226
227 additional_endpoints: HashMap<String, Vec<String>>,
229
230 routing: Option<MetricsRouting>,
234}
235
236#[derive(Clone)]
238#[cfg_attr(test, derive(Debug, PartialEq))]
239struct MetricsRouting {
240 catalog: Arc<RoutingTargetCatalog>,
241 targets: Arc<MetricsRoutingTargets>,
242}
243
244impl DatadogMetricsConfiguration {
245 pub fn from_configuration(shared: &SharedConfiguration) -> Self {
247 let endpoints = &shared.endpoints;
248 let metrics = &shared.metrics_encoding;
249
250 Self {
251 max_metrics_per_payload: metrics.max_metrics_per_payload,
252 max_payload_size: metrics.max_payload_size,
253 max_uncompressed_payload_size: metrics.max_uncompressed_payload_size,
254 max_series_payload_size: metrics.max_series_payload_size,
255 max_series_uncompressed_payload_size: metrics.max_series_uncompressed_payload_size,
256 max_series_points_per_payload: metrics.max_series_points_per_payload,
257 flush_timeout: metrics.flush_timeout,
258 compressor_kind: endpoints.compression.compressor_kind.clone(),
259 zstd_compressor_level: endpoints.compression.effective_zstd_level(),
260 use_v2_series_api: metrics.use_v2_series_api,
261 log_payloads: metrics.log_payloads,
262 additional_tags: None,
263 v3_api: (&metrics.v3_api).into(),
264 use_v3_api: UseV3ApiConfig { series: metrics.into() },
265 opw_metrics: OpwMetricsConfiguration::from_configuration(endpoints),
266 primary_endpoint: endpoints.primary_endpoint(),
267 additional_endpoints: endpoints.additional_endpoints.clone(),
268 routing: None,
269 }
270 }
271
272 pub fn with_additional_tags(mut self, additional_tags: SharedTagSet) -> Self {
274 self.additional_tags = Some(additional_tags);
275 self
276 }
277
278 pub fn with_routing(mut self, catalog: Arc<RoutingTargetCatalog>, targets: RoutingTargetSet) -> Self {
283 self.routing = Some(MetricsRouting {
284 catalog,
285 targets: Arc::new(MetricsRoutingTargets::new(targets)),
286 });
287 self
288 }
289
290 pub fn with_metrics_endpoint_override(mut self, dd_url: String) -> Self {
295 self.primary_endpoint = dd_url;
296 self.additional_endpoints = HashMap::new();
297 self.opw_metrics.disable();
298 self
299 }
300
301 pub fn with_v2_series_only(mut self) -> Self {
305 self.use_v3_api.series.enabled = V3SeriesMode::Disabled;
306 self.use_v3_api.series.endpoints.clear();
307 self.opw_metrics.clear_v3_series_overrides();
308 self
309 }
310
311 fn v3_payload_limits(&self) -> V3PayloadLimits {
312 V3PayloadLimits::new(
313 self.max_series_payload_size,
314 self.max_series_uncompressed_payload_size,
315 self.max_metrics_per_payload,
316 self.max_series_points_per_payload,
317 )
318 }
319
320 fn endpoint_v3_settings(
321 &self, configured_endpoint: &str, metrics_primary_v3_override: Option<bool>,
322 ) -> EndpointV3Settings {
323 EndpointV3Settings::from_v3_config(V3EndpointConfig {
324 configured_endpoint,
325 series_config: &self.use_v3_api.series,
326 metrics_primary_v3_override,
327 serializer_v3_sketches_endpoints: &self.v3_api.sketches.endpoints,
328 })
329 }
330
331 fn primary_stream_v3_settings(&self) -> Result<EndpointV3Settings, GenericError> {
336 if let Some((metrics_primary_url, metrics_primary_v3_override)) =
337 selected_metrics_primary_endpoint(&self.opw_metrics)
338 {
339 let metrics_primary = ResolvedEndpoint::from_raw_endpoint(metrics_primary_url, "")
340 .error_context("Failed parsing/resolving the metrics primary destination endpoint.")?;
341 Ok(self.endpoint_v3_settings(metrics_primary.configured_endpoint(), Some(metrics_primary_v3_override)))
342 } else {
343 let primary = ResolvedEndpoint::from_raw_endpoint(&self.primary_endpoint, "")
344 .error_context("Failed parsing/resolving the primary destination endpoint.")?;
345 Ok(self.endpoint_v3_settings(primary.configured_endpoint(), None))
346 }
347 }
348
349 fn any_series_endpoint_matches(
350 &self, mut predicate: impl FnMut(&EndpointV3Settings) -> bool,
351 ) -> Result<bool, GenericError> {
352 if let Some(routing) = &self.routing {
353 for target in routing.targets.targets().iter() {
354 let Some(target) = routing.catalog.get(target) else {
355 continue;
356 };
357 let settings = match target.kind() {
358 RoutingTargetKind::Primary => self.primary_stream_v3_settings()?,
359 RoutingTargetKind::Additional => self.endpoint_v3_settings(target.configured_endpoint(), None),
360 };
361 if predicate(&settings) {
362 return Ok(true);
363 }
364 }
365
366 return Ok(false);
367 }
368
369 if predicate(&self.primary_stream_v3_settings()?) {
370 return Ok(true);
371 }
372
373 for endpoint in resolve_additional_endpoints(&self.additional_endpoints)
374 .error_context("Failed parsing/resolving the additional destination endpoints.")?
375 {
376 let settings = self.endpoint_v3_settings(endpoint.configured_endpoint(), None);
377 if predicate(&settings) {
378 return Ok(true);
379 }
380 }
381
382 Ok(false)
383 }
384
385 fn requires_v2_series(&self, metrics_v3_disabled_by_compressor: bool) -> Result<bool, GenericError> {
386 if !self.use_v2_series_api || metrics_v3_disabled_by_compressor {
387 return Ok(true);
388 }
389
390 self.any_series_endpoint_matches(|settings| !settings.use_v3_series)
391 }
392
393 fn requires_v3_series(&self, metrics_v3_disabled_by_compressor: bool) -> Result<bool, GenericError> {
394 if metrics_v3_disabled_by_compressor {
395 return Ok(false);
396 }
397
398 let metrics_primary_v3_override = selected_metrics_primary_v3_override(&self.opw_metrics);
399 if !series_v3_can_be_enabled_for_config(
400 self.use_v2_series_api,
401 metrics_primary_v3_override,
402 !self.additional_endpoints.is_empty(),
403 &self.use_v3_api.series,
404 ) {
405 return Ok(false);
406 }
407
408 self.any_series_endpoint_matches(|settings| settings.use_v3_series)
409 }
410}
411
412#[async_trait]
413impl EncoderBuilder for DatadogMetricsConfiguration {
414 fn input_event_type(&self) -> EventType {
415 EventType::Metric
416 }
417
418 fn output_payload_type(&self) -> PayloadType {
419 PayloadType::Http
420 }
421
422 async fn build(&self, context: BuildContext) -> Result<Box<dyn Encoder + Send>, GenericError> {
423 let metrics_builder = MetricsBuilder::from_component_context(context.component_context());
424 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
425 let v3_serializer_telemetry = V3SerializerTelemetry::from_builder(&metrics_builder);
426
427 let v2_compression_scheme = CompressionScheme::new(&self.compressor_kind, self.zstd_compressor_level);
428 let v3_compression_scheme = if self.v3_api.compression_level > 0 {
429 CompressionScheme::new(&self.compressor_kind, self.v3_api.compression_level)
430 } else {
431 v2_compression_scheme
432 };
433 let series_endpoint_uri = METRICS_SERIES_V3_PATH.to_string();
434 let payload_limits = self.v3_payload_limits();
435
436 let v2_endpoint_config = EndpointConfiguration::new(
437 v2_compression_scheme,
438 self.max_metrics_per_payload,
439 self.max_series_points_per_payload,
440 self.additional_tags.clone(),
441 );
442 let endpoint_config = EndpointConfiguration::new(
443 v3_compression_scheme,
444 self.max_metrics_per_payload,
445 usize::MAX,
448 self.additional_tags.clone(),
449 );
450
451 let metrics_v3_disabled_by_compressor = matches!(v3_compression_scheme, CompressionScheme::Zlib(_));
453 let use_v3_series = self.requires_v3_series(metrics_v3_disabled_by_compressor)?;
454 let series_mode = metrics_encoder_mode_for_config(use_v3_series, metrics_v3_disabled_by_compressor);
455 let sketches_mode =
456 metrics_encoder_mode_for_config(self.v3_api.use_v3_sketches(), metrics_v3_disabled_by_compressor);
457 let series_endpoint = if self.use_v2_series_api {
458 MetricsEndpoint::SeriesV2
459 } else {
460 MetricsEndpoint::SeriesV1
461 };
462 let v3_runtime_config = V3RuntimeConfig {
463 endpoint_config,
464 payload_limits,
465 series_endpoint_uri,
466 serializer_telemetry: v3_serializer_telemetry,
467 };
468 let generic_payload_limits = clamp_payload_limits(
469 self.max_uncompressed_payload_size,
470 self.max_payload_size,
471 DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT,
472 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT,
473 );
474 let (series_uncompressed_limit, series_compressed_limit) = if series_endpoint == MetricsEndpoint::SeriesV2 {
475 clamp_payload_limits(
476 self.max_series_uncompressed_payload_size,
477 self.max_series_payload_size,
478 v2::SERIES_V2_UNCOMPRESSED_SIZE_LIMIT,
479 v2::SERIES_V2_COMPRESSED_SIZE_LIMIT,
480 )
481 } else {
482 generic_payload_limits
483 };
484 let v2_series_builder = if self.requires_v2_series(metrics_v3_disabled_by_compressor)? {
485 let mut builder = v2::create_v2_request_builder(series_endpoint, &v2_endpoint_config)
486 .await
487 .error_context("Failed to create V2 series request builder.")?;
488 builder.with_len_limits(series_uncompressed_limit, series_compressed_limit)?;
489 Some(builder)
490 } else {
491 debug!("All metrics series endpoints use authoritative V3; disabling V2 series encoding.");
492 None
493 };
494
495 let (sketches_uncompressed_limit, sketches_compressed_limit) = generic_payload_limits;
496 let mut v2_sketch_builder = v2::create_v2_request_builder(MetricsEndpoint::Sketches, &v2_endpoint_config)
497 .await
498 .error_context("Failed to create V2 sketches request builder.")?;
499 v2_sketch_builder.with_len_limits(sketches_uncompressed_limit, sketches_compressed_limit)?;
500 let v2_sketch_builder = Some(v2_sketch_builder);
501
502 let flush_timeout = if self.flush_timeout.is_zero() {
503 Duration::from_millis(10)
506 } else {
507 self.flush_timeout
508 };
509
510 if series_mode.needs_v3() || sketches_mode.needs_v3() {
511 debug!(
512 ?series_mode,
513 ?sketches_mode,
514 v3_sketches_endpoints = ?self.v3_api.sketches.endpoints,
515 "V3 encoding support is enabled."
516 );
517 }
518
519 Ok(Box::new(DatadogMetrics {
520 v2_series_builder,
521 v2_sketch_builder,
522 series_mode,
523 sketches_mode,
524 v3_runtime_config,
525 telemetry,
526 flush_timeout,
527 log_payloads: self.log_payloads,
528 routing_targets: self.routing.as_ref().map(|routing| Arc::clone(&routing.targets)),
529 }))
530 }
531}
532
533impl MemoryBounds for DatadogMetricsConfiguration {
534 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
535 builder
543 .minimum()
544 .with_single_value::<DatadogMetrics>("component struct")
545 .with_array::<EventsBuffer>("request builder events channel", 8)
546 .with_array::<PayloadsBuffer>("request builder payloads channel", 8);
547
548 builder
549 .firm()
550 .with_array::<Metric>("series metrics split re-encode buffer", self.max_metrics_per_payload)
553 .with_array::<Metric>("sketch metrics split re-encode buffer", self.max_metrics_per_payload);
554 }
555}
556
557pub struct DatadogMetrics {
558 v2_series_builder: Option<RequestBuilder<v2::MetricsEndpointEncoder>>,
559 v2_sketch_builder: Option<RequestBuilder<v2::MetricsEndpointEncoder>>,
560 series_mode: MetricsEncoderMode,
561 sketches_mode: MetricsEncoderMode,
562 v3_runtime_config: V3RuntimeConfig,
563 telemetry: ComponentTelemetry,
564 flush_timeout: Duration,
565 log_payloads: bool,
566 routing_targets: Option<Arc<MetricsRoutingTargets>>,
567}
568
569struct V3RuntimeConfig {
570 endpoint_config: EndpointConfiguration,
571 payload_limits: V3PayloadLimits,
572 series_endpoint_uri: String,
573 serializer_telemetry: V3SerializerTelemetry,
574}
575
576#[async_trait]
577impl Encoder for DatadogMetrics {
578 async fn run(mut self: Box<Self>, mut context: EncoderContext) -> Result<(), GenericError> {
579 let Self {
580 v2_series_builder,
581 v2_sketch_builder,
582 series_mode,
583 sketches_mode,
584 v3_runtime_config,
585 telemetry,
586 flush_timeout,
587 log_payloads,
588 routing_targets,
589 } = *self;
590
591 let mut health = context.take_health_handle();
592
593 let (events_tx, events_rx) = mpsc::channel(8);
594 let (payloads_tx, mut payloads_rx) = mpsc::channel(8);
595
596 let request_builder_fut = run_request_builder(
601 v2_series_builder,
602 v2_sketch_builder,
603 series_mode,
604 sketches_mode,
605 v3_runtime_config,
606 telemetry,
607 events_rx,
608 payloads_tx,
609 flush_timeout,
610 log_payloads,
611 );
612 runtime::worker("request_builder", request_builder_fut)
613 .on_runtime(context.topology_context().global_thread_pool().clone())
614 .needed()
615 .spawn();
616
617 health.mark_ready();
618 debug!("Datadog Metrics encoder started.");
619
620 loop {
621 select! {
622 biased;
623
624 _ = health.live() => continue,
625 maybe_payload = payloads_rx.recv() => match maybe_payload {
626 Some(payload) => {
627 apply_routing_and_dispatch(context.dispatcher(), payload, routing_targets.as_ref()).await;
628 }
629 None => break,
630 },
631 maybe_event_buffer = context.events().next() => match maybe_event_buffer {
632 Some(event_buffer) => {
633 let permit = loop {
640 select! {
641 biased;
642
643 permit = events_tx.reserve() => break permit
644 .error_context("Failed to reserve capacity for event buffer.")?,
645 maybe_payload = payloads_rx.recv() => match maybe_payload {
646 Some(payload) => {
647 apply_routing_and_dispatch(context.dispatcher(), payload, routing_targets.as_ref()).await;
648 },
649
650 None => return Err(generic_error!("Request builder task stopped before accepting event buffer.")),
652 }
653 }
654 };
655
656 permit.send(event_buffer);
657 }
658 None => break,
659 },
660 }
661 }
662
663 drop(events_tx);
665
666 while let Some(payload) = payloads_rx.recv().await {
668 apply_routing_and_dispatch(context.dispatcher(), payload, routing_targets.as_ref()).await;
669 }
670
671 debug!("Datadog Metrics encoder stopped.");
674
675 Ok(())
676 }
677}
678
679async fn apply_routing_and_dispatch(
683 dispatcher: &PayloadsDispatcher, payload: Payload, routing_targets: Option<&Arc<MetricsRoutingTargets>>,
684) {
685 let payload = apply_routing_targets(payload, routing_targets);
686 if let Err(e) = dispatcher.dispatch(payload).await {
687 error!("Failed to dispatch payload: {}", e);
688 }
689}
690
691fn apply_routing_targets(payload: Payload, routing_targets: Option<&Arc<MetricsRoutingTargets>>) -> Payload {
692 let Some(routing_targets) = routing_targets else {
693 return payload;
694 };
695
696 match payload {
697 Payload::Http(http_payload) => {
698 let (mut metadata, request) = http_payload.into_parts();
699 metadata.set(Arc::clone(routing_targets));
700 Payload::Http(HttpPayload::new(metadata, request))
701 }
702 payload => payload,
703 }
704}
705
706fn log_metric_payload(metric: &Metric) {
710 match metric.values() {
711 MetricValues::Counter(..) | MetricValues::Rate(..) | MetricValues::Gauge(..) | MetricValues::Set(..) => {
712 debug!(?metric, "Flushing series metric.")
713 }
714 MetricValues::Histogram(..) | MetricValues::Distribution(..) => {
715 debug!(?metric, "Flushing sketch metric.")
716 }
717 }
718}
719
720#[allow(clippy::too_many_arguments)]
721async fn run_request_builder(
722 mut v2_series_builder: Option<RequestBuilder<v2::MetricsEndpointEncoder>>,
723 mut v2_sketch_builder: Option<RequestBuilder<v2::MetricsEndpointEncoder>>, series_mode: MetricsEncoderMode,
724 sketches_mode: MetricsEncoderMode, v3_runtime_config: V3RuntimeConfig, telemetry: ComponentTelemetry,
725 mut events_rx: mpsc::Receiver<EventsBuffer>, mut payloads_tx: mpsc::Sender<PayloadsBuffer>,
726 flush_timeout: Duration, log_payloads: bool,
727) -> Result<(), GenericError> {
728 let mut pending_flush = false;
729 let pending_flush_timeout = sleep(flush_timeout);
730 tokio::pin!(pending_flush_timeout);
731
732 let mut v3_series_metrics = series_mode.needs_v3().then(Vec::<Metric>::new);
733 let mut v3_sketch_metrics = sketches_mode.needs_v3().then(Vec::<Metric>::new);
734 let mut v3_series_points = 0usize;
735 let mut v3_sketch_points = 0usize;
736 let mut v3_series_ratio = V3CompressionRatio::default();
737 let mut v3_sketch_ratio = V3CompressionRatio::default();
738
739 let tag_series = series_mode.needs_tagging();
740 let tag_sketches = sketches_mode.needs_tagging();
741 let v3_flush_context = V3FlushContext {
742 endpoint_config: &v3_runtime_config.endpoint_config,
743 payload_limits: v3_runtime_config.payload_limits,
744 series_endpoint_uri: &v3_runtime_config.series_endpoint_uri,
745 serializer_telemetry: &v3_runtime_config.serializer_telemetry,
746 telemetry: &telemetry,
747 };
748
749 loop {
750 select! {
751 Some(event_buffer) = events_rx.recv() => {
752 for event in event_buffer {
753 let metric = match event.try_into_metric() {
754 Some(metric) => metric,
755 None => continue,
756 };
757
758 if log_payloads {
759 log_metric_payload(&metric);
760 }
761
762 if !v1::has_emittable_point(&metric) {
765 debug!(metric = %metric.context().name(), "Dropping series metric with no finite points.");
766 telemetry.events_dropped_encoder().increment(1);
767 continue;
768 }
769
770 let endpoint = MetricsEndpoint::from_metric(&metric);
772 let (endpoint_mode, maybe_v2_builder, maybe_v3_metrics, v3_points) = match endpoint {
773 MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2 => (
774 series_mode,
775 &mut v2_series_builder,
776 &mut v3_series_metrics,
777 &mut v3_series_points,
778 ),
779 MetricsEndpoint::Sketches => (
780 sketches_mode,
781 &mut v2_sketch_builder,
782 &mut v3_sketch_metrics,
783 &mut v3_sketch_points,
784 ),
785 };
786 let metric_point_count = metric.values().len();
787 let should_buffer_v3 = endpoint_mode.needs_v3();
788
789 if should_buffer_v3 {
794 if let Some(metrics) = maybe_v3_metrics {
795 metrics.push(metric.clone());
796 *v3_points += metric_point_count;
797 }
798 }
799
800 let v2_payload_info = match endpoint {
805 MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2 => {
806 tag_series.then(MetricsPayloadInfo::v2_series)
807 }
808 MetricsEndpoint::Sketches => tag_sketches.then(MetricsPayloadInfo::v2_sketches),
809 };
810 let _v2_flushed = if let Some(builder) = maybe_v2_builder {
811 let result = encode_v2_metrics(builder, metric, &telemetry, &mut payloads_tx, v2_payload_info).await?;
812 if should_buffer_v3
813 && !result.encoded()
814 && !matches!(endpoint_mode, MetricsEncoderMode::V3Enabled)
815 {
816 if let Some(metrics) = maybe_v3_metrics {
817 let _ = metrics.pop();
818 *v3_points = v3_points.saturating_sub(metric_point_count);
819 }
820 }
821 result.flushed()
822 } else {
823 false
824 };
825
826 let v3_payload_info = match endpoint {
827 MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2 => {
828 tag_series.then(MetricsPayloadInfo::v3_series)
829 }
830 MetricsEndpoint::Sketches => tag_sketches.then(MetricsPayloadInfo::v3_sketches),
831 };
832 let mut split_metric = None;
833 let _v3_flushed = if let Some(v3_metrics) = maybe_v3_metrics {
834 let should_flush_v3 = match endpoint_mode {
835 MetricsEncoderMode::V2Only => false,
836 MetricsEncoderMode::V3Enabled => {
837 if v3_flush_context.payload_limits.point_count_fits(metric_point_count)
838 && v3_flush_context.payload_limits.point_count_exceeds_limit(*v3_points)
839 && v3_metrics.len() > 1
840 {
841 v3_flush_context
842 .serializer_telemetry
843 .record_split_reason(V3PayloadSplitReason::MaxPoints);
844 if let Some(metric) = v3_metrics.pop() {
845 *v3_points = v3_points.saturating_sub(metric.values().len());
846 split_metric = Some(metric);
847 }
848 true
849 } else {
850 v3_flush_context.payload_limits.should_flush_point_count_limit(*v3_points)
851 || v3_flush_context
852 .payload_limits
853 .should_flush_metric_count_limit(v3_metrics)
854 }
855 }
856 };
857 if should_flush_v3 {
858 encode_and_flush_v3_metrics(
859 endpoint,
860 v3_flush_context,
861 v3_metrics,
862 &mut v3_series_ratio,
863 &mut v3_sketch_ratio,
864 &mut payloads_tx,
865 v3_payload_info,
866 )
867 .await?;
868 *v3_points = 0;
869 true
870 } else {
871 false
872 }
873 } else {
874 false
875 };
876
877 if matches!(endpoint_mode, MetricsEncoderMode::V3Enabled) {
878 if let Some(m) = split_metric.take() {
879 let point_count = m.values().len();
880 if let Some(metrics) = maybe_v3_metrics {
881 metrics.push(m);
882 *v3_points += point_count;
883 }
884 }
885 }
886 }
887
888 debug!("Processed event buffer.");
889
890 if !pending_flush {
892 pending_flush_timeout.as_mut().reset(tokio::time::Instant::now() + flush_timeout);
893 pending_flush = true;
894 }
895 },
896 _ = &mut pending_flush_timeout, if pending_flush => {
897 debug!("Flushing pending request(s).");
898
899 pending_flush = false;
900
901 let v2_series_payload_info = tag_series.then(MetricsPayloadInfo::v2_series);
903 let mut v2_series_flush_succeeded = true;
904 if let Some(builder) = &mut v2_series_builder {
905 if let Err(e) = flush_v2_metrics(builder, &mut payloads_tx, v2_series_payload_info).await {
906 error!(error = %e, "Failed to flush V2 series metrics: {}", e);
907 v2_series_flush_succeeded = false;
908 }
909 }
910
911 let v3_series_payload_info = tag_series.then(MetricsPayloadInfo::v3_series);
912 if let Some(metrics) = &mut v3_series_metrics {
913 if v2_series_flush_succeeded || matches!(series_mode, MetricsEncoderMode::V3Enabled) {
914 if let Err(e) = encode_and_flush_v3_series_metrics(
915 v3_flush_context,
916 metrics,
917 &mut v3_series_ratio,
918 &mut payloads_tx,
919 v3_series_payload_info,
920 )
921 .await
922 {
923 error!(error = %e, "Failed to flush V3 series metrics: {}", e);
924 }
925 v3_series_points = 0;
926 } else {
927 warn!("Failed to flush V2 series metrics, skipping V3 series flush.");
928 metrics.clear();
929 v3_series_points = 0;
930 }
931 }
932
933 let v2_sketches_payload_info = tag_sketches.then(MetricsPayloadInfo::v2_sketches);
935 let mut v2_sketches_flush_succeeded = true;
936 if let Some(builder) = &mut v2_sketch_builder {
937 if let Err(e) = flush_v2_metrics(builder, &mut payloads_tx, v2_sketches_payload_info).await {
938 error!(error = %e, "Failed to flush V2 sketch metrics: {}", e);
939 v2_sketches_flush_succeeded = false;
940 }
941 }
942
943 let v3_sketches_payload_info = tag_sketches.then(MetricsPayloadInfo::v3_sketches);
944 if let Some(metrics) = &mut v3_sketch_metrics {
945 if v2_sketches_flush_succeeded || matches!(sketches_mode, MetricsEncoderMode::V3Enabled) {
946 if let Err(e) = encode_and_flush_v3_sketch_metrics(
947 v3_flush_context,
948 metrics,
949 &mut v3_sketch_ratio,
950 &mut payloads_tx,
951 v3_sketches_payload_info,
952 )
953 .await
954 {
955 error!(error = %e, "Failed to flush V3 sketch metrics: {}", e);
956 }
957 v3_sketch_points = 0;
958 } else {
959 warn!("Failed to flush V2 sketch metrics, skipping V3 sketch flush.");
960 metrics.clear();
961 v3_sketch_points = 0;
962 }
963 }
964
965 debug!("All flushed requests sent to I/O task. Waiting for next event buffer...");
966 },
967
968 else => break,
970 }
971 }
972
973 Ok(())
974}
975
976struct EncodeResult {
977 encoded: bool,
978 flushed: bool,
979}
980
981impl EncodeResult {
982 pub const fn new(encoded: bool, flushed: bool) -> Self {
983 Self { encoded, flushed }
984 }
985
986 pub const fn encoded(&self) -> bool {
987 self.encoded
988 }
989
990 pub const fn flushed(&self) -> bool {
991 self.flushed
992 }
993}
994
995async fn encode_v2_metrics(
996 request_builder: &mut RequestBuilder<v2::MetricsEndpointEncoder>, metric: Metric, telemetry: &ComponentTelemetry,
997 payloads_tx: &mut mpsc::Sender<Payload>, payload_info: Option<MetricsPayloadInfo>,
998) -> Result<EncodeResult, GenericError> {
999 let metric_to_retry = match request_builder.encode(metric).await {
1003 Ok(None) => return Ok(EncodeResult::new(true, false)),
1004 Ok(Some(metric)) => metric,
1005 Err(RequestBuilderError::InvalidInput { input }) => {
1006 debug!(metric_name = %input.context().name(), "Dropping metric with no emittable values.");
1007 telemetry.events_dropped_encoder().increment(1);
1008 return Ok(EncodeResult::new(false, false));
1009 }
1010 Err(e) => {
1011 error!(error = %e, "Failed to encode metric.");
1012 telemetry.events_dropped_encoder().increment(1);
1013 return Ok(EncodeResult::new(false, false));
1014 }
1015 };
1016
1017 flush_v2_metrics(request_builder, payloads_tx, payload_info).await?;
1018
1019 match request_builder.encode(metric_to_retry).await {
1023 Ok(None) => Ok(EncodeResult::new(true, true)),
1024 Ok(Some(_)) => unreachable!(
1025 "failure to encode due to size should never occur after flush for metrics which aren't unencodable"
1026 ),
1027 Err(e) => {
1028 error!(error = %e, "Failed to encode metric.");
1029 telemetry.events_dropped_encoder().increment(1);
1030 Ok(EncodeResult::new(false, true))
1031 }
1032 }
1033}
1034
1035async fn flush_v2_metrics(
1036 request_builder: &mut RequestBuilder<MetricsEndpointEncoder>, payloads_tx: &mut mpsc::Sender<Payload>,
1037 payload_info: Option<MetricsPayloadInfo>,
1038) -> Result<usize, GenericError> {
1039 let mut requests_flushed = 0;
1040
1041 let maybe_requests = request_builder.flush().await;
1042 for maybe_request in maybe_requests {
1043 match maybe_request {
1044 Ok((events, data_points, request)) => {
1045 requests_flushed += 1;
1046
1047 flush_payload(request, events, data_points, payloads_tx, payload_info).await?;
1048 }
1049
1050 Err(e) => {
1052 if !e.is_recoverable() {
1053 return Err(GenericError::from(e).context("Failed to flush request."));
1054 }
1055 }
1056 }
1057 }
1058
1059 Ok(requests_flushed)
1060}
1061
1062#[derive(Clone, Copy)]
1063struct V3FlushContext<'a> {
1064 endpoint_config: &'a EndpointConfiguration,
1065 payload_limits: V3PayloadLimits,
1066 series_endpoint_uri: &'a str,
1067 serializer_telemetry: &'a V3SerializerTelemetry,
1068 telemetry: &'a ComponentTelemetry,
1069}
1070
1071#[allow(clippy::too_many_arguments)]
1072async fn encode_and_flush_v3_metrics(
1073 endpoint: MetricsEndpoint, context: V3FlushContext<'_>, metrics: &mut Vec<Metric>,
1074 series_ratio: &mut V3CompressionRatio, sketches_ratio: &mut V3CompressionRatio,
1075 payloads_tx: &mut mpsc::Sender<Payload>, payload_info: Option<MetricsPayloadInfo>,
1076) -> Result<(), GenericError> {
1077 match endpoint {
1078 MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2 => {
1079 encode_and_flush_v3_series_metrics(context, metrics, series_ratio, payloads_tx, payload_info).await
1080 }
1081 MetricsEndpoint::Sketches => {
1082 encode_and_flush_v3_sketch_metrics(context, metrics, sketches_ratio, payloads_tx, payload_info).await
1083 }
1084 }
1085}
1086
1087async fn encode_and_flush_v3_series_metrics(
1088 context: V3FlushContext<'_>, metrics: &mut Vec<Metric>, ratio: &mut V3CompressionRatio,
1089 payloads_tx: &mut mpsc::Sender<Payload>, payload_info: Option<MetricsPayloadInfo>,
1090) -> Result<(), GenericError> {
1091 if metrics.is_empty() {
1092 return Ok(());
1093 }
1094 let metrics_to_flush = std::mem::take(metrics);
1095
1096 encode_and_flush_v3_payload_requests(
1097 context.series_endpoint_uri,
1098 &metrics_to_flush,
1099 context,
1100 "series",
1101 ratio,
1102 payloads_tx,
1103 payload_info,
1104 )
1105 .await
1106}
1107
1108async fn encode_and_flush_v3_sketch_metrics(
1109 context: V3FlushContext<'_>, metrics: &mut Vec<Metric>, ratio: &mut V3CompressionRatio,
1110 payloads_tx: &mut mpsc::Sender<Payload>, payload_info: Option<MetricsPayloadInfo>,
1111) -> Result<(), GenericError> {
1112 if metrics.is_empty() {
1113 return Ok(());
1114 }
1115 let metrics_to_flush = std::mem::take(metrics);
1116
1117 encode_and_flush_v3_payload_requests(
1118 V3_SKETCHES_ENDPOINT_URI,
1119 &metrics_to_flush,
1120 context,
1121 "sketches",
1122 ratio,
1123 payloads_tx,
1124 payload_info,
1125 )
1126 .await
1127}
1128
1129const V3_MIN_COMPRESSION_RATIO: f64 = 0.02;
1133
1134const V3_BATCH_TARGET_MARGIN: f64 = 0.9;
1139
1140const V3_RATIO_SMOOTHING: f64 = 0.25;
1142
1143#[derive(Default)]
1150struct V3CompressionRatio {
1151 ratio: Option<f64>,
1152}
1153
1154impl V3CompressionRatio {
1155 fn uncompressed_target(&self, limits: V3PayloadLimits) -> usize {
1157 let ratio = self.ratio.unwrap_or(1.0).clamp(V3_MIN_COMPRESSION_RATIO, 1.0);
1160 let target_from_compressed_limit = (limits.max_compressed_size as f64 / ratio) as usize;
1161 let target = target_from_compressed_limit.min(limits.max_uncompressed_size);
1162
1163 (((target as f64) * V3_BATCH_TARGET_MARGIN) as usize).max(1)
1165 }
1166
1167 fn record(&mut self, uncompressed_len: usize, compressed_len: usize) {
1169 if uncompressed_len == 0 {
1170 return;
1171 }
1172
1173 let observed = compressed_len as f64 / uncompressed_len as f64;
1174 self.ratio = Some(match self.ratio {
1175 Some(current) => (current * (1.0 - V3_RATIO_SMOOTHING)) + (observed * V3_RATIO_SMOOTHING),
1176 None => observed,
1177 });
1178 }
1179}
1180
1181struct V3Batch {
1183 encoded: V3EncodedMetrics,
1184 event_count: usize,
1185 data_point_count: usize,
1186}
1187
1188fn build_v3_batch(
1195 metrics: &[Metric], idx: &mut usize, context: V3FlushContext<'_>, payload_kind: &'static str,
1196 target_uncompressed_len: usize,
1197) -> Option<V3Batch> {
1198 let mut writer = V3Writer::new();
1199 let mut tags_deduplicator = ReusableDeduplicator::new();
1200 let additional_tags = context.endpoint_config.additional_tags();
1201 let mut data_point_count = 0usize;
1202 let mut cut_for_size = false;
1203
1204 while *idx < metrics.len() {
1205 let metric = &metrics[*idx];
1206
1207 if !metric_has_emittable_values(metric) {
1208 debug!(metric_name = %metric.context().name(), "Dropping metric with no emittable values.");
1209 context.telemetry.events_dropped_encoder().increment(1);
1210 *idx += 1;
1211 continue;
1212 }
1213
1214 let metric_points = metric.values().len();
1215 if !context.payload_limits.point_count_fits(metric_points) {
1216 context.serializer_telemetry.record_item_too_big();
1218 context
1219 .serializer_telemetry
1220 .record_split_reason(V3PayloadSplitReason::ItemTooBig);
1221 warn!(
1222 payload_kind,
1223 data_points = metric_points,
1224 point_limit = context.payload_limits.max_points_per_payload,
1225 "Dropping oversized V3 metric that exceeds the point-count limit."
1226 );
1227 context.telemetry.events_dropped_encoder().increment(1);
1228 *idx += 1;
1229 continue;
1230 }
1231
1232 if writer.metric_count() > 0 {
1235 if !context
1236 .payload_limits
1237 .point_count_fits(data_point_count + metric_points)
1238 {
1239 context
1240 .serializer_telemetry
1241 .record_split_reason(V3PayloadSplitReason::MaxPoints);
1242 break;
1243 }
1244
1245 if context.payload_limits.metric_count_reached(writer.metric_count()) {
1246 break;
1247 }
1248 }
1249
1250 write_metric_to_v3(&mut writer, metric, additional_tags, &mut tags_deduplicator);
1251 data_point_count += metric_points;
1252 *idx += 1;
1253
1254 if writer.estimated_uncompressed_len() >= target_uncompressed_len {
1258 cut_for_size = true;
1259 break;
1260 }
1261 }
1262
1263 if cut_for_size && *idx < metrics.len() {
1266 context
1267 .serializer_telemetry
1268 .record_split_reason(V3PayloadSplitReason::PayloadFull);
1269 }
1270
1271 let event_count = writer.metric_count();
1272 if event_count == 0 {
1273 return None;
1274 }
1275
1276 match writer.finalize() {
1277 Ok(encoded) => Some(V3Batch {
1278 encoded,
1279 event_count,
1280 data_point_count,
1281 }),
1282 Err(e) => {
1283 error!(error = %e, payload_kind, events = event_count, "Failed to encode V3 metrics payload request.");
1284 context.telemetry.events_dropped_encoder().increment(event_count as u64);
1285 None
1286 }
1287 }
1288}
1289
1290async fn create_v3_batch_request(
1292 endpoint_uri: &str, batch: V3Batch, context: V3FlushContext<'_>, payload_kind: &'static str,
1293 ratio: &mut V3CompressionRatio,
1294) -> Option<V3PayloadRequest> {
1295 let V3Batch {
1296 encoded,
1297 event_count,
1298 data_point_count,
1299 } = batch;
1300
1301 let mut encoded_request =
1302 match create_v3_request(endpoint_uri, encoded, context.endpoint_config.compression_scheme()).await {
1303 Ok(request) => request,
1304 Err(e) => {
1305 error!(error = %e, payload_kind, events = event_count, "Failed to create V3 metrics request.");
1306 context.telemetry.events_dropped_encoder().increment(event_count as u64);
1307 return None;
1308 }
1309 };
1310
1311 ratio.record(encoded_request.uncompressed_len, encoded_request.compressed_len);
1314
1315 if !context.payload_limits.request_fits(&encoded_request) {
1316 return None;
1317 }
1318
1319 if let Err(e) =
1323 measure_v3_column_compressed_sizes(&mut encoded_request.stats, context.endpoint_config.compression_scheme())
1324 .await
1325 {
1326 error!(error = %e, payload_kind, "Failed to measure V3 column compressed sizes.");
1327 } else {
1328 record_v3_serializer_stats(context.serializer_telemetry, &encoded_request.stats);
1329 }
1330
1331 Some(V3PayloadRequest {
1332 request: encoded_request.request,
1333 event_count,
1334 data_point_count,
1335 })
1336}
1337
1338async fn split_and_encode_oversized_v3_range(
1344 endpoint_uri: &str, metrics: &[Metric], range: Range<usize>, context: V3FlushContext<'_>,
1345 payload_kind: &'static str, ratio: &mut V3CompressionRatio,
1346) -> Vec<V3PayloadRequest> {
1347 let accepted_metrics = metrics[range]
1350 .iter()
1351 .filter(|metric| {
1352 metric_has_emittable_values(metric) && context.payload_limits.point_count_fits(metric.values().len())
1353 })
1354 .collect::<Vec<_>>();
1355
1356 let mut requests = Vec::new();
1357 let mut pending_ranges = VecDeque::new();
1358 pending_ranges.push_back(0..accepted_metrics.len());
1359
1360 while let Some(range) = pending_ranges.pop_front() {
1361 if range.is_empty() {
1362 continue;
1363 }
1364
1365 let metrics_in_range = &accepted_metrics[range.clone()];
1366 let event_count = metrics_in_range.len();
1367 let data_point_count = metrics_in_range.iter().map(|metric| metric.values().len()).sum();
1368
1369 let encoded = match encode_v3_metrics_batch(
1370 metrics_in_range.iter().copied(),
1371 context.endpoint_config.additional_tags(),
1372 ) {
1373 Ok(encoded) => encoded,
1374 Err(e) => {
1375 error!(error = %e, payload_kind, events = event_count, "Failed to encode V3 metrics payload request.");
1376 context.telemetry.events_dropped_encoder().increment(event_count as u64);
1377 continue;
1378 }
1379 };
1380 let batch = V3Batch {
1381 encoded,
1382 event_count,
1383 data_point_count,
1384 };
1385
1386 if let Some(request) = create_v3_batch_request(endpoint_uri, batch, context, payload_kind, ratio).await {
1387 requests.push(request);
1388 continue;
1389 }
1390
1391 if range.len() == 1 {
1392 context.serializer_telemetry.record_item_too_big();
1394 context
1395 .serializer_telemetry
1396 .record_split_reason(V3PayloadSplitReason::ItemTooBig);
1397 warn!(
1398 payload_kind,
1399 compressed_limit = context.payload_limits.max_compressed_size,
1400 uncompressed_limit = context.payload_limits.max_uncompressed_size,
1401 "Dropping oversized V3 metric that cannot be split further."
1402 );
1403 context.telemetry.events_dropped_encoder().increment(1);
1404 continue;
1405 }
1406
1407 context
1409 .serializer_telemetry
1410 .record_split_reason(V3PayloadSplitReason::PayloadFull);
1411 let pivot = range.start + range.len() / 2;
1412 pending_ranges.push_front(pivot..range.end);
1413 pending_ranges.push_front(range.start..pivot);
1414 }
1415
1416 requests
1417}
1418
1419#[allow(clippy::too_many_arguments)]
1420async fn encode_and_flush_v3_payload_requests(
1421 endpoint_uri: &str, metrics: &[Metric], context: V3FlushContext<'_>, payload_kind: &'static str,
1422 ratio: &mut V3CompressionRatio, payloads_tx: &mut mpsc::Sender<Payload>, payload_info: Option<MetricsPayloadInfo>,
1423) -> Result<(), GenericError> {
1424 let target_uncompressed_len = ratio.uncompressed_target(context.payload_limits);
1425 let mut idx = 0;
1426
1427 while idx < metrics.len() {
1428 let batch_start = idx;
1429 let Some(batch) = build_v3_batch(metrics, &mut idx, context, payload_kind, target_uncompressed_len) else {
1430 if idx == batch_start {
1433 error!(
1434 payload_kind,
1435 "V3 batching made no progress; dropping remaining metrics."
1436 );
1437 context
1438 .telemetry
1439 .events_dropped_encoder()
1440 .increment((metrics.len() - idx) as u64);
1441 break;
1442 }
1443 continue;
1444 };
1445
1446 let requests = match create_v3_batch_request(endpoint_uri, batch, context, payload_kind, ratio).await {
1447 Some(request) => vec![request],
1448 None => {
1452 split_and_encode_oversized_v3_range(
1453 endpoint_uri,
1454 metrics,
1455 batch_start..idx,
1456 context,
1457 payload_kind,
1458 ratio,
1459 )
1460 .await
1461 }
1462 };
1463
1464 for payload_request in requests {
1465 flush_payload(
1466 payload_request.request,
1467 payload_request.event_count,
1468 payload_request.data_point_count,
1469 payloads_tx,
1470 payload_info,
1471 )
1472 .await?;
1473 debug!(
1474 payload_kind,
1475 events = payload_request.event_count,
1476 data_points = payload_request.data_point_count,
1477 "Sent V3 payload."
1478 );
1479 }
1480 }
1481
1482 Ok(())
1483}
1484
1485fn record_v3_serializer_stats(telemetry: &V3SerializerTelemetry, stats: &V3EncoderStats) {
1486 telemetry.record_values_count(stats.value_encoding_stats);
1487
1488 for column in &stats.columns {
1489 let uncompressed_size = column.bytes.len() as u64;
1490 let compressed_size = column.compressed_len as u64;
1491 telemetry.record_column_size(column.field_number, uncompressed_size, compressed_size);
1492 }
1493}
1494
1495async fn measure_v3_column_compressed_sizes(
1500 stats: &mut V3EncoderStats, compression_scheme: CompressionScheme,
1501) -> Result<(), GenericError> {
1502 for column in &mut stats.columns {
1503 column.compressed_len = compressed_v3_len(&column.bytes, compression_scheme)
1504 .await
1505 .error_context("Failed to measure V3 column compressed size.")?;
1506 }
1507
1508 Ok(())
1509}
1510
1511async fn compressed_v3_len(bytes: &[u8], compression_scheme: CompressionScheme) -> Result<usize, GenericError> {
1512 if matches!(compression_scheme, CompressionScheme::Noop) {
1513 return Ok(bytes.len());
1514 }
1515
1516 let buffer = ChunkedBytesBuffer::new(RB_BUFFER_CHUNK_SIZE);
1517 let mut compressor = Compressor::from_scheme(compression_scheme, buffer);
1518 compressor
1519 .write_all(bytes)
1520 .await
1521 .error_context("Failed to compress V3 bytes.")?;
1522 compressor
1523 .flush()
1524 .await
1525 .error_context("Failed to flush V3 compressor.")?;
1526 compressor
1527 .shutdown()
1528 .await
1529 .error_context("Failed to shutdown V3 compressor.")?;
1530
1531 Ok(compressor.into_inner().freeze().len())
1532}
1533
1534async fn flush_payload(
1535 request: Request<FrozenChunkedBytesBuffer>, event_count: usize, data_point_count: usize,
1536 payloads_tx: &mut mpsc::Sender<Payload>, payload_info: Option<MetricsPayloadInfo>,
1537) -> Result<(), GenericError> {
1538 let mut payload_meta = PayloadMetadata::from_event_and_data_point_count(event_count, data_point_count);
1539 if let Some(info) = payload_info {
1540 payload_meta = payload_meta.with(info);
1541 }
1542 let http_payload = HttpPayload::new(payload_meta, request);
1543 let payload = Payload::Http(http_payload);
1544
1545 payloads_tx
1546 .send(payload)
1547 .await
1548 .error_context("Failed to send payload.")?;
1549
1550 Ok(())
1551}
1552
1553fn encode_v3_metrics_batch<'a>(
1555 metrics: impl IntoIterator<Item = &'a Metric>, additional_tags: &SharedTagSet,
1556) -> Result<V3EncodedMetrics, GenericError> {
1557 let mut writer = V3Writer::new();
1558 let mut tags_deduplicator = ReusableDeduplicator::new();
1559
1560 for metric in metrics {
1561 write_metric_to_v3(&mut writer, metric, additional_tags, &mut tags_deduplicator);
1562 }
1563
1564 writer
1565 .finalize()
1566 .map_err(|e| generic_error!("Failed to serialize V3 payload: {}", e))
1567}
1568
1569pub(super) fn sketch_has_emittable_values(sketch: &DDSketch) -> bool {
1571 !sketch.is_empty() || !sketch.bins().is_empty()
1572}
1573
1574pub(super) fn metric_has_emittable_values(metric: &Metric) -> bool {
1575 match metric.values() {
1576 MetricValues::Counter(points) | MetricValues::Rate(points, _) | MetricValues::Gauge(points) => {
1577 points.into_iter().any(|(_, value)| emittable_scalar_point(value))
1578 }
1579 MetricValues::Set(points) => points.into_iter().any(|(_, value)| emittable_scalar_point(value)),
1580 MetricValues::Distribution(sketches) => sketches
1581 .into_iter()
1582 .any(|(_, sketch)| sketch_has_emittable_values(sketch)),
1583 MetricValues::Histogram(points) => points.into_iter().any(|(_, histogram)| !histogram.samples().is_empty()),
1584 }
1585}
1586
1587fn write_metric_to_v3(
1588 writer: &mut V3Writer, metric: &Metric, additional_tags: &SharedTagSet,
1589 tags_deduplicator: &mut ReusableDeduplicator<Tag>,
1590) {
1591 if !metric_has_emittable_values(metric) {
1592 debug!(metric_name = %metric.context().name(), "Dropping metric with no emittable values.");
1593 return;
1594 }
1595
1596 let metric_type = match metric.values() {
1597 MetricValues::Counter(..) => V3MetricType::Count,
1598 MetricValues::Rate(..) => V3MetricType::Rate,
1599 MetricValues::Gauge(..) | MetricValues::Set(..) => V3MetricType::Gauge,
1600 MetricValues::Histogram(..) | MetricValues::Distribution(..) => V3MetricType::Sketch,
1601 };
1602 let is_sketch = metric_type == V3MetricType::Sketch;
1603
1604 let mut builder = writer.write(metric_type, metric.context().name());
1605
1606 let chained_tags = metric
1608 .context()
1609 .tags()
1610 .into_iter()
1611 .chain(additional_tags)
1612 .chain(metric.context().origin_tags());
1613 let all_tags = tags_deduplicator
1614 .deduplicated(chained_tags)
1615 .filter(|t| is_sketch || !is_v3_series_resource_tag(t) && !is_v3_series_device_tag(t))
1616 .map(|t| t.as_str());
1617 builder.set_tags(all_tags);
1618
1619 let mut resources = Vec::new();
1621 if let Some(host) = metric.context().host().filter(|host| !host.is_empty()) {
1622 resources.push(("host", host));
1623 }
1624 if !is_sketch {
1625 let mut device_resource = None;
1626 let chained_tags = metric
1627 .context()
1628 .origin_tags()
1629 .into_iter()
1630 .chain(metric.context().tags())
1631 .chain(additional_tags);
1632 for tag in tags_deduplicator.deduplicated(chained_tags) {
1633 if is_v3_series_device_tag(tag) {
1634 device_resource = tag.value().filter(|device| !device.is_empty());
1635 } else if is_v3_series_resource_tag(tag) {
1636 if let Some((rtype, rname)) = tag.value().and_then(|value| value.split_once(':')) {
1637 if !rtype.is_empty() && !rname.is_empty() {
1638 resources.push((rtype, rname));
1639 }
1640 }
1641 }
1642 }
1643 if let Some(device) = device_resource {
1644 let device_idx = usize::from(metric.context().host().is_some_and(|host| !host.is_empty()));
1645 resources.insert(device_idx, ("device", device));
1646 }
1647 }
1648 builder.set_resources(&resources);
1649
1650 if let Some(origin) = metric.metadata().origin() {
1652 match origin {
1653 MetricOrigin::SourceType(source_type) => {
1654 builder.set_source_type(source_type.as_ref());
1655 }
1656 MetricOrigin::OriginMetadata {
1657 product,
1658 subproduct,
1659 product_detail,
1660 } => {
1661 builder.set_origin(*product, *subproduct, *product_detail, false);
1662 }
1663 }
1664 }
1665
1666 if metric_type != V3MetricType::Sketch {
1667 if let Some(unit) = metric.metadata().unit() {
1668 builder.set_unit(unit);
1669 }
1670 }
1671
1672 match metric.values() {
1674 MetricValues::Counter(points) | MetricValues::Gauge(points) => {
1675 for (ts, val) in points {
1676 if !emittable_scalar_point(val) {
1677 continue;
1678 }
1679 let timestamp = ts.map(|t| t.get() as i64).unwrap_or(0);
1680 builder.add_point(timestamp, val);
1681 }
1682 }
1683 MetricValues::Rate(points, interval) => {
1684 builder.set_interval(interval.as_secs());
1685 for (ts, val) in points {
1686 let scaled = if interval.is_zero() {
1688 val
1689 } else {
1690 val / interval.as_secs_f64()
1691 };
1692 if !emittable_scalar_point(scaled) {
1693 continue;
1694 }
1695 let timestamp = ts.map(|t| t.get() as i64).unwrap_or(0);
1696 builder.add_point(timestamp, scaled);
1697 }
1698 }
1699 MetricValues::Set(points) => {
1700 for (ts, count) in points {
1702 if !emittable_scalar_point(count) {
1703 continue;
1704 }
1705 let timestamp = ts.map(|t| t.get() as i64).unwrap_or(0);
1706 builder.add_point(timestamp, count);
1707 }
1708 }
1709 MetricValues::Distribution(sketches) => {
1710 for (ts, sketch) in sketches {
1711 let timestamp = ts.map(|t| t.get() as i64).unwrap_or(0);
1712 if sketch_has_emittable_values(sketch) {
1713 let bin_keys: Vec<i32> = sketch.bins().iter().map(|b| b.key()).collect();
1714 let bin_counts: Vec<u32> = sketch.bins().iter().map(|b| b.count()).collect();
1715 builder.add_sketch(
1716 timestamp,
1717 sketch.count() as i64,
1718 sketch.stored_sum(),
1719 sketch.stored_min(),
1720 sketch.stored_max(),
1721 &bin_keys,
1722 &bin_counts,
1723 );
1724 }
1725 }
1726 }
1727 MetricValues::Histogram(histograms) => {
1728 for (ts, histogram) in histograms {
1729 let timestamp = ts.map(|t| t.get() as i64).unwrap_or(0);
1730 let mut sketch = DDSketch::default();
1732 for sample in histogram.samples() {
1733 sketch.insert_n(sample.value.into_inner(), sample.weight.0 as u64);
1734 }
1735 if !sketch.is_empty() {
1736 let bin_keys: Vec<i32> = sketch.bins().iter().map(|b| b.key()).collect();
1737 let bin_counts: Vec<u32> = sketch.bins().iter().map(|b| b.count()).collect();
1738 builder.add_sketch(
1739 timestamp,
1740 sketch.count() as i64,
1741 sketch.sum().unwrap_or(0.0),
1742 sketch.min().unwrap_or(0.0),
1743 sketch.max().unwrap_or(0.0),
1744 &bin_keys,
1745 &bin_counts,
1746 );
1747 }
1748 }
1749 }
1750 }
1751
1752 builder.close();
1753}
1754
1755#[inline]
1756fn emittable_scalar_point(point: f64) -> bool {
1757 point.is_finite()
1758}
1759
1760fn is_v3_series_device_tag(tag: &Tag) -> bool {
1761 tag.name() == "device" && tag.value().is_some()
1762}
1763
1764fn is_v3_series_resource_tag(tag: &Tag) -> bool {
1765 tag.name() == "dd.internal.resource" && tag.value().is_some()
1766}
1767
1768async fn create_v3_request(
1770 endpoint_uri: &str, encoded: V3EncodedMetrics, compression_scheme: CompressionScheme,
1771) -> Result<V3EncodedRequest, GenericError> {
1772 let mut header_buf = [0; 16];
1775 let header_len = {
1776 let mut header_writer = CodedOutputStream::bytes(&mut header_buf);
1777 header_writer.write_tag(3, WireType::LengthDelimited)?;
1778 header_writer.write_uint64_no_tag(encoded.payload.len() as u64)?;
1779 header_writer.flush()?;
1780 header_writer.total_bytes_written() as usize
1781 };
1782
1783 let uncompressed_len = header_len + encoded.payload.len();
1784
1785 let buffer = ChunkedBytesBuffer::new(RB_BUFFER_CHUNK_SIZE);
1786 let mut compressor = Compressor::from_scheme(compression_scheme, buffer);
1787 compressor
1788 .write_all(&header_buf[..header_len])
1789 .await
1790 .error_context("Failed to compress V3 payload.")?;
1791 compressor
1792 .write_all(&encoded.payload)
1793 .await
1794 .error_context("Failed to compress V3 payload.")?;
1795 compressor
1796 .flush()
1797 .await
1798 .error_context("Failed to flush V3 compressor.")?;
1799 compressor
1800 .shutdown()
1801 .await
1802 .error_context("Failed to shutdown V3 compressor.")?;
1803
1804 let compressed_buf = compressor.into_inner().freeze();
1805 let compressed_len = compressed_buf.len();
1806
1807 let mut builder = Request::builder()
1808 .method(Method::POST)
1809 .uri(endpoint_uri)
1810 .header(http::header::CONTENT_TYPE, "application/x-protobuf");
1811
1812 if let Some(encoding) = content_encoding_for_scheme(compression_scheme) {
1813 builder = builder.header(http::header::CONTENT_ENCODING, encoding);
1814 }
1815
1816 let request = builder
1817 .body(compressed_buf)
1818 .map_err(|e| generic_error!("Failed to build V3 request: {}", e))?;
1819
1820 Ok(V3EncodedRequest {
1821 request,
1822 compressed_len,
1823 uncompressed_len,
1824 stats: encoded.stats,
1825 })
1826}
1827
1828fn content_encoding_for_scheme(compression_scheme: CompressionScheme) -> Option<HeaderValue> {
1829 match compression_scheme {
1830 CompressionScheme::Noop => None,
1831 CompressionScheme::Gzip(_) => Some(HeaderValue::from_static("gzip")),
1832 CompressionScheme::Zlib(_) => Some(HeaderValue::from_static("deflate")),
1833 CompressionScheme::Zstd(_) => Some(HeaderValue::from_static("zstd")),
1834 }
1835}
1836
1837#[cfg(test)]
1838mod tests {
1839 use std::{collections::HashMap, io::Cursor};
1840
1841 use agent_data_plane_config::{
1842 shared::{
1843 AltMetricsIntake, MetricsEncoding as TypedMetricsEncoding, V3ApiSettings as TypedV3ApiSettings,
1844 V3SeriesMode,
1845 },
1846 ConfigValue,
1847 };
1848 use bytes::Bytes;
1849 use datadog_protos::metrics::v3::MetricData as V3MetricData;
1850 use protobuf::Message as _;
1851 use saluki_core::components::test_util::TestComponentDriver;
1852 use saluki_core::data_model::{
1853 event::{
1854 metric::{context::Context, MetricMetadata},
1855 Event,
1856 },
1857 payload::Payload,
1858 tags::{Tag, TagSet},
1859 };
1860 use saluki_metrics::test::TestRecorder;
1861 use stringtheory::MetaString;
1862 use tokio::time::timeout;
1863
1864 use super::*;
1865 use crate::common::datadog::test_util::shared_configuration;
1866
1867 fn metrics_config_from(shared: &SharedConfiguration) -> DatadogMetricsConfiguration {
1869 DatadogMetricsConfiguration::from_configuration(shared)
1870 }
1871
1872 fn agent_series_config() -> UseV3ApiSeriesConfig {
1874 (&TypedMetricsEncoding::default()).into()
1875 }
1876
1877 fn catalog_for(shared: &SharedConfiguration) -> Arc<RoutingTargetCatalog> {
1879 Arc::new(
1880 RoutingTargetCatalog::new(
1881 &shared.endpoints.primary_endpoint(),
1882 &shared.endpoints.additional_endpoints,
1883 )
1884 .expect("catalog should build"),
1885 )
1886 }
1887
1888 #[test]
1889 fn routing_targets_address_series_and_sketch_payloads() {
1890 use crate::common::datadog::{
1891 METRICS_SERIES_V1_PATH, METRICS_SERIES_V2_PATH, METRICS_SERIES_V3_BETA_PATH, METRICS_SKETCHES_PATH,
1892 };
1893
1894 let catalog = catalog_for(&shared_configuration());
1895 for targets in [RoutingTargetSet::new(), catalog.select(|_| true)] {
1896 let routing_targets = Arc::new(MetricsRoutingTargets::new(targets));
1897 for (path, info) in [
1898 (METRICS_SERIES_V1_PATH, MetricsPayloadInfo::v2_series()),
1899 (METRICS_SERIES_V2_PATH, MetricsPayloadInfo::v2_series()),
1900 (METRICS_SERIES_V3_BETA_PATH, MetricsPayloadInfo::v3_series()),
1901 (METRICS_SERIES_V3_PATH, MetricsPayloadInfo::v3_series()),
1902 (METRICS_SKETCHES_PATH, MetricsPayloadInfo::v2_sketches()),
1903 (METRICS_SKETCHES_V3_PATH, MetricsPayloadInfo::v3_sketches()),
1904 ] {
1905 for tagged in [false, true] {
1906 let mut metadata = PayloadMetadata::from_event_count(1);
1907 if tagged {
1908 metadata.set(info);
1909 }
1910 let body = ChunkedBytesBuffer::new(RB_BUFFER_CHUNK_SIZE).freeze();
1911 let request = Request::builder().uri(path).body(body).unwrap();
1912 let payload = Payload::Http(HttpPayload::new(metadata, request));
1913 let routed = apply_routing_targets(payload, Some(&routing_targets))
1914 .try_into_http_payload()
1915 .unwrap();
1916 let (metadata, _) = routed.into_parts();
1917 let payload_targets = metadata.get::<Arc<MetricsRoutingTargets>>();
1918 assert!(Arc::ptr_eq(payload_targets.unwrap(), &routing_targets));
1919 assert_eq!(metadata.get::<MetricsPayloadInfo>(), tagged.then_some(&info));
1920 }
1921 }
1922 }
1923 }
1924
1925 #[test]
1934 fn v3_api_settings_come_from_resolved_configuration() {
1935 let mut shared = shared_configuration();
1936 shared.metrics_encoding.v3_api.compression_level = 7;
1937 shared.metrics_encoding.v3_api.sketches = TypedV3ApiSettings {
1938 endpoints: vec!["https://app.datadoghq.eu".to_string()],
1939 };
1940
1941 let config = metrics_config_from(&shared);
1942
1943 assert_eq!(7, config.v3_api.compression_level);
1944 assert_eq!(
1945 Some("https://app.datadoghq.eu"),
1946 config.v3_api.sketches.endpoints.first().map(String::as_str)
1947 );
1948 }
1949
1950 #[test]
1951 fn metrics_routing_comes_from_resolved_configuration() {
1952 let mut shared = shared_configuration();
1953 shared.endpoints.compression.compressor_kind = "zstd".to_string();
1954 shared.endpoints.opw_intake = AltMetricsIntake {
1955 enabled: true,
1956 url: "https://opw.example.com".to_string(),
1957 use_v3_series: true,
1958 };
1959 shared.metrics_encoding.use_v2_series_api = false;
1960 shared.metrics_encoding.v3_api.compression_level = 7;
1961 shared.metrics_encoding.v3_series_mode = V3SeriesMode::Disabled;
1962 shared.metrics_encoding.v3_series_endpoint_modes =
1963 HashMap::from([("https://app.datadoghq.com".to_string(), V3SeriesMode::Enabled)]);
1964
1965 let config = metrics_config_from(&shared);
1966
1967 assert_eq!("zstd", config.compressor_kind);
1968 assert!(!config.use_v2_series_api);
1969 assert_eq!(7, config.v3_api.compression_level);
1970 assert_eq!(V3SeriesMode::Disabled, config.use_v3_api.series.enabled);
1971 assert_eq!(
1972 Some(&V3SeriesMode::Enabled),
1973 config.use_v3_api.series.endpoints.get("https://app.datadoghq.com")
1974 );
1975 let opw = config
1977 .opw_metrics
1978 .selected_endpoint()
1979 .expect("alternate intake routing should be selected");
1980 assert_eq!("https://opw.example.com", opw.url);
1981 assert!(opw.use_v3_series);
1982 }
1983
1984 #[test]
1985 fn metrics_v3_disabled_by_compressor_uses_v2_only() {
1986 assert_eq!(MetricsEncoderMode::V2Only, metrics_encoder_mode_for_config(true, true));
1987 assert_eq!(
1988 MetricsEncoderMode::V3Enabled,
1989 metrics_encoder_mode_for_config(true, false)
1990 );
1991 }
1992
1993 #[test]
1994 fn mixed_v2_and_v3_endpoints_require_both_series_encoders() {
1995 let mut shared = shared_configuration();
1998 shared.metrics_encoding.v3_series_mode = V3SeriesMode::DatadogOnly;
1999 shared.endpoints.additional_endpoints = HashMap::from([(
2000 "https://custom.example.com".to_string(),
2001 vec!["additional-api-key".to_string()],
2002 )]);
2003 let config = metrics_config_from(&shared);
2004
2005 assert!(config.requires_v2_series(false).expect("endpoints should resolve"));
2006 assert!(config.requires_v3_series(false).expect("endpoints should resolve"));
2007 }
2008
2009 #[test]
2010 fn routing_targets_limit_protocol_selection_to_payload_targets() {
2011 let mut shared = shared_configuration();
2015 shared.metrics_encoding.v3_series_mode = V3SeriesMode::DatadogOnly;
2016 shared.endpoints.additional_endpoints = HashMap::from([(
2017 "https://custom.example.com".to_string(),
2018 vec!["additional-api-key".to_string()],
2019 )]);
2020 let catalog = catalog_for(&shared);
2021 let primary = catalog.select(|target| target.kind() == RoutingTargetKind::Primary);
2022 let additional = catalog.select(|target| target.kind() == RoutingTargetKind::Additional);
2023
2024 for (targets, expects_v2, expects_v3) in [
2025 (primary, false, true),
2026 (additional, true, false),
2027 (catalog.select(|_| true), true, true),
2028 (RoutingTargetSet::new(), false, false),
2029 ] {
2030 let config = metrics_config_from(&shared).with_routing(Arc::clone(&catalog), targets);
2031 assert_eq!(config.requires_v2_series(false).unwrap(), expects_v2);
2032 assert_eq!(config.requires_v3_series(false).unwrap(), expects_v3);
2033 }
2034 }
2035
2036 #[test]
2037 fn alternate_metrics_intakes_inherit_the_primary_target_but_keep_their_protocol() {
2038 for use_vector in [false, true] {
2039 for use_v3_series in [false, true] {
2040 let mut shared = shared_configuration();
2041 shared.metrics_encoding.v3_series_mode = if use_v3_series {
2043 V3SeriesMode::Disabled
2044 } else {
2045 V3SeriesMode::Enabled
2046 };
2047 let alternate = AltMetricsIntake {
2048 enabled: true,
2049 url: "https://alternate.example.com".to_string(),
2050 use_v3_series,
2051 };
2052 if use_vector {
2053 shared.endpoints.vector_intake = alternate;
2054 } else {
2055 shared.endpoints.opw_intake = alternate;
2056 }
2057 shared.endpoints.additional_endpoints = HashMap::from([(
2058 "https://alternate.example.com".to_string(),
2059 vec!["additional-key".to_string()],
2060 )]);
2061 let catalog = catalog_for(&shared);
2062 let primary = catalog.select(|target| target.kind() == RoutingTargetKind::Primary);
2063 let additional = catalog.select(|target| target.kind() == RoutingTargetKind::Additional);
2064 for (targets, expects_v3) in [(primary, use_v3_series), (additional, !use_v3_series)] {
2065 let config = metrics_config_from(&shared).with_routing(Arc::clone(&catalog), targets);
2066 assert_eq!(config.requires_v2_series(false).unwrap(), !expects_v3);
2067 assert_eq!(config.requires_v3_series(false).unwrap(), expects_v3);
2068 }
2069 }
2070 }
2071 }
2072
2073 #[test]
2074 fn all_v2_endpoints_do_not_require_v3_series() {
2075 let mut shared = shared_configuration();
2076 shared.metrics_encoding.v3_series_mode = V3SeriesMode::DatadogOnly;
2077 shared.endpoints.dd_url = ConfigValue::explicit("http://127.0.0.1:9091".to_string());
2078 let config = metrics_config_from(&shared);
2079
2080 assert!(config.requires_v2_series(false).expect("endpoints should resolve"));
2081 assert!(!config.requires_v3_series(false).expect("endpoints should resolve"));
2082 }
2083
2084 #[test]
2085 fn enabled_v3_series_mode_requires_only_v3_series() {
2086 let mut shared = shared_configuration();
2087 shared.metrics_encoding.v3_series_mode = V3SeriesMode::Enabled;
2088 let config = metrics_config_from(&shared);
2089
2090 assert!(!config.requires_v2_series(false).expect("endpoints should resolve"));
2091 assert!(config.requires_v3_series(false).expect("endpoints should resolve"));
2092 }
2093
2094 #[test]
2095 fn v1_series_configuration_takes_precedence_over_v3() {
2096 let mut shared = shared_configuration();
2097 shared.metrics_encoding.use_v2_series_api = false;
2098 shared.metrics_encoding.v3_series_mode = V3SeriesMode::Enabled;
2099 let config = metrics_config_from(&shared);
2100
2101 let series_v3_can_be_enabled =
2102 series_v3_can_be_enabled_for_config(config.use_v2_series_api, None, false, &config.use_v3_api.series);
2103
2104 assert!(!series_v3_can_be_enabled);
2105
2106 assert_eq!(
2107 MetricsEncoderMode::V2Only,
2108 metrics_encoder_mode_for_config(series_v3_can_be_enabled, false)
2109 );
2110
2111 assert!(
2112 config.requires_v2_series(false).expect("endpoint should resolve"),
2113 "V1 configuration must retain the legacy series builder even when V3 is enabled"
2114 );
2115 }
2116
2117 #[test]
2118 fn endpoint_override_uses_the_overridden_endpoint_protocol() {
2119 let mut shared = shared_configuration();
2120 shared.metrics_encoding.v3_series_mode = V3SeriesMode::Disabled;
2121 shared.endpoints.dd_url = ConfigValue::explicit("https://primary.example.com".to_string());
2122 shared.metrics_encoding.v3_series_endpoint_modes =
2123 HashMap::from([("https://v3-mrf.example.com".to_string(), V3SeriesMode::Enabled)]);
2124 let config = metrics_config_from(&shared);
2125
2126 let v2_mrf_config = config
2127 .clone()
2128 .with_metrics_endpoint_override("https://v2-mrf.example.com".to_string());
2129 let v3_mrf_config = config.with_metrics_endpoint_override("https://v3-mrf.example.com".to_string());
2130
2131 assert!(v2_mrf_config
2132 .requires_v2_series(false)
2133 .expect("V2 MRF endpoint should resolve"));
2134 assert!(!v2_mrf_config
2135 .requires_v3_series(false)
2136 .expect("V2 MRF endpoint should resolve"));
2137 assert!(!v3_mrf_config
2138 .requires_v2_series(false)
2139 .expect("V3 MRF endpoint should resolve"));
2140 assert!(v3_mrf_config
2141 .requires_v3_series(false)
2142 .expect("V3 MRF endpoint should resolve"));
2143 }
2144
2145 #[test]
2146 fn an_endpoint_override_drops_the_configured_additional_and_alternate_endpoints() {
2147 let mut shared = shared_configuration();
2148 shared.endpoints.dd_url = ConfigValue::explicit("https://primary.example.com".to_string());
2149 shared.endpoints.additional_endpoints = HashMap::from([(
2150 "https://additional.example.com".to_string(),
2151 vec!["additional-api-key".to_string()],
2152 )]);
2153 shared.endpoints.opw_intake = AltMetricsIntake {
2154 enabled: true,
2155 url: "https://opw.example.com".to_string(),
2156 use_v3_series: true,
2157 };
2158
2159 let config = metrics_config_from(&shared).with_metrics_endpoint_override("https://mrf.example.com".to_string());
2160
2161 assert_eq!("https://mrf.example.com", config.primary_endpoint);
2162 assert!(config.additional_endpoints.is_empty());
2163 assert_eq!(None, config.opw_metrics.selected_endpoint().map(|opw| opw.url));
2164 }
2165
2166 #[test]
2167 fn v2_series_only_override_requires_only_v2_series() {
2168 let mut shared = shared_configuration();
2169 shared.metrics_encoding.v3_series_mode = V3SeriesMode::Enabled;
2170 let config = metrics_config_from(&shared).with_v2_series_only();
2171
2172 assert_eq!(V3SeriesMode::Disabled, config.use_v3_api.series.enabled);
2173 assert!(config.use_v3_api.series.endpoints.is_empty());
2174 assert!(config.requires_v2_series(false).expect("endpoint should resolve"));
2175 assert!(!config.requires_v3_series(false).expect("endpoint should resolve"));
2176 }
2177
2178 #[test]
2179 fn agent_default_v3_does_not_enable_opw_only_encoder_mode() {
2180 let series_config = agent_series_config();
2181 let mut shared = shared_configuration();
2183 shared.endpoints.opw_intake = AltMetricsIntake {
2184 enabled: true,
2185 url: "http://[::1".to_string(),
2186 use_v3_series: false,
2187 };
2188 shared.endpoints.vector_intake = AltMetricsIntake {
2189 enabled: true,
2190 url: "http://vector.example.com".to_string(),
2191 use_v3_series: false,
2192 };
2193 let opw_metrics = OpwMetricsConfiguration::from_configuration(&shared.endpoints);
2194 let invalid_metrics_primary_override = selected_metrics_primary_v3_override(&opw_metrics);
2195
2196 assert!(!series_v3_can_be_enabled_for_config(
2197 true,
2198 Some(false),
2199 false,
2200 &series_config
2201 ));
2202 assert!(series_v3_can_be_enabled_for_config(
2203 true,
2204 Some(true),
2205 false,
2206 &series_config
2207 ));
2208 assert!(series_v3_can_be_enabled_for_config(
2209 true,
2210 Some(false),
2211 true,
2212 &series_config
2213 ));
2214 assert_eq!(None, invalid_metrics_primary_override);
2215 assert!(series_v3_can_be_enabled_for_config(
2216 true,
2217 invalid_metrics_primary_override,
2218 false,
2219 &series_config
2220 ));
2221 }
2222
2223 #[test]
2224 fn v3_encodes_zero_count_distribution_with_bins() {
2225 let mut sketch = DDSketch::default();
2226 sketch.insert_n(42.0, 10);
2227 sketch.set_count(0);
2228 sketch.set_min(40.0);
2229 sketch.set_max(45.0);
2230 sketch.set_sum(123.0);
2231 sketch.set_avg(f64::NAN);
2232
2233 let metric = Metric::from_parts(
2234 Context::from_static_parts("zero.count.distribution", &[]),
2235 MetricValues::distribution((123_u64, sketch)),
2236 MetricMetadata::default(),
2237 );
2238
2239 let encoded = encode_v3_metrics_batch(&[metric], &SharedTagSet::default())
2240 .expect("zero-count distribution should encode to V3");
2241 let metric_data = V3MetricData::parse_from_bytes(&encoded.payload).expect("V3 metric data should decode");
2242
2243 assert_eq!(metric_data.numPoints, vec![1]);
2244 }
2245
2246 #[test]
2247 fn v3_skips_empty_distribution() {
2248 let metric = Metric::from_parts(
2249 Context::from_static_parts("empty.distribution", &[]),
2250 MetricValues::distribution((123_u64, DDSketch::default())),
2251 MetricMetadata::default(),
2252 );
2253
2254 let encoded =
2255 encode_v3_metrics_batch(&[metric], &SharedTagSet::default()).expect("empty distribution should encode");
2256 let metric_data = V3MetricData::parse_from_bytes(&encoded.payload).expect("V3 metric data should decode");
2257
2258 assert!(metric_data.numPoints.is_empty());
2259 }
2260
2261 #[tokio::test]
2262 async fn create_v3_request_uses_configured_endpoint_uri() {
2263 let encoded = V3Writer::new().finalize().expect("empty V3 payload should encode");
2264 let request = create_v3_request("/api/intake/metrics/custom/series", encoded, CompressionScheme::noop())
2265 .await
2266 .expect("request should be created");
2267
2268 assert_eq!("/api/intake/metrics/custom/series", request.request.uri());
2269 }
2270
2271 #[tokio::test]
2272 async fn create_v3_request_uses_single_stream_body_and_column_telemetry() {
2273 let metrics = vec![Metric::counter("v3.single.stream", 42.0)];
2274 let encoded = encode_v3_metrics_batch(&metrics, &SharedTagSet::default()).expect("metrics should encode to V3");
2275 let expected_payload = encoded.payload.clone();
2276
2277 let mut request = create_v3_request(METRICS_SERIES_V3_PATH, encoded, CompressionScheme::noop())
2278 .await
2279 .expect("request should be created");
2280 measure_v3_column_compressed_sizes(&mut request.stats, CompressionScheme::noop())
2281 .await
2282 .expect("column compressed sizes should be measured");
2283
2284 for column in &request.stats.columns {
2285 assert_eq!(column.compressed_len, column.bytes.len());
2286 }
2287
2288 let mut expected_body = Vec::new();
2289 {
2290 let mut os = CodedOutputStream::vec(&mut expected_body);
2291 os.write_tag(3, WireType::LengthDelimited).unwrap();
2292 os.write_uint64_no_tag(expected_payload.len() as u64).unwrap();
2293 os.flush().unwrap();
2294 }
2295 expected_body.extend_from_slice(&expected_payload);
2296
2297 assert_eq!(request.request.into_body().into_bytes(), Bytes::from(expected_body));
2298 }
2299
2300 #[tokio::test]
2301 async fn create_v3_request_zstd_body_decodes_to_metric_payload() {
2302 let metrics = vec![Metric::counter("v3.single.stream.zstd", 42.0)];
2303 let encoded = encode_v3_metrics_batch(&metrics, &SharedTagSet::default()).expect("metrics should encode to V3");
2304 let expected_payload = encoded.payload.clone();
2305
2306 let mut request = create_v3_request(METRICS_SERIES_V3_PATH, encoded, CompressionScheme::zstd_default())
2307 .await
2308 .expect("request should be created");
2309 measure_v3_column_compressed_sizes(&mut request.stats, CompressionScheme::zstd_default())
2310 .await
2311 .expect("column compressed sizes should be measured");
2312
2313 for column in &request.stats.columns {
2314 let expected_compressed_len = compressed_v3_len(&column.bytes, CompressionScheme::zstd_default())
2315 .await
2316 .expect("column compressed size should be measured");
2317 assert_eq!(column.compressed_len, expected_compressed_len);
2318 }
2319
2320 let mut expected_body = Vec::new();
2321 {
2322 let mut os = CodedOutputStream::vec(&mut expected_body);
2323 os.write_tag(3, WireType::LengthDelimited).unwrap();
2324 os.write_uint64_no_tag(expected_payload.len() as u64).unwrap();
2325 os.flush().unwrap();
2326 }
2327 expected_body.extend_from_slice(&expected_payload);
2328
2329 let decoded_body = zstd::stream::decode_all(Cursor::new(request.request.into_body().into_bytes()))
2330 .expect("compressed V3 body should decode");
2331 assert_eq!(decoded_body, expected_body);
2332 }
2333
2334 #[test]
2335 fn v3_drops_non_finite_scalar_points() {
2336 const NUM_POINTS_FIELD_NUMBER: u32 = 15;
2337 const VALUE_SINT64_FIELD_NUMBER: u32 = 17;
2338
2339 let metrics = vec![
2340 Metric::from_parts(
2341 Context::from_static_parts("v3.finite.counter", &[]),
2342 MetricValues::counter([(1, 1.0_f64), (2, f64::NAN), (3, f64::INFINITY), (4, 2.0)]),
2343 MetricMetadata::default(),
2344 ),
2345 Metric::from_parts(
2346 Context::from_static_parts("v3.finite.gauge", &[]),
2347 MetricValues::gauge([(1, 3.0_f64), (2, f64::NAN), (3, 4.0)]),
2348 MetricMetadata::default(),
2349 ),
2350 Metric::from_parts(
2351 Context::from_static_parts("v3.finite.rate", &[]),
2352 MetricValues::rate(
2353 [(1, 30.0_f64), (2, f64::INFINITY), (3, f64::NAN)],
2354 Duration::from_secs(10),
2355 ),
2356 MetricMetadata::default(),
2357 ),
2358 Metric::set("v3.finite.set", "alpha"),
2359 ];
2360
2361 let encoded = encode_v3_metrics_batch(&metrics, &SharedTagSet::default()).expect("metrics should encode to V3");
2362 let column = |field_number| {
2363 encoded
2364 .stats
2365 .columns
2366 .iter()
2367 .find(|column| column.field_number == field_number)
2368 .map(|column| column.bytes.as_slice())
2369 };
2370
2371 assert_eq!(column(NUM_POINTS_FIELD_NUMBER), Some(&[2, 2, 1, 1][..]));
2372 assert_eq!(column(VALUE_SINT64_FIELD_NUMBER), Some(&[2, 4, 6, 8, 6, 2][..]));
2374 assert_eq!(encoded.stats.value_encoding_stats.sint64, 6);
2375 assert_eq!(encoded.stats.value_encoding_stats.float64, 0);
2376 }
2377
2378 #[test]
2379 fn v3_encodes_zero_interval_rate_without_scaling() {
2380 const NUM_POINTS_FIELD_NUMBER: u32 = 15;
2381 const VALUE_SINT64_FIELD_NUMBER: u32 = 17;
2382
2383 let metrics = vec![Metric::rate("v3.unnormalized.rate", 42.0, Duration::ZERO)];
2384 let encoded = encode_v3_metrics_batch(&metrics, &SharedTagSet::default()).expect("rate should encode to V3");
2385 let column = |field_number| {
2386 encoded
2387 .stats
2388 .columns
2389 .iter()
2390 .find(|column| column.field_number == field_number)
2391 .map(|column| column.bytes.as_slice())
2392 };
2393
2394 assert_eq!(column(NUM_POINTS_FIELD_NUMBER), Some(&[1][..]));
2395 assert_eq!(column(VALUE_SINT64_FIELD_NUMBER), Some(&[84][..]));
2396 }
2397
2398 #[tokio::test]
2399 async fn v3_serializer_stats_record_agent_style_value_and_column_counters() {
2400 const VALUE_SINT64_FIELD_NUMBER: u32 = 17;
2401
2402 let recorder = TestRecorder::default();
2403 let _local = metrics::set_default_local_recorder(&recorder);
2404 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2405 let metrics = vec![
2406 Metric::gauge("v3.telemetry.zero", [(123, 0.0), (124, 0.0)]),
2407 Metric::counter("v3.telemetry.sint64", [(123, 100.0), (124, 200.0)]),
2408 Metric::gauge("v3.telemetry.float32", [(123, 1.5), (124, 2.25)]),
2409 Metric::gauge("v3.telemetry.float64", [(123, (1i64 << 30) as f64), (124, 1.5)]),
2410 ];
2411 let encoded = encode_v3_metrics_batch(&metrics, &SharedTagSet::default()).expect("metrics should encode to V3");
2412 let mut request = create_v3_request(METRICS_SERIES_V3_PATH, encoded, CompressionScheme::noop())
2413 .await
2414 .expect("request should be created");
2415 measure_v3_column_compressed_sizes(&mut request.stats, CompressionScheme::noop())
2416 .await
2417 .expect("column compressed sizes should be measured");
2418
2419 let value_sint64_column = request
2420 .stats
2421 .columns
2422 .iter()
2423 .find(|column| column.field_number == VALUE_SINT64_FIELD_NUMBER)
2424 .expect("sint64 value column should be present");
2425 let value_sint64_len = value_sint64_column.bytes.len() as u64;
2426 assert_eq!(value_sint64_column.compressed_len as u64, value_sint64_len);
2427
2428 record_v3_serializer_stats(&serializer_telemetry, &request.stats);
2429
2430 assert_eq!(
2431 recorder.counter(("serializer.v3_values_count", &[("type", "zero")])),
2432 Some(2)
2433 );
2434 assert_eq!(
2435 recorder.counter(("serializer.v3_values_count", &[("type", "sint64")])),
2436 Some(2)
2437 );
2438 assert_eq!(
2439 recorder.counter(("serializer.v3_values_count", &[("type", "float32")])),
2440 Some(2)
2441 );
2442 assert_eq!(
2443 recorder.counter(("serializer.v3_values_count", &[("type", "float64")])),
2444 Some(2)
2445 );
2446 assert_eq!(
2447 recorder.counter((
2448 "serializer.v3_column_size",
2449 &[("column", "ValueSint64"), ("compressed", "uncompressed")]
2450 )),
2451 Some(value_sint64_len)
2452 );
2453 assert_eq!(
2454 recorder.counter((
2455 "serializer.v3_column_size",
2456 &[("column", "ValueSint64"), ("compressed", "compressed")]
2457 )),
2458 Some(value_sint64_len)
2459 );
2460 }
2461
2462 async fn create_v3_test_request(metrics: &[Metric]) -> V3EncodedRequest {
2463 let encoded = encode_v3_metrics_batch(metrics, &SharedTagSet::default()).expect("metrics should encode to V3");
2464 create_v3_request(METRICS_SERIES_V3_PATH, encoded, CompressionScheme::noop())
2465 .await
2466 .expect("request should be created")
2467 }
2468
2469 fn test_v3_flush_context<'a>(
2470 ep_config: &'a EndpointConfiguration, payload_limits: V3PayloadLimits,
2471 serializer_telemetry: &'a V3SerializerTelemetry, telemetry: &'a ComponentTelemetry,
2472 ) -> V3FlushContext<'a> {
2473 V3FlushContext {
2474 endpoint_config: ep_config,
2475 payload_limits,
2476 series_endpoint_uri: METRICS_SERIES_V3_PATH,
2477 serializer_telemetry,
2478 telemetry,
2479 }
2480 }
2481
2482 struct CollectedV3Payload {
2484 event_count: usize,
2485 data_point_count: usize,
2486 request: Request<FrozenChunkedBytesBuffer>,
2487 }
2488
2489 async fn collect_v3_payload_requests(metrics: &[Metric], context: V3FlushContext<'_>) -> Vec<CollectedV3Payload> {
2494 let (mut payloads_tx, mut payloads_rx) = tokio::sync::mpsc::channel(64);
2495 let mut ratio = V3CompressionRatio::default();
2496 encode_and_flush_v3_payload_requests(
2497 METRICS_SERIES_V3_PATH,
2498 metrics,
2499 context,
2500 "series",
2501 &mut ratio,
2502 &mut payloads_tx,
2503 None,
2504 )
2505 .await
2506 .expect("payload requests should encode and flush");
2507 drop(payloads_tx);
2508
2509 let mut collected = Vec::new();
2510 while let Some(payload) = payloads_rx.recv().await {
2511 let Payload::Http(http_payload) = payload else {
2512 panic!("expected HTTP payload");
2513 };
2514 let (metadata, request) = http_payload.into_parts();
2515 collected.push(CollectedV3Payload {
2516 event_count: metadata.event_count(),
2517 data_point_count: metadata.data_point_count(),
2518 request,
2519 });
2520 }
2521
2522 collected
2523 }
2524
2525 #[tokio::test]
2526 async fn v3_payload_requests_split_by_compressed_size_limit() {
2527 let metrics = vec![
2528 Metric::counter("v3.compressed.split.one", 1.0),
2529 Metric::counter("v3.compressed.split.two", 2.0),
2530 ];
2531 let single_request = create_v3_test_request(&metrics[..1]).await;
2532 let combined_request = create_v3_test_request(&metrics).await;
2533 assert!(combined_request.compressed_len > single_request.compressed_len);
2534
2535 let limits = V3PayloadLimits::new(single_request.compressed_len, usize::MAX, 10_000, 10_000);
2536 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, usize::MAX, None);
2537 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2538 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2539
2540 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2541 let requests = collect_v3_payload_requests(&metrics, context).await;
2542
2543 assert_eq!(2, requests.len());
2544 assert_eq!(
2545 vec![1, 1],
2546 requests.iter().map(|request| request.event_count).collect::<Vec<_>>()
2547 );
2548 assert!(requests
2549 .iter()
2550 .all(|request| request.request.body().len() <= limits.max_compressed_size));
2551 }
2552
2553 #[tokio::test]
2554 async fn v3_byte_fallback_does_not_resurrect_metric_over_point_limit() {
2555 let point_oversized_metric = Metric::counter("v3.fallback.over.point.limit", [(123, 1.0), (124, 2.0)]);
2556
2557 let byte_oversized_metric = Metric::counter(
2558 concat!(
2559 "v3.fallback.over.byte.limit.",
2560 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2561 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
2562 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
2563 "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
2564 ),
2565 3.0,
2566 );
2567
2568 let point_oversized_request = create_v3_test_request(std::slice::from_ref(&point_oversized_metric)).await;
2569 let byte_oversized_request = create_v3_test_request(std::slice::from_ref(&byte_oversized_metric)).await;
2570
2571 assert!(byte_oversized_request.compressed_len > point_oversized_request.compressed_len);
2572
2573 let limits = V3PayloadLimits::new(point_oversized_request.compressed_len, usize::MAX, 10_000, 1);
2574
2575 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, usize::MAX, None);
2576 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2577 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2578 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2579
2580 let requests = collect_v3_payload_requests(&[point_oversized_metric, byte_oversized_metric], context).await;
2581
2582 let emitted_point_counts = requests
2583 .iter()
2584 .map(|request| request.data_point_count)
2585 .collect::<Vec<_>>();
2586
2587 assert!(
2588 emitted_point_counts
2589 .iter()
2590 .all(|point_count| *point_count <= limits.max_points_per_payload),
2591 "fallback emitted request point counts {emitted_point_counts:?} above limit {}",
2592 limits.max_points_per_payload
2593 );
2594 }
2595
2596 #[tokio::test]
2597 async fn v3_serializer_stats_record_payload_full_split_reason() {
2598 let metrics = vec![
2599 Metric::counter("v3.telemetry.payload_full.one", 1.0),
2600 Metric::counter("v3.telemetry.payload_full.two", 2.0),
2601 ];
2602 let single_request = create_v3_test_request(&metrics[..1]).await;
2603 let limits = V3PayloadLimits::new(single_request.compressed_len, usize::MAX, 10_000, 10_000);
2604 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, usize::MAX, None);
2605 let recorder = TestRecorder::default();
2606 let _local = metrics::set_default_local_recorder(&recorder);
2607 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2608 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2609
2610 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2611 let requests = collect_v3_payload_requests(&metrics, context).await;
2612
2613 assert_eq!(2, requests.len());
2614 assert_eq!(
2615 recorder.counter(("serializer.v3_payload_split_reason", &[("reason", "payload_full")])),
2616 Some(1)
2617 );
2618 }
2619
2620 #[tokio::test]
2621 async fn v3_serializer_stats_record_item_too_big_split_reason() {
2622 let metrics = vec![Metric::counter("v3.telemetry.item_too_big", 1.0)];
2623 let request = create_v3_test_request(&metrics).await;
2624 let limits = V3PayloadLimits::new(request.compressed_len - 1, usize::MAX, 10_000, 10_000);
2625 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, usize::MAX, None);
2626 let recorder = TestRecorder::default();
2627 let _local = metrics::set_default_local_recorder(&recorder);
2628 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2629 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2630
2631 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2632 let requests = collect_v3_payload_requests(&metrics, context).await;
2633
2634 assert!(requests.is_empty());
2635 assert_eq!(recorder.counter("serializer.v3_item_too_big"), Some(1));
2636 assert_eq!(
2637 recorder.counter(("serializer.v3_payload_split_reason", &[("reason", "item_too_big")])),
2638 Some(1)
2639 );
2640 }
2641
2642 #[tokio::test]
2643 async fn v3_payload_requests_split_by_uncompressed_size_limit() {
2644 let metrics = vec![
2645 Metric::counter("v3.uncompressed.split.one", 1.0),
2646 Metric::counter("v3.uncompressed.split.two", 2.0),
2647 ];
2648 let single_request = create_v3_test_request(&metrics[..1]).await;
2649 let combined_request = create_v3_test_request(&metrics).await;
2650 assert!(combined_request.uncompressed_len > single_request.uncompressed_len);
2651
2652 let limits = V3PayloadLimits::new(usize::MAX, single_request.uncompressed_len, 10_000, 10_000);
2653 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, usize::MAX, None);
2654 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2655 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2656
2657 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2658 let requests = collect_v3_payload_requests(&metrics, context).await;
2659
2660 assert_eq!(2, requests.len());
2661 assert_eq!(
2662 vec![1, 1],
2663 requests.iter().map(|request| request.event_count).collect::<Vec<_>>()
2664 );
2665 }
2666
2667 fn collect_v3_batches(
2669 metrics: &[Metric], context: V3FlushContext<'_>, target_uncompressed_len: usize,
2670 ) -> Vec<(Range<usize>, usize)> {
2671 let mut batches = Vec::new();
2672 let mut idx = 0;
2673 while idx < metrics.len() {
2674 let start = idx;
2675 match build_v3_batch(metrics, &mut idx, context, "series", target_uncompressed_len) {
2676 Some(batch) => batches.push((start..idx, batch.event_count)),
2677 None if idx == start => break,
2678 None => {}
2679 }
2680 }
2681
2682 batches
2683 }
2684
2685 #[test]
2686 fn v3_batches_are_cut_at_the_point_limit() {
2687 let metrics = vec![
2688 Metric::counter("v3.points.split.one", [(123, 1.0), (124, 2.0)]),
2689 Metric::counter("v3.points.split.two", [(123, 3.0), (124, 4.0)]),
2690 Metric::counter("v3.points.split.three", 5.0),
2691 ];
2692 let limits = V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 3);
2693 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, usize::MAX, None);
2694 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2695 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2696 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2697
2698 let batches = collect_v3_batches(&metrics, context, usize::MAX);
2699
2700 assert_eq!(vec![(0..1, 1), (1..3, 2)], batches);
2701 }
2702
2703 #[test]
2704 fn v3_batches_are_cut_at_the_metric_limit() {
2705 let metrics = vec![
2706 Metric::counter("v3.metric.limit.a", 1.0),
2707 Metric::counter("v3.metric.limit.b", 2.0),
2708 Metric::counter("v3.metric.limit.c", 3.0),
2709 Metric::counter("v3.metric.limit.d", 4.0),
2710 Metric::counter("v3.metric.limit.e", 5.0),
2711 ];
2712 let limits = V3PayloadLimits::new(usize::MAX, usize::MAX, 2, 10_000);
2713 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 2, usize::MAX, None);
2714 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2715 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2716 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2717
2718 let batches = collect_v3_batches(&metrics, context, usize::MAX);
2719
2720 assert_eq!(vec![(0..2, 2), (2..4, 2), (4..5, 1)], batches);
2721 }
2722
2723 #[test]
2724 fn v3_batches_are_cut_at_the_uncompressed_byte_target() {
2725 let metrics = vec![
2726 Metric::counter("v3.byte.target.a", 1.0),
2727 Metric::counter("v3.byte.target.b", 2.0),
2728 Metric::counter("v3.byte.target.c", 3.0),
2729 Metric::counter("v3.byte.target.d", 4.0),
2730 ];
2731 let limits = V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 10_000);
2732 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, usize::MAX, None);
2733 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2734 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2735 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2736
2737 let batches = collect_v3_batches(&metrics, context, 1);
2740
2741 assert_eq!(4, batches.len());
2742 assert!(batches.iter().all(|(_, event_count)| *event_count == 1));
2743 }
2744
2745 #[test]
2746 fn v3_serializer_stats_record_max_points_split_reason() {
2747 let metrics = vec![
2748 Metric::counter("v3.telemetry.max_points.one", [(123, 1.0), (124, 2.0)]),
2749 Metric::counter("v3.telemetry.max_points.two", [(123, 3.0), (124, 4.0)]),
2750 ];
2751 let limits = V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 2);
2752 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, usize::MAX, None);
2753 let recorder = TestRecorder::default();
2754 let _local = metrics::set_default_local_recorder(&recorder);
2755 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2756 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2757 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2758
2759 let batches = collect_v3_batches(&metrics, context, usize::MAX);
2760
2761 assert_eq!(vec![(0..1, 1), (1..2, 1)], batches);
2762 assert_eq!(
2763 recorder.counter(("serializer.v3_payload_split_reason", &[("reason", "max_points")])),
2764 Some(1)
2765 );
2766 }
2767
2768 #[test]
2769 fn v3_batches_drop_oversized_metric_and_keep_batching() {
2770 let metrics = vec![
2771 Metric::counter("v3.points.oversized.before", [(123, 1.0), (124, 2.0)]),
2772 Metric::counter(
2773 "v3.points.oversized.too_big",
2774 [(123, 3.0), (124, 4.0), (125, 5.0), (126, 6.0)],
2775 ),
2776 Metric::counter("v3.points.oversized.after", 7.0),
2777 ];
2778 let limits = V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 3);
2779 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, usize::MAX, None);
2780 let recorder = TestRecorder::default();
2781 let _local = metrics::set_default_local_recorder(&recorder);
2782 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2783 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2784 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2785
2786 let batches = collect_v3_batches(&metrics, context, usize::MAX);
2787
2788 assert_eq!(vec![(0..3, 2)], batches);
2791 assert_eq!(recorder.counter("serializer.v3_item_too_big"), Some(1));
2792 }
2793
2794 #[test]
2795 fn v3_batches_skip_zero_point_metrics_without_splitting() {
2796 let metrics = vec![
2797 Metric::counter("v3.points.zero.before", 1.0),
2798 Metric::counter("v3.points.zero.empty", &[] as &[f64]),
2799 Metric::counter("v3.points.zero.after", 2.0),
2800 ];
2801 let limits = V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 10_000);
2802 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, usize::MAX, None);
2803 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2804 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2805 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2806
2807 let batches = collect_v3_batches(&metrics, context, usize::MAX);
2808
2809 assert_eq!(vec![(0..3, 2)], batches);
2810 }
2811
2812 #[test]
2813 fn v3_compression_ratio_targets_the_compressed_limit() {
2814 let limits = V3PayloadLimits::new(500_000, 5_000_000, 10_000, 10_000);
2815 let mut ratio = V3CompressionRatio::default();
2816
2817 let initial_target = ratio.uncompressed_target(limits);
2820 assert_eq!((500_000.0 * V3_BATCH_TARGET_MARGIN) as usize, initial_target);
2821
2822 ratio.record(4_000_000, 1_000_000);
2824 let observed_target = ratio.uncompressed_target(limits);
2825 assert!(
2826 observed_target > initial_target,
2827 "target should grow once compression is observed: {observed_target} vs {initial_target}"
2828 );
2829
2830 ratio.record(4_000_000, 40_000);
2832 ratio.record(4_000_000, 40_000);
2833 ratio.record(4_000_000, 40_000);
2834 ratio.record(4_000_000, 40_000);
2835 ratio.record(4_000_000, 40_000);
2836 assert!(ratio.uncompressed_target(limits) <= limits.max_uncompressed_size);
2837 }
2838
2839 #[test]
2840 fn v3_series_metric_unit_refs_are_encoded_sparsely() {
2841 let context = Context::from_static_parts("my.timer.avg", &[]);
2842 let metadata = MetricMetadata::default().with_unit(MetaString::from_static("millisecond"));
2843 let gauge = Metric::from_parts(context, MetricValues::gauge([1.0_f64]), metadata);
2844 let context = Context::from_static_parts("my.counter", &[]);
2845 let no_unit = Metric::from_parts(context, MetricValues::gauge([2.0_f64]), MetricMetadata::default());
2846 let context = Context::from_static_parts("my.timer.max", &[]);
2847 let metadata = MetricMetadata::default().with_unit(MetaString::from_static("millisecond"));
2848 let same_unit = Metric::from_parts(context, MetricValues::gauge([3.0_f64]), metadata);
2849
2850 let payload = encode_v3_metrics_batch(&[gauge, no_unit, same_unit], &SharedTagSet::default())
2851 .expect("V3 metric should encode successfully")
2852 .payload;
2853
2854 let expected_unit_dict = [
2855 0xca, 0x01, 0x0c, 0x0b, b'm', b'i', b'l', b'l', b'i', b's', b'e', b'c', b'o', b'n', b'd',
2858 ];
2859 assert!(
2860 payload
2861 .windows(expected_unit_dict.len())
2862 .any(|window| window == expected_unit_dict),
2863 "V3 payload should contain DictUnitStr field for 'millisecond', got bytes: {:?}",
2864 payload
2865 );
2866
2867 let expected_unit_ref = [
2868 0xd2, 0x01, 0x02, 0x02, 0x00, ];
2872 assert!(
2873 payload
2874 .windows(expected_unit_ref.len())
2875 .any(|window| window == expected_unit_ref),
2876 "V3 payload should contain UnitRef field for 'millisecond', got bytes: {:?}",
2877 payload
2878 );
2879 }
2880
2881 #[test]
2882 fn v3_sketch_metric_unit_not_encoded() {
2883 let context = Context::from_static_parts("my.histogram", &[]);
2884 let metadata = MetricMetadata::default().with_unit(MetaString::from_static("millisecond"));
2885 let histogram = Metric::from_parts(context, MetricValues::histogram([1.0_f64]), metadata);
2886
2887 let payload = encode_v3_metrics_batch(&[histogram], &SharedTagSet::default())
2888 .expect("V3 sketch metric should encode successfully")
2889 .payload;
2890
2891 assert!(
2892 !payload
2893 .windows(b"millisecond".len())
2894 .any(|window| window == b"millisecond"),
2895 "V3 sketch payload should not contain unit bytes, matching the Agent V3 sketch builder: {:?}",
2896 payload
2897 );
2898 }
2899
2900 #[test]
2901 fn v3_series_promotes_device_and_internal_resource_tags_to_resources() {
2902 let context = Context::from_static_parts(
2903 "series.resources",
2904 &[
2905 "env:prod",
2906 "device:switch1",
2907 "dd.internal.resource:pod:pod-a",
2908 "dd.internal.resource:malformed",
2909 ],
2910 );
2911 let context = context.with_host(Some(MetaString::from_static("host-a")));
2912 let metadata = MetricMetadata::default();
2913 let metric = Metric::from_parts(context, MetricValues::gauge([1.0_f64]), metadata);
2914
2915 let payload = encode_v3_metrics_batch(&[metric], &SharedTagSet::default())
2916 .expect("V3 series should encode successfully")
2917 .payload;
2918
2919 assert_contains_bytes(&payload, b"env:prod");
2920 assert!(!contains_bytes(&payload, b"device:switch1"));
2921 assert!(!contains_bytes(&payload, b"dd.internal.resource:pod:pod-a"));
2922 assert!(!contains_bytes(&payload, b"dd.internal.resource:malformed"));
2923
2924 let expected_resource_dict = [
2925 0x22, 0x25, 0x04, b'h', b'o', b's', b't', 0x06, b'h', b'o', b's', b't', b'-', b'a', 0x06, b'd', b'e', b'v', b'i', b'c',
2928 b'e', 0x07, b's', b'w', b'i', b't', b'c', b'h', b'1', 0x03, b'p', b'o', b'd', 0x05, b'p', b'o', b'd', b'-',
2929 b'a',
2930 ];
2931 assert_contains_bytes(&payload, &expected_resource_dict);
2932 }
2933
2934 #[test]
2935 fn v3_series_promotes_additional_and_origin_resource_tags_without_empty_host() {
2936 let context = Context::from_static_parts("series.additional_origin_resources", &["env:prod"])
2937 .with_origin_tags(tag_set(["dd.internal.resource:pod:pod-origin"]));
2938 let additional_tags = SharedTagSet::from(tag_set([
2939 "team:core",
2940 "device:switch1",
2941 "dd.internal.resource:container:container-a",
2942 ]));
2943 let context = context.with_host(Some(MetaString::empty()));
2944 let metadata = MetricMetadata::default();
2945 let metric = Metric::from_parts(context, MetricValues::gauge([1.0_f64]), metadata);
2946
2947 let payload = encode_v3_metrics_batch(&[metric], &additional_tags)
2948 .expect("V3 series should encode successfully")
2949 .payload;
2950
2951 assert_contains_bytes(&payload, b"env:prod");
2952 assert_contains_bytes(&payload, b"team:core");
2953 assert!(!contains_bytes(&payload, b"device:switch1"));
2954 assert!(!contains_bytes(&payload, b"dd.internal.resource:container:container-a"));
2955 assert!(!contains_bytes(&payload, b"dd.internal.resource:pod:pod-origin"));
2956
2957 let expected_resource_dict = [
2958 0x22, 0x34, 0x06, b'd', b'e', b'v', b'i', b'c', b'e', 0x07, b's', b'w', b'i', b't', b'c', b'h', b'1', 0x03, b'p', b'o',
2961 b'd', 0x0a, b'p', b'o', b'd', b'-', b'o', b'r', b'i', b'g', b'i', b'n', 0x09, b'c', b'o', b'n', b't', b'a',
2962 b'i', b'n', b'e', b'r', 0x0b, b'c', b'o', b'n', b't', b'a', b'i', b'n', b'e', b'r', b'-', b'a',
2963 ];
2964 assert_contains_bytes(&payload, &expected_resource_dict);
2965 assert!(!contains_bytes(&payload, b"host"));
2966 }
2967
2968 #[test]
2969 fn v3_sketch_keeps_device_and_internal_resource_tags_as_tags() {
2970 let context = Context::from_static_parts(
2971 "sketch.resources",
2972 &["env:prod", "device:switch1", "dd.internal.resource:pod:pod-a"],
2973 );
2974 let context = context.with_host(Some(MetaString::from_static("host-a")));
2975 let metadata = MetricMetadata::default();
2976 let metric = Metric::from_parts(context, MetricValues::histogram([1.0_f64]), metadata);
2977
2978 let payload = encode_v3_metrics_batch(&[metric], &SharedTagSet::default())
2979 .expect("V3 sketch should encode successfully")
2980 .payload;
2981
2982 assert_contains_bytes(&payload, b"env:prod");
2983 assert_contains_bytes(&payload, b"device:switch1");
2984 assert_contains_bytes(&payload, b"dd.internal.resource:pod:pod-a");
2985
2986 let expected_resource_dict = [
2987 0x22, 0x0c, 0x04, b'h', b'o', b's', b't', 0x06, b'h', b'o', b's', b't', b'-', b'a',
2990 ];
2991 assert_contains_bytes(&payload, &expected_resource_dict);
2992 }
2993
2994 struct RequestBuilderScenario {
2996 series_mode: MetricsEncoderMode,
2997 sketches_mode: MetricsEncoderMode,
2998 payload_limits: V3PayloadLimits,
2999 flush_timeout: Duration,
3000 }
3001
3002 impl RequestBuilderScenario {
3003 fn new(series_mode: MetricsEncoderMode, sketches_mode: MetricsEncoderMode) -> Self {
3005 Self {
3006 series_mode,
3007 sketches_mode,
3008 payload_limits: V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 10_000),
3009 flush_timeout: Duration::from_millis(10),
3010 }
3011 }
3012
3013 fn with_max_points_per_payload(mut self, max_points_per_payload: usize) -> Self {
3015 self.payload_limits = V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, max_points_per_payload);
3016 self
3017 }
3018
3019 fn with_flush_timeout(mut self, flush_timeout: Duration) -> Self {
3021 self.flush_timeout = flush_timeout;
3022 self
3023 }
3024
3025 async fn spawn(self) -> RequestBuilderHarness {
3028 let v3_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, usize::MAX, None);
3029 let metrics_builder = MetricsBuilder::default();
3030 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
3031 let serializer_telemetry = V3SerializerTelemetry::from_builder(&metrics_builder);
3032 let (events_tx, events_rx) = tokio::sync::mpsc::channel(1);
3033 let (payloads_tx, payloads_rx) = tokio::sync::mpsc::channel(8);
3034
3035 let handle = tokio::spawn(run_request_builder(
3036 None,
3037 None,
3038 self.series_mode,
3039 self.sketches_mode,
3040 V3RuntimeConfig {
3041 endpoint_config: v3_endpoint_config,
3042 payload_limits: self.payload_limits,
3043 series_endpoint_uri: METRICS_SERIES_V3_PATH.to_string(),
3044 serializer_telemetry,
3045 },
3046 telemetry,
3047 events_rx,
3048 payloads_tx,
3049 self.flush_timeout,
3050 false,
3051 ));
3052
3053 RequestBuilderHarness {
3054 events_tx,
3055 payloads_rx,
3056 handle,
3057 }
3058 }
3059 }
3060
3061 struct RequestBuilderHarness {
3063 events_tx: tokio::sync::mpsc::Sender<EventsBuffer>,
3064 payloads_rx: tokio::sync::mpsc::Receiver<PayloadsBuffer>,
3065 handle: tokio::task::JoinHandle<Result<(), GenericError>>,
3066 }
3067
3068 impl RequestBuilderHarness {
3069 async fn push_metrics(&self, metrics: impl IntoIterator<Item = Metric>) {
3071 let mut events = EventsBuffer::default();
3072 for metric in metrics {
3073 assert!(
3074 events.try_push(Event::Metric(metric)).is_none(),
3075 "event buffer should accept metric"
3076 );
3077 }
3078 self.events_tx
3079 .send(events)
3080 .await
3081 .expect("events should be sent to request builder");
3082 }
3083
3084 async fn next_http_request(&mut self) -> (PayloadMetadata, Request<FrozenChunkedBytesBuffer>) {
3086 let payload = timeout(Duration::from_secs(1), self.payloads_rx.recv())
3087 .await
3088 .expect("payload should arrive before timeout")
3089 .expect("payload channel should remain open");
3090 match payload {
3091 Payload::Http(http_payload) => http_payload.into_parts(),
3092 _ => panic!("expected HTTP payload"),
3093 }
3094 }
3095
3096 async fn shutdown(self) {
3098 drop(self.events_tx);
3099 self.handle
3100 .await
3101 .expect("request builder task should complete")
3102 .expect("request builder should stop cleanly");
3103 }
3104 }
3105
3106 #[tokio::test]
3107 async fn authoritative_v3_flushes_previous_point_limit_batch() {
3108 let recorder = TestRecorder::default();
3112 let _local = metrics::set_default_local_recorder(&recorder);
3113
3114 let mut shared = shared_configuration();
3116 shared.metrics_encoding.v3_series_mode = V3SeriesMode::DatadogOnly;
3117 let mut config = metrics_config_from(&shared);
3118 config.max_series_points_per_payload = 3;
3119 config.flush_timeout = Duration::from_millis(250);
3120 assert!(!config.requires_v2_series(false).expect("endpoints should resolve"));
3121
3122 let (control, mut payloads) = TestComponentDriver::options()
3123 .with_component_id("metrics")
3124 .encoder(config)
3125 .await
3126 .expect("metrics encoder should build");
3127 let mut next_request_uri = async || {
3128 let payload = payloads.next().await.expect("the encoder should flush a payload");
3129 let (_, request) = payload
3130 .try_into_http_payload()
3131 .expect("metrics are sent over HTTP")
3132 .into_parts();
3133 request.uri().clone()
3134 };
3135
3136 control
3137 .send_events([
3138 Event::Metric(Metric::counter("authoritative.v3.points.one", [(123, 1.0), (124, 2.0)])),
3139 Event::Metric(Metric::counter("authoritative.v3.points.two", [(123, 3.0), (124, 4.0)])),
3140 ])
3141 .await;
3142
3143 assert_eq!(METRICS_SERIES_V3_PATH, next_request_uri().await);
3145 assert!(
3146 timeout(Duration::from_millis(50), next_request_uri()).await.is_err(),
3147 "the carried-over metric should wait for the pending-flush timeout"
3148 );
3149
3150 assert_eq!(METRICS_SERIES_V3_PATH, next_request_uri().await);
3152 assert_eq!(
3153 recorder.counter((
3154 "serializer.v3_payload_split_reason",
3155 &[
3156 ("component_id", "metrics"),
3157 ("component_type", "encoder"),
3158 ("reason", "max_points")
3159 ]
3160 )),
3161 Some(1)
3162 );
3163
3164 control.shutdown().await.expect("metrics encoder should stop cleanly");
3165 assert!(payloads.collect().await.is_empty(), "nothing should be left to flush");
3166 }
3167
3168 #[tokio::test]
3169 async fn authoritative_v3_sketches_flush_previous_point_limit_batch() {
3170 let recorder = TestRecorder::default();
3173 let _local = metrics::set_default_local_recorder(&recorder);
3174
3175 let mut harness = RequestBuilderScenario::new(MetricsEncoderMode::V2Only, MetricsEncoderMode::V3Enabled)
3176 .with_max_points_per_payload(3)
3177 .with_flush_timeout(Duration::from_millis(250))
3178 .spawn()
3179 .await;
3180
3181 harness
3182 .push_metrics([
3183 Metric::distribution("authoritative.v3.sketch.points.one", [(123, 1.0), (124, 2.0)]),
3184 Metric::distribution("authoritative.v3.sketch.points.two", [(123, 3.0), (124, 4.0)]),
3185 ])
3186 .await;
3187
3188 for stage in ["point-limit split", "timeout"] {
3189 let (_, request) = harness.next_http_request().await;
3190 assert_eq!(
3191 V3_SKETCHES_ENDPOINT_URI,
3192 request.uri(),
3193 "{stage} sketches payload should target the V3 sketches endpoint"
3194 );
3195 }
3196 assert_eq!(
3197 recorder.counter(("serializer.v3_payload_split_reason", &[("reason", "max_points")])),
3198 Some(1)
3199 );
3200
3201 harness.shutdown().await;
3202 }
3203
3204 fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
3205 haystack.windows(needle.len()).any(|window| window == needle)
3206 }
3207
3208 fn assert_contains_bytes(haystack: &[u8], needle: &[u8]) {
3209 assert!(
3210 contains_bytes(haystack, needle),
3211 "expected payload to contain bytes {:?}, got {:?}",
3212 needle,
3213 haystack
3214 );
3215 }
3216
3217 fn tag_set<const N: usize>(tags: [&'static str; N]) -> TagSet {
3218 tags.into_iter().map(Tag::from_static).collect()
3219 }
3220
3221 #[tokio::test]
3226 async fn v2_series_builder_enforces_max_series_points_per_payload() {
3227 let v2_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, 10_000, None);
3228 let mut builder = v2::create_v2_request_builder(MetricsEndpoint::SeriesV2, &v2_endpoint_config)
3229 .await
3230 .expect("V2 request builder should be created");
3231 builder
3232 .with_len_limits(usize::MAX, usize::MAX)
3233 .expect("byte limits should be accepted");
3234
3235 let mut total_points = 0;
3236 for i in 0..6_000u32 {
3237 let metric = Metric::gauge(
3238 Context::from_parts(MetaString::from(format!("g{i}")), TagSet::default()),
3239 [(1u64, 1.0), (2u64, 2.0)],
3240 );
3241 let mut pending = Some(metric);
3242 while let Some(metric) = pending.take() {
3243 if let Some(returned) = builder.encode(metric).await.expect("encode should not error") {
3244 for request in builder.flush().await {
3245 let (_, points, _) = request.expect("request should build");
3246 assert!(
3247 points <= 10_000,
3248 "payload carried {} points, over the 10000 limit",
3249 points
3250 );
3251 total_points += points;
3252 }
3253 pending = Some(returned);
3254 }
3255 }
3256 }
3257 for request in builder.flush().await {
3258 let (_, points, _) = request.expect("request should build");
3259 assert!(
3260 points <= 10_000,
3261 "final payload carried {} points, over the 10000 limit",
3262 points
3263 );
3264 total_points += points;
3265 }
3266
3267 assert_eq!(
3268 total_points, 12_000,
3269 "all points should be emitted across the split payloads"
3270 );
3271 }
3272}
3273
3274#[cfg(test)]
3275mod payload_limits {
3276 use super::{v2, DatadogMetricsConfiguration};
3277 use crate::common::datadog::{clamp_payload_limits, test_util::shared_configuration};
3278
3279 #[test]
3280 fn payload_limits_come_from_resolved_configuration() {
3281 let mut shared = shared_configuration();
3282 shared.metrics_encoding.max_payload_size = 4321;
3283 shared.metrics_encoding.max_uncompressed_payload_size = 8765;
3284 shared.metrics_encoding.max_series_payload_size = 1234;
3285 shared.metrics_encoding.max_series_uncompressed_payload_size = 5678;
3286 shared.metrics_encoding.max_series_points_per_payload = 500;
3287 shared.metrics_encoding.max_metrics_per_payload = 42;
3288
3289 let config = DatadogMetricsConfiguration::from_configuration(&shared);
3290
3291 assert_eq!(4321, config.max_payload_size);
3292 assert_eq!(8765, config.max_uncompressed_payload_size);
3293 assert_eq!(1234, config.max_series_payload_size);
3294 assert_eq!(5678, config.max_series_uncompressed_payload_size);
3295 assert_eq!(500, config.max_series_points_per_payload);
3296 assert_eq!(500, config.v3_payload_limits().max_points_per_payload);
3297 assert_eq!(42, config.max_metrics_per_payload);
3298 }
3299
3300 #[test]
3301 fn clamps_series_payload_limit_keys_to_api_limits() {
3302 let (uncompressed_limit, compressed_limit) = clamp_payload_limits(
3303 v2::SERIES_V2_UNCOMPRESSED_SIZE_LIMIT + 1,
3304 v2::SERIES_V2_COMPRESSED_SIZE_LIMIT + 1,
3305 v2::SERIES_V2_UNCOMPRESSED_SIZE_LIMIT,
3306 v2::SERIES_V2_COMPRESSED_SIZE_LIMIT,
3307 );
3308 assert_eq!(uncompressed_limit, v2::SERIES_V2_UNCOMPRESSED_SIZE_LIMIT);
3309 assert_eq!(compressed_limit, v2::SERIES_V2_COMPRESSED_SIZE_LIMIT);
3310
3311 let (uncompressed_limit, compressed_limit) = clamp_payload_limits(
3312 5678,
3313 1234,
3314 v2::SERIES_V2_UNCOMPRESSED_SIZE_LIMIT,
3315 v2::SERIES_V2_COMPRESSED_SIZE_LIMIT,
3316 );
3317 assert_eq!(uncompressed_limit, 5678);
3318 assert_eq!(compressed_limit, 1234);
3319 }
3320}