1use std::{collections::VecDeque, ops::Range, time::Duration};
2
3use async_trait::async_trait;
4use ddsketch::DDSketch;
5use facet::Facet;
6use http::{HeaderValue, Method, Request};
7use protobuf::{rt::WireType, CodedOutputStream};
8use resource_accounting::{MemoryBounds, MemoryBoundsBuilder};
9use saluki_common::{
10 buf::{ChunkedBytesBuffer, FrozenChunkedBytesBuffer},
11 iter::ReusableDeduplicator,
12 task::HandleExt as _,
13};
14use saluki_config::GenericConfiguration;
15use saluki_context::tags::{SharedTagSet, Tag};
16use saluki_core::{
17 components::{encoders::*, ComponentContext},
18 data_model::{
19 event::{
20 metric::{Metric, MetricOrigin, MetricValues},
21 EventType,
22 },
23 payload::{HttpPayload, Payload, PayloadMetadata, PayloadType},
24 },
25 observability::ComponentMetricsExt as _,
26 topology::{EventsBuffer, PayloadsBuffer},
27};
28use saluki_error::{generic_error, ErrorContext as _, GenericError};
29use saluki_io::compression::{CompressionScheme, Compressor};
30use saluki_metrics::MetricsBuilder;
31use serde::Deserialize;
32use tokio::{io::AsyncWriteExt as _, select, sync::mpsc, time::sleep};
33use tracing::{debug, error, warn};
34use url::Url;
35use uuid::Uuid;
36
37#[cfg(test)]
38use self::shadow::shadow_sample_matches;
39use self::{
40 shadow::{SeriesShadowConfig, SeriesShadowState},
41 v3::{
42 V3EncodedMetrics, V3EncodedRequest, V3EncoderStats, V3MetricType, V3PayloadLimits, V3PayloadRequest,
43 V3PayloadSplitReason, V3SerializerTelemetry, V3Writer,
44 },
45};
46use crate::{
47 common::datadog::{
48 clamp_payload_limits, default_serializer_compressor_kind,
49 endpoints::{series_v3_config_can_enable_v3, AdditionalEndpoints},
50 io::RB_BUFFER_CHUNK_SIZE,
51 protocol::{MetricsPayloadInfo, UseV3ApiSeriesConfig, V3ApiConfig},
52 request_builder::RequestBuilder,
53 telemetry::ComponentTelemetry,
54 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT, DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT, METRICS_SERIES_V3_PATH,
55 METRICS_SKETCHES_V3_PATH,
56 },
57 encoders::datadog::metrics::v2::MetricsEndpointEncoder,
58};
59
60mod endpoint;
61use self::endpoint::{EndpointConfiguration, MetricsEndpoint};
62
63mod shadow;
64mod v1;
65mod v2;
66mod v3;
67
68const V3_SERIES_ENDPOINT_URI: &str = METRICS_SERIES_V3_PATH;
69const V3_SKETCHES_ENDPOINT_URI: &str = METRICS_SKETCHES_V3_PATH;
70
71const fn default_max_metrics_per_payload() -> usize {
72 10_000
73}
74
75const fn default_max_payload_size() -> usize {
76 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT
77}
78
79const fn default_max_uncompressed_payload_size() -> usize {
80 DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT
81}
82
83const fn default_max_series_payload_size() -> usize {
84 v2::SERIES_V2_COMPRESSED_SIZE_LIMIT
85}
86
87const fn default_max_series_uncompressed_payload_size() -> usize {
88 v2::SERIES_V2_UNCOMPRESSED_SIZE_LIMIT
89}
90
91const fn default_max_series_points_per_payload() -> usize {
92 10_000
93}
94
95const fn default_flush_timeout_secs() -> u64 {
96 2
97}
98
99const fn default_zstd_compressor_level() -> i32 {
100 3
101}
102
103const fn default_use_v2_api_series() -> bool {
104 true
105}
106
107const fn default_log_payloads() -> bool {
108 false
109}
110
111fn series_shadow_config_for_endpoint(
112 series_endpoint: MetricsEndpoint, sample_rate: f64, metrics_v3_disabled_by_compressor: bool,
113) -> SeriesShadowConfig {
114 SeriesShadowConfig::new(
115 if !metrics_v3_disabled_by_compressor && series_endpoint == MetricsEndpoint::SeriesV2 {
116 sample_rate
117 } else {
118 0.0
119 },
120 )
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125enum MetricsEncoderMode {
126 V2Only,
128 V3Enabled,
131 Validation,
133}
134
135impl MetricsEncoderMode {
136 fn from_config(use_v3: bool, validate: bool) -> Self {
137 match (use_v3, validate) {
138 (false, _) => Self::V2Only,
139 (true, false) => Self::V3Enabled,
140 (true, true) => Self::Validation,
141 }
142 }
143
144 fn needs_v3(self) -> bool {
145 matches!(self, Self::V3Enabled | Self::Validation)
146 }
147
148 fn needs_batch_id(self) -> bool {
149 matches!(self, Self::Validation)
150 }
151
152 fn needs_tagging(self) -> bool {
153 matches!(self, Self::V3Enabled | Self::Validation)
154 }
155}
156
157fn metrics_encoder_mode_for_config(
158 use_v3: bool, validate: bool, metrics_v3_disabled_by_compressor: bool,
159) -> MetricsEncoderMode {
160 let use_v3 = use_v3 && !metrics_v3_disabled_by_compressor;
161 MetricsEncoderMode::from_config(use_v3, use_v3 && validate)
162}
163
164fn selected_metrics_primary_v3_override(
165 opw_enabled: bool, opw_url: &str, opw_use_v3_series: bool, vector_enabled: bool, vector_url: &str,
166 vector_use_v3_series: bool,
167) -> Option<bool> {
168 if opw_enabled {
169 metrics_primary_v3_override_for_url(opw_url, opw_use_v3_series)
170 } else if vector_enabled {
171 metrics_primary_v3_override_for_url(vector_url, vector_use_v3_series)
172 } else {
173 None
174 }
175}
176
177fn metrics_primary_v3_override_for_url(url: &str, use_v3_series: bool) -> Option<bool> {
178 metrics_primary_url_can_resolve(url).then_some(use_v3_series)
179}
180
181fn metrics_primary_url_can_resolve(url: &str) -> bool {
182 let url = url.trim();
183 if url.is_empty() {
184 return false;
185 }
186 if url.starts_with("http://") || url.starts_with("https://") {
187 Url::parse(url).is_ok_and(|url| url.host_str().is_some())
188 } else {
189 Url::parse(&format!("https://{url}")).is_ok_and(|url| url.host_str().is_some())
190 }
191}
192
193fn series_v3_can_be_enabled_for_config(
194 serializer_use_v3_series: bool, metrics_primary_v3_override: Option<bool>, has_additional_endpoints: bool,
195 series_config: &UseV3ApiSeriesConfig,
196) -> bool {
197 serializer_use_v3_series
198 || metrics_primary_v3_override == Some(true)
199 || ((metrics_primary_v3_override != Some(false) || has_additional_endpoints)
200 && series_v3_config_can_enable_v3(series_config))
201}
202
203#[derive(Clone, Deserialize, Facet)]
207#[cfg_attr(test, derive(Debug, PartialEq, serde::Serialize))]
208pub struct DatadogMetricsConfiguration {
209 #[serde(
215 rename = "serializer_max_metrics_per_payload",
216 default = "default_max_metrics_per_payload"
217 )]
218 max_metrics_per_payload: usize,
219
220 #[serde(rename = "serializer_max_payload_size", default = "default_max_payload_size")]
230 max_payload_size: usize,
231
232 #[serde(
242 rename = "serializer_max_uncompressed_payload_size",
243 default = "default_max_uncompressed_payload_size"
244 )]
245 max_uncompressed_payload_size: usize,
246
247 #[serde(
257 rename = "serializer_max_series_payload_size",
258 default = "default_max_series_payload_size"
259 )]
260 max_series_payload_size: usize,
261
262 #[serde(
272 rename = "serializer_max_series_uncompressed_payload_size",
273 default = "default_max_series_uncompressed_payload_size"
274 )]
275 max_series_uncompressed_payload_size: usize,
276
277 #[serde(
286 rename = "serializer_max_series_points_per_payload",
287 default = "default_max_series_points_per_payload"
288 )]
289 max_series_points_per_payload: usize,
290
291 #[serde(default = "default_flush_timeout_secs")]
301 flush_timeout_secs: u64,
302
303 #[serde(
307 rename = "serializer_compressor_kind",
308 default = "default_serializer_compressor_kind"
309 )]
310 compressor_kind: String,
311
312 #[serde(
316 rename = "serializer_zstd_compressor_level",
317 default = "default_zstd_compressor_level"
318 )]
319 zstd_compressor_level: i32,
320
321 #[serde(default = "default_use_v2_api_series")]
329 use_v2_api_series: bool,
330
331 #[serde(default = "default_log_payloads")]
337 log_payloads: bool,
338
339 #[serde(default, skip)]
341 #[facet(opaque)]
342 additional_tags: Option<SharedTagSet>,
343
344 #[serde(rename = "serializer_experimental_use_v3_api", default)]
348 v3_api: V3ApiConfig,
349
350 #[serde(flatten)]
352 use_v3_api_series: UseV3ApiSeriesConfig,
353
354 #[serde(default, rename = "data_plane_metrics_v3_series_enabled")]
358 data_plane_metrics_v3_series_enabled: bool,
359
360 #[serde(default, rename = "observability_pipelines_worker_metrics_enabled")]
362 observability_pipelines_worker_metrics_enabled: bool,
363
364 #[serde(default, rename = "observability_pipelines_worker_metrics_url")]
366 observability_pipelines_worker_metrics_url: String,
367
368 #[serde(default, rename = "observability_pipelines_worker_metrics_use_v3_api_series")]
372 observability_pipelines_worker_metrics_use_v3_api_series: bool,
373
374 #[serde(default, rename = "vector_metrics_enabled")]
376 vector_metrics_enabled: bool,
377
378 #[serde(default, rename = "vector_metrics_url")]
380 vector_metrics_url: String,
381
382 #[serde(default, rename = "vector_metrics_use_v3_api_series")]
388 vector_metrics_use_v3_api_series: bool,
389
390 #[serde(default)]
392 additional_endpoints: AdditionalEndpoints,
393}
394
395impl DatadogMetricsConfiguration {
396 pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
398 Ok(config.as_typed()?)
399 }
400
401 pub fn with_additional_tags(mut self, additional_tags: SharedTagSet) -> Self {
403 self.additional_tags = Some(additional_tags);
404 self
405 }
406
407 fn v3_payload_limits(&self) -> V3PayloadLimits {
408 V3PayloadLimits::new(
409 self.max_series_payload_size,
410 self.max_series_uncompressed_payload_size,
411 self.max_metrics_per_payload,
412 self.max_series_points_per_payload,
413 )
414 }
415}
416
417#[async_trait]
418impl EncoderBuilder for DatadogMetricsConfiguration {
419 fn input_event_type(&self) -> EventType {
420 EventType::Metric
421 }
422
423 fn output_payload_type(&self) -> PayloadType {
424 PayloadType::Http
425 }
426
427 async fn build(&self, context: ComponentContext) -> Result<Box<dyn Encoder + Send>, GenericError> {
428 let metrics_builder = MetricsBuilder::from_component_context(&context);
429 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
430 let v3_serializer_telemetry = V3SerializerTelemetry::from_builder(&metrics_builder);
431
432 let v2_compression_scheme = CompressionScheme::new(&self.compressor_kind, self.zstd_compressor_level);
433 let v3_compression_scheme = if self.v3_api.compression_level > 0 {
434 CompressionScheme::new(&self.compressor_kind, self.v3_api.compression_level)
435 } else {
436 v2_compression_scheme
437 };
438 let series_endpoint_uri = if self.v3_api.series.use_beta {
439 self.v3_api.series.beta_route.clone()
440 } else {
441 V3_SERIES_ENDPOINT_URI.to_string()
442 };
443 let shadow_series_endpoint_uri = self.v3_api.series.beta_route.clone();
444 let payload_limits = self.v3_payload_limits();
445
446 let v2_endpoint_config = EndpointConfiguration::new(
447 v2_compression_scheme,
448 self.max_metrics_per_payload,
449 self.additional_tags.clone(),
450 );
451 let endpoint_config = EndpointConfiguration::new(
452 v3_compression_scheme,
453 self.max_metrics_per_payload,
454 self.additional_tags.clone(),
455 );
456
457 let metrics_v3_disabled_by_compressor = matches!(v3_compression_scheme, CompressionScheme::Zlib(_));
459 let metrics_primary_v3_override = selected_metrics_primary_v3_override(
460 self.observability_pipelines_worker_metrics_enabled,
461 &self.observability_pipelines_worker_metrics_url,
462 self.observability_pipelines_worker_metrics_use_v3_api_series,
463 self.vector_metrics_enabled,
464 &self.vector_metrics_url,
465 self.vector_metrics_use_v3_api_series,
466 );
467 let series_v3_can_be_enabled = series_v3_can_be_enabled_for_config(
468 self.v3_api.use_v3_series(),
469 metrics_primary_v3_override,
470 !self.additional_endpoints.is_empty(),
471 &self.use_v3_api_series,
472 );
473 let use_v3_series = self.data_plane_metrics_v3_series_enabled && series_v3_can_be_enabled;
474 let series_mode = metrics_encoder_mode_for_config(
475 use_v3_series,
476 self.v3_api.series.validate,
477 metrics_v3_disabled_by_compressor,
478 );
479 let sketches_mode = metrics_encoder_mode_for_config(
480 self.v3_api.use_v3_sketches(),
481 self.v3_api.sketches.validate,
482 metrics_v3_disabled_by_compressor,
483 );
484 let series_endpoint = if self.use_v2_api_series {
485 MetricsEndpoint::SeriesV2
486 } else {
487 MetricsEndpoint::SeriesV1
488 };
489 let series_shadow_config = series_shadow_config_for_endpoint(
490 series_endpoint,
491 self.v3_api.series.shadow_sample_rate,
492 metrics_v3_disabled_by_compressor,
493 );
494 let v3_runtime_config = V3RuntimeConfig {
495 endpoint_config,
496 payload_limits,
497 series_endpoint_uri,
498 shadow_series_endpoint_uri,
499 series_shadow_config,
500 serializer_telemetry: v3_serializer_telemetry,
501 };
502 let generic_payload_limits = clamp_payload_limits(
503 self.max_uncompressed_payload_size,
504 self.max_payload_size,
505 DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT,
506 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT,
507 );
508 let (series_uncompressed_limit, series_compressed_limit) = if series_endpoint == MetricsEndpoint::SeriesV2 {
509 clamp_payload_limits(
510 self.max_series_uncompressed_payload_size,
511 self.max_series_payload_size,
512 v2::SERIES_V2_UNCOMPRESSED_SIZE_LIMIT,
513 v2::SERIES_V2_COMPRESSED_SIZE_LIMIT,
514 )
515 } else {
516 generic_payload_limits
517 };
518 let mut v2_series_builder = v2::create_v2_request_builder(series_endpoint, &v2_endpoint_config)
519 .await
520 .error_context("Failed to create V2 series request builder.")?;
521 v2_series_builder.with_len_limits(series_uncompressed_limit, series_compressed_limit)?;
522 let v2_series_builder = Some(v2_series_builder);
523
524 let (sketches_uncompressed_limit, sketches_compressed_limit) = generic_payload_limits;
525 let mut v2_sketch_builder = v2::create_v2_request_builder(MetricsEndpoint::Sketches, &v2_endpoint_config)
526 .await
527 .error_context("Failed to create V2 sketches request builder.")?;
528 v2_sketch_builder.with_len_limits(sketches_uncompressed_limit, sketches_compressed_limit)?;
529 let v2_sketch_builder = Some(v2_sketch_builder);
530
531 let flush_timeout = match self.flush_timeout_secs {
532 0 => Duration::from_millis(10),
535 secs => Duration::from_secs(secs),
536 };
537
538 if series_mode.needs_v3() || sketches_mode.needs_v3() {
539 debug!(
540 ?series_mode,
541 ?sketches_mode,
542 v3_series_endpoints = ?self.v3_api.series.endpoints,
543 v3_sketches_endpoints = ?self.v3_api.sketches.endpoints,
544 "V3 encoding support is enabled."
545 );
546 }
547
548 Ok(Box::new(DatadogMetrics {
549 v2_series_builder,
550 v2_sketch_builder,
551 series_mode,
552 sketches_mode,
553 v3_runtime_config,
554 telemetry,
555 flush_timeout,
556 log_payloads: self.log_payloads,
557 }))
558 }
559}
560
561impl MemoryBounds for DatadogMetricsConfiguration {
562 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
563 builder
571 .minimum()
572 .with_single_value::<DatadogMetrics>("component struct")
573 .with_array::<EventsBuffer>("request builder events channel", 8)
574 .with_array::<PayloadsBuffer>("request builder payloads channel", 8);
575
576 builder
577 .firm()
578 .with_array::<Metric>("series metrics split re-encode buffer", self.max_metrics_per_payload)
581 .with_array::<Metric>("sketch metrics split re-encode buffer", self.max_metrics_per_payload);
582 }
583}
584
585pub struct DatadogMetrics {
586 v2_series_builder: Option<RequestBuilder<v2::MetricsEndpointEncoder>>,
587 v2_sketch_builder: Option<RequestBuilder<v2::MetricsEndpointEncoder>>,
588 series_mode: MetricsEncoderMode,
589 sketches_mode: MetricsEncoderMode,
590 v3_runtime_config: V3RuntimeConfig,
591 telemetry: ComponentTelemetry,
592 flush_timeout: Duration,
593 log_payloads: bool,
594}
595
596struct V3RuntimeConfig {
597 endpoint_config: EndpointConfiguration,
598 payload_limits: V3PayloadLimits,
599 series_endpoint_uri: String,
600 shadow_series_endpoint_uri: String,
601 series_shadow_config: SeriesShadowConfig,
602 serializer_telemetry: V3SerializerTelemetry,
603}
604
605#[async_trait]
606impl Encoder for DatadogMetrics {
607 async fn run(mut self: Box<Self>, mut context: EncoderContext) -> Result<(), GenericError> {
608 let Self {
609 v2_series_builder,
610 v2_sketch_builder,
611 series_mode,
612 sketches_mode,
613 v3_runtime_config,
614 telemetry,
615 flush_timeout,
616 log_payloads,
617 } = *self;
618
619 let mut health = context.take_health_handle();
620
621 let (events_tx, events_rx) = mpsc::channel(8);
623 let (payloads_tx, mut payloads_rx) = mpsc::channel(8);
624 let request_builder_fut = run_request_builder(
625 v2_series_builder,
626 v2_sketch_builder,
627 series_mode,
628 sketches_mode,
629 v3_runtime_config,
630 telemetry,
631 events_rx,
632 payloads_tx,
633 flush_timeout,
634 log_payloads,
635 );
636 let request_builder_handle = context
637 .topology_context()
638 .global_thread_pool()
639 .spawn_traced_named("dd-metrics-request-builder", request_builder_fut);
640
641 health.mark_ready();
642 debug!("Datadog Metrics encoder started.");
643
644 loop {
645 select! {
646 biased;
647
648 _ = health.live() => continue,
649 maybe_payload = payloads_rx.recv() => match maybe_payload {
650 Some(payload) => {
651 if let Err(e) = context.dispatcher().dispatch(payload).await {
652 error!("Failed to dispatch payload: {}", e);
653 }
654 }
655 None => break,
656 },
657 maybe_event_buffer = context.events().next() => match maybe_event_buffer {
658 Some(event_buffer) => events_tx.send(event_buffer).await
659 .error_context("Failed to send event buffer to request builder task.")?,
660 None => break,
661 },
662 }
663 }
664
665 drop(events_tx);
667
668 while let Some(payload) = payloads_rx.recv().await {
670 if let Err(e) = context.dispatcher().dispatch(payload).await {
671 error!("Failed to dispatch payload: {}", e);
672 }
673 }
674
675 match request_builder_handle.await {
677 Ok(Ok(())) => debug!("Request builder task stopped."),
678 Ok(Err(e)) => error!(error = %e, "Request builder task failed."),
679 Err(e) => error!(error = %e, "Request builder task panicked."),
680 }
681
682 debug!("Datadog Metrics encoder stopped.");
683
684 Ok(())
685 }
686}
687
688fn log_metric_payload(metric: &Metric) {
692 match metric.values() {
693 MetricValues::Counter(..) | MetricValues::Rate(..) | MetricValues::Gauge(..) | MetricValues::Set(..) => {
694 debug!(?metric, "Flushing series metric.")
695 }
696 MetricValues::Histogram(..) | MetricValues::Distribution(..) => {
697 debug!(?metric, "Flushing sketch metric.")
698 }
699 }
700}
701
702#[allow(clippy::too_many_arguments)]
703async fn run_request_builder(
704 mut v2_series_builder: Option<RequestBuilder<v2::MetricsEndpointEncoder>>,
705 mut v2_sketch_builder: Option<RequestBuilder<v2::MetricsEndpointEncoder>>, series_mode: MetricsEncoderMode,
706 sketches_mode: MetricsEncoderMode, v3_runtime_config: V3RuntimeConfig, telemetry: ComponentTelemetry,
707 mut events_rx: mpsc::Receiver<EventsBuffer>, mut payloads_tx: mpsc::Sender<PayloadsBuffer>,
708 flush_timeout: Duration, log_payloads: bool,
709) -> Result<(), GenericError> {
710 let mut pending_flush = false;
711 let pending_flush_timeout = sleep(flush_timeout);
712 tokio::pin!(pending_flush_timeout);
713
714 let mut v3_series_metrics =
715 (series_mode.needs_v3() || v3_runtime_config.series_shadow_config.is_enabled()).then(Vec::<Metric>::new);
716 let mut v3_sketch_metrics = sketches_mode.needs_v3().then(Vec::<Metric>::new);
717 let mut v3_series_points = 0usize;
718 let mut v3_sketch_points = 0usize;
719
720 let mut series_batch_id = None;
721 let mut sketches_batch_id = None;
722 let mut series_shadow_state = SeriesShadowState::default();
723 let series_shadow_config = v3_runtime_config.series_shadow_config;
724
725 let tag_series = series_mode.needs_tagging();
726 let tag_sketches = sketches_mode.needs_tagging();
727 let v3_flush_context = V3FlushContext {
728 endpoint_config: &v3_runtime_config.endpoint_config,
729 payload_limits: v3_runtime_config.payload_limits,
730 series_endpoint_uri: &v3_runtime_config.series_endpoint_uri,
731 serializer_telemetry: &v3_runtime_config.serializer_telemetry,
732 telemetry: &telemetry,
733 };
734 let v3_shadow_flush_context = V3FlushContext {
735 endpoint_config: &v3_runtime_config.endpoint_config,
736 payload_limits: v3_runtime_config.payload_limits,
737 series_endpoint_uri: &v3_runtime_config.shadow_series_endpoint_uri,
738 serializer_telemetry: &v3_runtime_config.serializer_telemetry,
739 telemetry: &telemetry,
740 };
741
742 loop {
743 select! {
744 Some(event_buffer) = events_rx.recv() => {
745 for event in event_buffer {
746 let metric = match event.try_into_metric() {
747 Some(metric) => metric,
748 None => continue,
749 };
750
751 if log_payloads {
752 log_metric_payload(&metric);
753 }
754
755 if !v1::has_emittable_point(&metric) {
758 debug!(metric = %metric.context().name(), "Dropping series metric with no finite points.");
759 telemetry.events_dropped_encoder().increment(1);
760 continue;
761 }
762
763 let endpoint = MetricsEndpoint::from_metric(&metric);
765 let (endpoint_mode, maybe_v2_builder, maybe_v3_metrics, v3_points, batch_id) = match endpoint {
766 MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2 => (
767 series_mode,
768 &mut v2_series_builder,
769 &mut v3_series_metrics,
770 &mut v3_series_points,
771 &mut series_batch_id,
772 ),
773 MetricsEndpoint::Sketches => (
774 sketches_mode,
775 &mut v2_sketch_builder,
776 &mut v3_sketch_metrics,
777 &mut v3_sketch_points,
778 &mut sketches_batch_id,
779 ),
780 };
781 let metric_point_count = metric.values().len();
782 let is_series = matches!(endpoint, MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2);
783 let series_shadow_active = is_series
784 && matches!(endpoint_mode, MetricsEncoderMode::V2Only)
785 && series_shadow_state.ensure_decision(series_shadow_config);
786 let needs_batch_id = endpoint_mode.needs_batch_id() || series_shadow_active;
787 if needs_batch_id && batch_id.is_none() {
788 *batch_id = Some(Uuid::now_v7());
789 }
790 let active_batch_id = needs_batch_id.then_some(batch_id.as_ref()).flatten();
791 let should_buffer_v3 = endpoint_mode.needs_v3() || series_shadow_active;
792
793 if should_buffer_v3 {
798 if let Some(metrics) = maybe_v3_metrics {
799 metrics.push(metric.clone());
800 *v3_points += metric_point_count;
801 }
802 }
803
804 let v2_payload_info = match endpoint {
811 MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2 => {
812 if series_shadow_active {
813 Some(MetricsPayloadInfo::v2_shadow_series())
814 } else {
815 tag_series.then(MetricsPayloadInfo::v2_series)
816 }
817 }
818 MetricsEndpoint::Sketches => tag_sketches.then(MetricsPayloadInfo::v2_sketches),
819 };
820 let metric_for_next_shadow_batch = (is_series
823 && matches!(endpoint_mode, MetricsEncoderMode::V2Only)
824 && series_shadow_config.is_enabled()
825 && !series_shadow_active)
826 .then(|| metric.clone());
827 let mut v2_encoded = false;
828 let v2_flushed = if let Some(builder) = maybe_v2_builder {
829 let result =
830 encode_v2_metrics(builder, metric, &telemetry, &mut payloads_tx, active_batch_id, v2_payload_info).await?;
831 v2_encoded = result.encoded();
832 if should_buffer_v3
833 && !result.encoded()
834 && !matches!(endpoint_mode, MetricsEncoderMode::V3Enabled)
835 {
836 if let Some(metrics) = maybe_v3_metrics {
837 let _ = metrics.pop();
838 *v3_points = v3_points.saturating_sub(metric_point_count);
839 }
840 }
841
842 result.flushed()
843 } else {
844 false
845 };
846
847 let v3_payload_info = match endpoint {
850 MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2 => {
851 if series_shadow_active {
852 Some(MetricsPayloadInfo::v3_shadow_series())
853 } else {
854 tag_series.then(MetricsPayloadInfo::v3_series)
855 }
856 }
857 MetricsEndpoint::Sketches => tag_sketches.then(MetricsPayloadInfo::v3_sketches),
858 };
859 let mut split_metric = None;
860 let v3_flushed = if let Some(v3_metrics) = maybe_v3_metrics {
861 let should_flush_v3 = match endpoint_mode {
862 MetricsEncoderMode::V2Only => series_shadow_active && v2_flushed,
863 MetricsEncoderMode::V3Enabled => {
864 if v3_flush_context.payload_limits.point_count_fits(metric_point_count)
865 && v3_flush_context.payload_limits.point_count_exceeds_limit(*v3_points)
866 && v3_metrics.len() > 1
867 {
868 v3_flush_context
869 .serializer_telemetry
870 .record_split_reason(V3PayloadSplitReason::MaxPoints);
871 if let Some(metric) = v3_metrics.pop() {
872 *v3_points = v3_points.saturating_sub(metric.values().len());
873 split_metric = Some(metric);
874 }
875 true
876 } else {
877 v3_flush_context.payload_limits.should_flush_point_count_limit(*v3_points)
878 || v3_flush_context
879 .payload_limits
880 .should_flush_metric_count_limit(v3_metrics)
881 }
882 }
883 MetricsEncoderMode::Validation => v2_flushed,
884 };
885 if should_flush_v3 {
886 if !matches!(endpoint_mode, MetricsEncoderMode::V3Enabled) {
887 if v2_flushed {
890 if let Some(metric) = v3_metrics.pop() {
891 *v3_points = v3_points.saturating_sub(metric.values().len());
892 split_metric = Some(metric);
893 }
894 }
895 }
896 let flush_context = if series_shadow_active {
897 v3_shadow_flush_context
898 } else {
899 v3_flush_context
900 };
901 encode_and_flush_v3_metrics(
902 endpoint,
903 flush_context,
904 v3_metrics,
905 &mut payloads_tx,
906 active_batch_id,
907 v3_payload_info,
908 )
909 .await?;
910 *v3_points = 0;
911 true
912 } else {
913 false
914 }
915 } else {
916 false
917 };
918
919 if matches!(endpoint_mode, MetricsEncoderMode::V3Enabled) {
920 if let Some(m) = split_metric.take() {
923 let point_count = m.values().len();
924 if let Some(metrics) = maybe_v3_metrics {
925 metrics.push(m);
926 *v3_points += point_count;
927 }
928 }
929 } else if endpoint_mode.needs_batch_id() && (v2_flushed || v3_flushed) {
930 *batch_id = if let Some(m) = split_metric.take() {
933 let point_count = m.values().len();
934 if let Some(metrics) = maybe_v3_metrics {
935 metrics.push(m);
936 *v3_points += point_count;
937 }
938 Some(Uuid::now_v7())
939 } else {
940 None
941 };
942 } else if is_series && series_shadow_active && (v2_flushed || v3_flushed) {
943 series_shadow_state.reset();
946 *batch_id = if let Some(m) = split_metric.take() {
947 if series_shadow_state.ensure_decision(series_shadow_config) {
948 let point_count = m.values().len();
949 if let Some(metrics) = maybe_v3_metrics {
950 metrics.push(m);
951 *v3_points += point_count;
952 }
953 Some(Uuid::now_v7())
954 } else {
955 None
956 }
957 } else {
958 None
959 };
960 } else if is_series
961 && matches!(endpoint_mode, MetricsEncoderMode::V2Only)
962 && series_shadow_config.is_enabled()
963 && v2_flushed
964 {
965 series_shadow_state.reset();
968 *batch_id = None;
969 if v2_encoded {
970 if let Some(m) = metric_for_next_shadow_batch {
971 if series_shadow_state.ensure_decision(series_shadow_config) {
972 let point_count = m.values().len();
973 if let Some(metrics) = maybe_v3_metrics {
974 metrics.push(m);
975 *v3_points += point_count;
976 }
977 *batch_id = Some(Uuid::now_v7());
978 }
979 }
980 }
981 }
982 }
983
984 debug!("Processed event buffer.");
985
986 if !pending_flush {
988 pending_flush_timeout.as_mut().reset(tokio::time::Instant::now() + flush_timeout);
989 pending_flush = true;
990 }
991 },
992 _ = &mut pending_flush_timeout, if pending_flush => {
993 debug!("Flushing pending request(s).");
994
995 pending_flush = false;
996
997 let series_shadow_active = matches!(series_mode, MetricsEncoderMode::V2Only)
1001 && series_shadow_state.active.unwrap_or(false);
1002 let v2_series_payload_info = if series_shadow_active {
1003 Some(MetricsPayloadInfo::v2_shadow_series())
1004 } else {
1005 tag_series.then(MetricsPayloadInfo::v2_series)
1006 };
1007 let series_active_batch_id = (series_mode.needs_batch_id() || series_shadow_active)
1008 .then_some(series_batch_id.as_ref())
1009 .flatten();
1010 let mut v2_series_flush_succeeded = true;
1011 if let Some(builder) = &mut v2_series_builder {
1012 if let Err(e) = flush_v2_metrics(builder, &mut payloads_tx, series_active_batch_id, v2_series_payload_info).await {
1013 error!(error = %e, "Failed to flush V2 series metrics: {}", e);
1014 v2_series_flush_succeeded = false;
1015 }
1016 }
1017
1018 let v3_series_payload_info = if series_shadow_active {
1019 Some(MetricsPayloadInfo::v3_shadow_series())
1020 } else {
1021 tag_series.then(MetricsPayloadInfo::v3_series)
1022 };
1023 if let Some(metrics) = &mut v3_series_metrics {
1024 if v2_series_flush_succeeded || matches!(series_mode, MetricsEncoderMode::V3Enabled) {
1025 let flush_context = if series_shadow_active {
1027 v3_shadow_flush_context
1028 } else {
1029 v3_flush_context
1030 };
1031 if let Err(e) = encode_and_flush_v3_series_metrics(
1032 flush_context,
1033 metrics,
1034 &mut payloads_tx,
1035 series_active_batch_id,
1036 v3_series_payload_info,
1037 )
1038 .await
1039 {
1040 error!(error = %e, "Failed to flush V3 series metrics: {}", e);
1041 }
1042 v3_series_points = 0;
1043 } else {
1044 warn!("Failed to flush V2 series metrics, skipping V3 series flush.");
1046 metrics.clear();
1047 v3_series_points = 0;
1048 }
1049 }
1050 if series_mode.needs_batch_id() {
1051 series_batch_id = None;
1052 }
1053 if matches!(series_mode, MetricsEncoderMode::V2Only) && series_shadow_config.is_enabled() {
1054 series_shadow_state.reset();
1055 series_batch_id = None;
1056 }
1057
1058 let v2_sketches_payload_info = tag_sketches.then(MetricsPayloadInfo::v2_sketches);
1060 let sketches_active_batch_id = sketches_mode.needs_batch_id().then_some(sketches_batch_id.as_ref()).flatten();
1061 let mut v2_sketches_flush_succeeded = true;
1062 if let Some(builder) = &mut v2_sketch_builder {
1063 if let Err(e) = flush_v2_metrics(builder, &mut payloads_tx, sketches_active_batch_id, v2_sketches_payload_info).await {
1064 error!(error = %e, "Failed to flush V2 sketch metrics: {}", e);
1065 v2_sketches_flush_succeeded = false;
1066 }
1067 }
1068
1069 let v3_sketches_payload_info = tag_sketches.then(MetricsPayloadInfo::v3_sketches);
1070 if let Some(metrics) = &mut v3_sketch_metrics {
1071 if v2_sketches_flush_succeeded || matches!(sketches_mode, MetricsEncoderMode::V3Enabled) {
1072 if let Err(e) = encode_and_flush_v3_sketch_metrics(
1073 v3_flush_context,
1074 metrics,
1075 &mut payloads_tx,
1076 sketches_active_batch_id,
1077 v3_sketches_payload_info,
1078 )
1079 .await
1080 {
1081 error!(error = %e, "Failed to flush V3 sketch metrics: {}", e);
1082 }
1083 v3_sketch_points = 0;
1084 } else {
1085 warn!("Failed to flush V2 sketch metrics, skipping V3 sketch flush.");
1086 metrics.clear();
1087 v3_sketch_points = 0;
1088 }
1089 }
1090 if sketches_mode.needs_batch_id() {
1091 sketches_batch_id = None;
1092 }
1093
1094 debug!("All flushed requests sent to I/O task. Waiting for next event buffer...");
1095 },
1096
1097 else => break,
1099 }
1100 }
1101
1102 Ok(())
1103}
1104
1105struct EncodeResult {
1106 encoded: bool,
1107 flushed: bool,
1108}
1109
1110impl EncodeResult {
1111 pub const fn new(encoded: bool, flushed: bool) -> Self {
1112 Self { encoded, flushed }
1113 }
1114
1115 pub const fn encoded(&self) -> bool {
1116 self.encoded
1117 }
1118
1119 pub const fn flushed(&self) -> bool {
1120 self.flushed
1121 }
1122}
1123
1124async fn encode_v2_metrics(
1125 request_builder: &mut RequestBuilder<v2::MetricsEndpointEncoder>, metric: Metric, telemetry: &ComponentTelemetry,
1126 payloads_tx: &mut mpsc::Sender<Payload>, batch_id: Option<&Uuid>, payload_info: Option<MetricsPayloadInfo>,
1127) -> Result<EncodeResult, GenericError> {
1128 let metric_to_retry = match request_builder.encode(metric).await {
1132 Ok(None) => return Ok(EncodeResult::new(true, false)),
1133 Ok(Some(metric)) => metric,
1134 Err(e) => {
1135 error!(error = %e, "Failed to encode metric.");
1136 telemetry.events_dropped_encoder().increment(1);
1137 return Ok(EncodeResult::new(false, false));
1138 }
1139 };
1140
1141 flush_v2_metrics(request_builder, payloads_tx, batch_id, payload_info).await?;
1142
1143 match request_builder.encode(metric_to_retry).await {
1147 Ok(None) => Ok(EncodeResult::new(true, true)),
1148 Ok(Some(_)) => unreachable!(
1149 "failure to encode due to size should never occur after flush for metrics which aren't unencodable"
1150 ),
1151 Err(e) => {
1152 error!(error = %e, "Failed to encode metric.");
1153 telemetry.events_dropped_encoder().increment(1);
1154 Ok(EncodeResult::new(false, true))
1155 }
1156 }
1157}
1158
1159async fn flush_v2_metrics(
1160 request_builder: &mut RequestBuilder<MetricsEndpointEncoder>, payloads_tx: &mut mpsc::Sender<Payload>,
1161 batch_id: Option<&Uuid>, payload_info: Option<MetricsPayloadInfo>,
1162) -> Result<usize, GenericError> {
1163 let mut requests_flushed = 0;
1164
1165 let maybe_requests = request_builder.flush().await;
1166 let batch_len = maybe_requests.len();
1167 for (batch_seq, maybe_request) in maybe_requests.into_iter().enumerate() {
1168 match maybe_request {
1169 Ok((events, data_points, request)) => {
1170 requests_flushed += 1;
1171
1172 flush_payload(
1173 request,
1174 events,
1175 data_points,
1176 payloads_tx,
1177 batch_id,
1178 batch_seq,
1179 batch_len,
1180 payload_info,
1181 )
1182 .await?;
1183 }
1184
1185 Err(e) => {
1187 if !e.is_recoverable() {
1188 return Err(GenericError::from(e).context("Failed to flush request."));
1189 }
1190 }
1191 }
1192 }
1193
1194 Ok(requests_flushed)
1195}
1196
1197#[derive(Clone, Copy)]
1198struct V3FlushContext<'a> {
1199 endpoint_config: &'a EndpointConfiguration,
1200 payload_limits: V3PayloadLimits,
1201 series_endpoint_uri: &'a str,
1202 serializer_telemetry: &'a V3SerializerTelemetry,
1203 telemetry: &'a ComponentTelemetry,
1204}
1205
1206async fn encode_and_flush_v3_metrics(
1207 endpoint: MetricsEndpoint, context: V3FlushContext<'_>, metrics: &mut Vec<Metric>,
1208 payloads_tx: &mut mpsc::Sender<Payload>, batch_id: Option<&Uuid>, payload_info: Option<MetricsPayloadInfo>,
1209) -> Result<(), GenericError> {
1210 match endpoint {
1211 MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2 => {
1212 encode_and_flush_v3_series_metrics(context, metrics, payloads_tx, batch_id, payload_info).await
1213 }
1214 MetricsEndpoint::Sketches => {
1215 encode_and_flush_v3_sketch_metrics(context, metrics, payloads_tx, batch_id, payload_info).await
1216 }
1217 }
1218}
1219
1220async fn encode_and_flush_v3_series_metrics(
1221 context: V3FlushContext<'_>, metrics: &mut Vec<Metric>, payloads_tx: &mut mpsc::Sender<Payload>,
1222 batch_id: Option<&Uuid>, payload_info: Option<MetricsPayloadInfo>,
1223) -> Result<(), GenericError> {
1224 if metrics.is_empty() {
1225 return Ok(());
1226 }
1227 let metrics_to_flush = std::mem::take(metrics);
1228
1229 let requests = encode_v3_payload_requests(context.series_endpoint_uri, &metrics_to_flush, context, "series").await;
1230 let batch_len = requests.len();
1231 for (batch_seq, payload_request) in requests.into_iter().enumerate() {
1232 flush_payload(
1233 payload_request.request,
1234 payload_request.event_count,
1235 payload_request.data_point_count,
1236 payloads_tx,
1237 batch_id,
1238 batch_seq,
1239 batch_len,
1240 payload_info,
1241 )
1242 .await?;
1243 debug!(
1244 events = payload_request.event_count,
1245 data_points = payload_request.data_point_count,
1246 "Sent V3 series payload."
1247 );
1248 }
1249
1250 Ok(())
1251}
1252
1253async fn encode_and_flush_v3_sketch_metrics(
1254 context: V3FlushContext<'_>, metrics: &mut Vec<Metric>, payloads_tx: &mut mpsc::Sender<Payload>,
1255 batch_id: Option<&Uuid>, payload_info: Option<MetricsPayloadInfo>,
1256) -> Result<(), GenericError> {
1257 if metrics.is_empty() {
1258 return Ok(());
1259 }
1260 let metrics_to_flush = std::mem::take(metrics);
1261
1262 let requests = encode_v3_payload_requests(V3_SKETCHES_ENDPOINT_URI, &metrics_to_flush, context, "sketches").await;
1263 let batch_len = requests.len();
1264 for (batch_seq, payload_request) in requests.into_iter().enumerate() {
1265 flush_payload(
1266 payload_request.request,
1267 payload_request.event_count,
1268 payload_request.data_point_count,
1269 payloads_tx,
1270 batch_id,
1271 batch_seq,
1272 batch_len,
1273 payload_info,
1274 )
1275 .await?;
1276 debug!(
1277 events = payload_request.event_count,
1278 data_points = payload_request.data_point_count,
1279 "Sent V3 sketches payload."
1280 );
1281 }
1282
1283 Ok(())
1284}
1285
1286async fn encode_v3_payload_requests(
1287 endpoint_uri: &str, metrics: &[Metric], context: V3FlushContext<'_>, payload_kind: &'static str,
1288) -> Vec<V3PayloadRequest> {
1289 let mut requests = Vec::new();
1290 let mut pending_ranges = split_v3_metric_ranges_by_point_limit(metrics, context, payload_kind);
1291
1292 while let Some(range) = pending_ranges.pop_front() {
1293 if range.is_empty() {
1294 continue;
1295 }
1296
1297 let metrics_in_range = &metrics[range.clone()];
1298 let event_count = metrics_in_range.len();
1299 let data_point_count = metrics_in_range.iter().map(|metric| metric.values().len()).sum();
1300
1301 let encoded = match encode_v3_metrics_batch(metrics_in_range, context.endpoint_config.additional_tags()) {
1302 Ok(encoded) => encoded,
1303 Err(e) => {
1304 error!(error = %e, payload_kind, events = event_count, "Failed to encode V3 metrics payload request.");
1305 context.telemetry.events_dropped_encoder().increment(event_count as u64);
1306 continue;
1307 }
1308 };
1309 let encoded_request =
1310 match create_v3_request(endpoint_uri, encoded, context.endpoint_config.compression_scheme()).await {
1311 Ok(request) => request,
1312 Err(e) => {
1313 error!(error = %e, payload_kind, events = event_count, "Failed to create V3 metrics request.");
1314 context.telemetry.events_dropped_encoder().increment(event_count as u64);
1315 continue;
1316 }
1317 };
1318
1319 if context.payload_limits.request_fits(&encoded_request) {
1320 record_v3_serializer_stats(context.serializer_telemetry, &encoded_request.stats);
1321 requests.push(V3PayloadRequest {
1322 request: encoded_request.request,
1323 event_count,
1324 data_point_count,
1325 });
1326 continue;
1327 }
1328
1329 if range.len() == 1 {
1330 context.serializer_telemetry.record_item_too_big();
1332 context
1333 .serializer_telemetry
1334 .record_split_reason(V3PayloadSplitReason::ItemTooBig);
1335 warn!(
1336 payload_kind,
1337 compressed_len = encoded_request.compressed_len,
1338 compressed_limit = context.payload_limits.max_compressed_size,
1339 uncompressed_len = encoded_request.uncompressed_len,
1340 uncompressed_limit = context.payload_limits.max_uncompressed_size,
1341 "Dropping oversized V3 metric that cannot be split further."
1342 );
1343 context.telemetry.events_dropped_encoder().increment(1);
1344 continue;
1345 }
1346
1347 context
1349 .serializer_telemetry
1350 .record_split_reason(V3PayloadSplitReason::PayloadFull);
1351 let pivot = range.start + range.len() / 2;
1352 pending_ranges.push_front(pivot..range.end);
1353 pending_ranges.push_front(range.start..pivot);
1354 }
1355
1356 requests
1357}
1358
1359fn record_v3_serializer_stats(telemetry: &V3SerializerTelemetry, stats: &V3EncoderStats) {
1360 telemetry.record_values_count(stats.value_encoding_stats);
1361
1362 for column in &stats.columns {
1363 let uncompressed_size = column.bytes.len() as u64;
1364 let compressed_size = column.compressed_len as u64;
1365 telemetry.record_column_size(column.field_number, uncompressed_size, compressed_size);
1366 }
1367}
1368
1369async fn compressed_v3_len(bytes: &[u8], compression_scheme: CompressionScheme) -> Result<usize, GenericError> {
1370 if matches!(compression_scheme, CompressionScheme::Noop) {
1371 return Ok(bytes.len());
1372 }
1373
1374 let buffer = ChunkedBytesBuffer::new(RB_BUFFER_CHUNK_SIZE);
1375 let mut compressor = Compressor::from_scheme(compression_scheme, buffer);
1376 compressor
1377 .write_all(bytes)
1378 .await
1379 .error_context("Failed to compress V3 bytes.")?;
1380 compressor
1381 .flush()
1382 .await
1383 .error_context("Failed to flush V3 compressor.")?;
1384 compressor
1385 .shutdown()
1386 .await
1387 .error_context("Failed to shutdown V3 compressor.")?;
1388
1389 Ok(compressor.into_inner().freeze().len())
1390}
1391
1392fn split_v3_metric_ranges_by_point_limit(
1393 metrics: &[Metric], context: V3FlushContext<'_>, payload_kind: &'static str,
1394) -> VecDeque<Range<usize>> {
1395 let mut ranges = VecDeque::new();
1396 let mut current_start = None;
1397 let mut current_points = 0usize;
1398
1399 for (idx, metric) in metrics.iter().enumerate() {
1400 let metric_points = metric.values().len();
1401 if metric_points == 0 {
1402 if let Some(start) = current_start.take() {
1404 if start < idx {
1405 ranges.push_back(start..idx);
1406 }
1407 }
1408 context.telemetry.events_dropped_encoder().increment(1);
1409 current_points = 0;
1410 continue;
1411 }
1412
1413 if !context.payload_limits.point_count_fits(metric_points) {
1414 context.serializer_telemetry.record_item_too_big();
1417 if let Some(start) = current_start.take() {
1418 if start < idx {
1419 ranges.push_back(start..idx);
1420 }
1421 }
1422 warn!(
1423 payload_kind,
1424 data_points = metric_points,
1425 point_limit = context.payload_limits.max_points_per_payload,
1426 "Dropping oversized V3 metric that exceeds the point-count limit."
1427 );
1428 context.telemetry.events_dropped_encoder().increment(1);
1429 current_points = 0;
1430 continue;
1431 }
1432
1433 let would_exceed_point_limit =
1434 current_points > 0 && !context.payload_limits.point_count_fits(current_points + metric_points);
1435 if would_exceed_point_limit {
1436 context
1439 .serializer_telemetry
1440 .record_split_reason(V3PayloadSplitReason::MaxPoints);
1441 if let Some(start) = current_start {
1442 ranges.push_back(start..idx);
1443 }
1444 current_start = Some(idx);
1445 current_points = 0;
1446 } else if current_start.is_none() {
1447 current_start = Some(idx);
1448 }
1449
1450 current_points += metric_points;
1451 }
1452
1453 if let Some(start) = current_start {
1454 if start < metrics.len() {
1455 ranges.push_back(start..metrics.len());
1456 }
1457 }
1458
1459 ranges
1460}
1461
1462fn uuid_to_header_value(uuid: &Uuid) -> HeaderValue {
1464 let s = uuid.as_hyphenated().to_string();
1465 HeaderValue::try_from(s).expect("hyphenated UUID should be a valid header value")
1466}
1467
1468fn usize_to_header_value(value: usize) -> HeaderValue {
1470 let s = value.to_string();
1471 HeaderValue::try_from(s).expect("usize should be a valid header value")
1472}
1473
1474async fn flush_payload(
1475 mut request: Request<FrozenChunkedBytesBuffer>, event_count: usize, data_point_count: usize,
1476 payloads_tx: &mut mpsc::Sender<Payload>, batch_id: Option<&Uuid>, batch_seq: usize, batch_len: usize,
1477 payload_info: Option<MetricsPayloadInfo>,
1478) -> Result<(), GenericError> {
1479 if let Some(batch_id) = batch_id {
1481 let headers = request.headers_mut();
1482 headers.insert("X-Metrics-Request-ID", uuid_to_header_value(batch_id));
1483 headers.insert("X-Metrics-Request-Seq", usize_to_header_value(batch_seq));
1484 headers.insert("X-Metrics-Request-Len", usize_to_header_value(batch_len));
1485 }
1486
1487 let mut payload_meta = PayloadMetadata::from_event_and_data_point_count(event_count, data_point_count);
1488 if let Some(info) = payload_info {
1489 payload_meta = payload_meta.with(info);
1490 }
1491 let http_payload = HttpPayload::new(payload_meta, request);
1492 let payload = Payload::Http(http_payload);
1493
1494 payloads_tx
1495 .send(payload)
1496 .await
1497 .error_context("Failed to send payload.")?;
1498
1499 Ok(())
1500}
1501
1502fn encode_v3_metrics_batch(
1504 metrics: &[Metric], additional_tags: &SharedTagSet,
1505) -> Result<V3EncodedMetrics, GenericError> {
1506 let mut writer = V3Writer::new();
1507 let mut tags_deduplicator = ReusableDeduplicator::new();
1508
1509 for metric in metrics {
1510 write_metric_to_v3(&mut writer, metric, additional_tags, &mut tags_deduplicator);
1511 }
1512
1513 writer
1514 .finalize()
1515 .map_err(|e| generic_error!("Failed to serialize V3 payload: {}", e))
1516}
1517
1518fn write_metric_to_v3(
1520 writer: &mut V3Writer, metric: &Metric, additional_tags: &SharedTagSet,
1521 tags_deduplicator: &mut ReusableDeduplicator<Tag>,
1522) {
1523 let metric_type = match metric.values() {
1524 MetricValues::Counter(..) => V3MetricType::Count,
1525 MetricValues::Rate(..) => V3MetricType::Rate,
1526 MetricValues::Gauge(..) | MetricValues::Set(..) => V3MetricType::Gauge,
1527 MetricValues::Histogram(..) | MetricValues::Distribution(..) => V3MetricType::Sketch,
1528 };
1529 let is_sketch = metric_type == V3MetricType::Sketch;
1530
1531 let mut builder = writer.write(metric_type, metric.context().name());
1532
1533 let chained_tags = metric
1535 .context()
1536 .tags()
1537 .into_iter()
1538 .chain(additional_tags)
1539 .chain(metric.context().origin_tags());
1540 let all_tags = tags_deduplicator
1541 .deduplicated(chained_tags)
1542 .filter(|t| is_sketch || !is_v3_series_resource_tag(t) && !is_v3_series_device_tag(t))
1543 .map(|t| t.as_str());
1544 builder.set_tags(all_tags);
1545
1546 let mut resources = Vec::new();
1548 if let Some(host) = metric.metadata().hostname().filter(|host| !host.is_empty()) {
1549 resources.push(("host", host));
1550 }
1551 if !is_sketch {
1552 let mut device_resource = None;
1553 let chained_tags = metric
1554 .context()
1555 .origin_tags()
1556 .into_iter()
1557 .chain(metric.context().tags())
1558 .chain(additional_tags);
1559 for tag in tags_deduplicator.deduplicated(chained_tags) {
1560 if is_v3_series_device_tag(tag) {
1561 device_resource = tag.value().filter(|device| !device.is_empty());
1562 } else if is_v3_series_resource_tag(tag) {
1563 if let Some((rtype, rname)) = tag.value().and_then(|value| value.split_once(':')) {
1564 if !rtype.is_empty() && !rname.is_empty() {
1565 resources.push((rtype, rname));
1566 }
1567 }
1568 }
1569 }
1570 if let Some(device) = device_resource {
1571 let device_idx = usize::from(metric.metadata().hostname().is_some_and(|host| !host.is_empty()));
1572 resources.insert(device_idx, ("device", device));
1573 }
1574 }
1575 builder.set_resources(&resources);
1576
1577 if let Some(origin) = metric.metadata().origin() {
1579 match origin {
1580 MetricOrigin::SourceType(source_type) => {
1581 builder.set_source_type(source_type.as_ref());
1582 }
1583 MetricOrigin::OriginMetadata {
1584 product,
1585 subproduct,
1586 product_detail,
1587 } => {
1588 builder.set_origin(*product, *subproduct, *product_detail, false);
1589 }
1590 }
1591 }
1592
1593 if metric_type != V3MetricType::Sketch {
1594 if let Some(unit) = metric.metadata().unit() {
1595 builder.set_unit(unit);
1596 }
1597 }
1598
1599 match metric.values() {
1601 MetricValues::Counter(points) | MetricValues::Gauge(points) => {
1602 for (ts, val) in points {
1603 if !emittable_scalar_point(val) {
1604 continue;
1605 }
1606 let timestamp = ts.map(|t| t.get() as i64).unwrap_or(0);
1607 builder.add_point(timestamp, val);
1608 }
1609 }
1610 MetricValues::Rate(points, interval) => {
1611 builder.set_interval(interval.as_secs());
1612 for (ts, val) in points {
1613 let scaled = val / interval.as_secs_f64();
1615 if !emittable_scalar_point(scaled) {
1616 continue;
1617 }
1618 let timestamp = ts.map(|t| t.get() as i64).unwrap_or(0);
1619 builder.add_point(timestamp, scaled);
1620 }
1621 }
1622 MetricValues::Set(points) => {
1623 for (ts, count) in points {
1625 if !emittable_scalar_point(count) {
1626 continue;
1627 }
1628 let timestamp = ts.map(|t| t.get() as i64).unwrap_or(0);
1629 builder.add_point(timestamp, count);
1630 }
1631 }
1632 MetricValues::Distribution(sketches) => {
1633 for (ts, sketch) in sketches {
1634 let timestamp = ts.map(|t| t.get() as i64).unwrap_or(0);
1635 if !sketch.is_empty() {
1636 let bin_keys: Vec<i32> = sketch.bins().iter().map(|b| b.key()).collect();
1637 let bin_counts: Vec<u32> = sketch.bins().iter().map(|b| b.count()).collect();
1638 builder.add_sketch(
1639 timestamp,
1640 sketch.count() as i64,
1641 sketch.sum().unwrap_or(0.0),
1642 sketch.min().unwrap_or(0.0),
1643 sketch.max().unwrap_or(0.0),
1644 &bin_keys,
1645 &bin_counts,
1646 );
1647 }
1648 }
1649 }
1650 MetricValues::Histogram(histograms) => {
1651 for (ts, histogram) in histograms {
1652 let timestamp = ts.map(|t| t.get() as i64).unwrap_or(0);
1653 let mut sketch = DDSketch::default();
1655 for sample in histogram.samples() {
1656 sketch.insert_n(sample.value.into_inner(), sample.weight.0 as u64);
1657 }
1658 if !sketch.is_empty() {
1659 let bin_keys: Vec<i32> = sketch.bins().iter().map(|b| b.key()).collect();
1660 let bin_counts: Vec<u32> = sketch.bins().iter().map(|b| b.count()).collect();
1661 builder.add_sketch(
1662 timestamp,
1663 sketch.count() as i64,
1664 sketch.sum().unwrap_or(0.0),
1665 sketch.min().unwrap_or(0.0),
1666 sketch.max().unwrap_or(0.0),
1667 &bin_keys,
1668 &bin_counts,
1669 );
1670 }
1671 }
1672 }
1673 }
1674
1675 builder.close();
1676}
1677
1678#[inline]
1679fn emittable_scalar_point(point: f64) -> bool {
1680 point.is_finite()
1681}
1682
1683fn is_v3_series_device_tag(tag: &Tag) -> bool {
1684 tag.name() == "device" && tag.value().is_some()
1685}
1686
1687fn is_v3_series_resource_tag(tag: &Tag) -> bool {
1688 tag.name() == "dd.internal.resource" && tag.value().is_some()
1689}
1690
1691async fn create_v3_request(
1693 endpoint_uri: &str, mut encoded: V3EncodedMetrics, compression_scheme: CompressionScheme,
1694) -> Result<V3EncodedRequest, GenericError> {
1695 let mut header_buf = [0; 16];
1698 let header_len = {
1699 let mut header_writer = CodedOutputStream::bytes(&mut header_buf);
1700 header_writer.write_tag(3, WireType::LengthDelimited)?;
1701 header_writer.write_uint64_no_tag(encoded.payload.len() as u64)?;
1702 header_writer.flush()?;
1703 header_writer.total_bytes_written() as usize
1704 };
1705
1706 let uncompressed_len = header_len + encoded.payload.len();
1707 for column in &mut encoded.stats.columns {
1708 column.compressed_len = compressed_v3_len(&column.bytes, compression_scheme)
1709 .await
1710 .error_context("Failed to measure V3 column compressed size.")?;
1711 }
1712
1713 let buffer = ChunkedBytesBuffer::new(RB_BUFFER_CHUNK_SIZE);
1714 let mut compressor = Compressor::from_scheme(compression_scheme, buffer);
1715 compressor
1716 .write_all(&header_buf[..header_len])
1717 .await
1718 .error_context("Failed to compress V3 payload.")?;
1719 compressor
1720 .write_all(&encoded.payload)
1721 .await
1722 .error_context("Failed to compress V3 payload.")?;
1723 compressor
1724 .flush()
1725 .await
1726 .error_context("Failed to flush V3 compressor.")?;
1727 compressor
1728 .shutdown()
1729 .await
1730 .error_context("Failed to shutdown V3 compressor.")?;
1731
1732 let compressed_buf = compressor.into_inner().freeze();
1733 let compressed_len = compressed_buf.len();
1734
1735 let mut builder = Request::builder()
1736 .method(Method::POST)
1737 .uri(endpoint_uri)
1738 .header(http::header::CONTENT_TYPE, "application/x-protobuf");
1739
1740 if let Some(encoding) = content_encoding_for_scheme(compression_scheme) {
1741 builder = builder.header(http::header::CONTENT_ENCODING, encoding);
1742 }
1743
1744 let request = builder
1745 .body(compressed_buf)
1746 .map_err(|e| generic_error!("Failed to build V3 request: {}", e))?;
1747
1748 Ok(V3EncodedRequest {
1749 request,
1750 compressed_len,
1751 uncompressed_len,
1752 stats: encoded.stats,
1753 })
1754}
1755
1756fn content_encoding_for_scheme(compression_scheme: CompressionScheme) -> Option<HeaderValue> {
1757 match compression_scheme {
1758 CompressionScheme::Noop => None,
1759 CompressionScheme::Gzip(_) => Some(HeaderValue::from_static("gzip")),
1760 CompressionScheme::Zlib(_) => Some(HeaderValue::from_static("deflate")),
1761 CompressionScheme::Zstd(_) => Some(HeaderValue::from_static("zstd")),
1762 }
1763}
1764
1765#[cfg(test)]
1766mod tests {
1767 use std::{io::Cursor, sync::Arc};
1768
1769 use bytes::Bytes;
1770 use saluki_context::{
1771 tags::{Tag, TagSet},
1772 Context,
1773 };
1774 use saluki_core::data_model::{
1775 event::{metric::MetricMetadata, Event},
1776 payload::Payload,
1777 };
1778 use saluki_metrics::test::TestRecorder;
1779 use stringtheory::MetaString;
1780 use tokio::time::timeout;
1781
1782 use super::*;
1783
1784 #[test]
1785 fn deser_agent_v3_api_nested_settings() {
1786 let raw = r#"
1787serializer_experimental_use_v3_api:
1788 compression_level: 7
1789 series:
1790 endpoints:
1791 - https://app.datadoghq.com
1792 validate: true
1793 use_beta: true
1794 beta_route: /api/intake/metrics/custom/series
1795 shadow_sample_rate: 0.25
1796 shadow_sites:
1797 - datadoghq.eu
1798 sketches:
1799 endpoints:
1800 - https://app.datadoghq.eu
1801"#;
1802
1803 let config =
1804 serde_yaml::from_str::<DatadogMetricsConfiguration>(raw).expect("configuration should deserialize");
1805
1806 assert_eq!(7, config.v3_api.compression_level);
1807 assert_eq!(
1808 Some("https://app.datadoghq.com"),
1809 config.v3_api.series.endpoints.first().map(String::as_str)
1810 );
1811 assert!(config.v3_api.series.validate);
1812 assert!(config.v3_api.series.use_beta);
1813 assert_eq!("/api/intake/metrics/custom/series", config.v3_api.series.beta_route);
1814 assert_eq!(0.25, config.v3_api.series.shadow_sample_rate);
1815 assert_eq!(vec!["datadoghq.eu"], config.v3_api.series.shadow_sites);
1816 assert_eq!(
1817 Some("https://app.datadoghq.eu"),
1818 config.v3_api.sketches.endpoints.first().map(String::as_str)
1819 );
1820 }
1821
1822 #[test]
1823 fn agent_v3_api_shadow_defaults_match_agent() {
1824 let config = serde_yaml::from_str::<DatadogMetricsConfiguration>("").expect("configuration should deserialize");
1825
1826 assert_eq!(0.0, config.v3_api.series.shadow_sample_rate);
1827 assert_eq!(vec!["datadoghq.com"], config.v3_api.series.shadow_sites);
1828 }
1829
1830 #[test]
1831 fn shadow_sample_matches_agent_threshold_behavior() {
1832 assert!(!shadow_sample_matches(0.0, 0.0));
1833 assert!(shadow_sample_matches(0.5, 0.4));
1834 assert!(!shadow_sample_matches(0.5, 0.5));
1835 assert!(!shadow_sample_matches(0.5, 0.6));
1836 }
1837
1838 #[test]
1839 fn shadow_sampling_is_disabled_for_v1_series_baseline_or_v3_incompatible_compressor() {
1840 assert!(series_shadow_config_for_endpoint(MetricsEndpoint::SeriesV2, 1.0, false).is_enabled());
1841 assert!(!series_shadow_config_for_endpoint(MetricsEndpoint::SeriesV1, 1.0, false).is_enabled());
1842 assert!(!series_shadow_config_for_endpoint(MetricsEndpoint::SeriesV2, 1.0, true).is_enabled());
1843 }
1844
1845 #[test]
1846 fn metrics_v3_disabled_by_compressor_uses_v2_only() {
1847 assert_eq!(
1848 MetricsEncoderMode::V2Only,
1849 metrics_encoder_mode_for_config(true, false, true)
1850 );
1851 assert_eq!(
1852 MetricsEncoderMode::V2Only,
1853 metrics_encoder_mode_for_config(true, true, true)
1854 );
1855 assert_eq!(
1856 MetricsEncoderMode::V3Enabled,
1857 metrics_encoder_mode_for_config(true, false, false)
1858 );
1859 assert_eq!(
1860 MetricsEncoderMode::Validation,
1861 metrics_encoder_mode_for_config(true, true, false)
1862 );
1863 }
1864
1865 #[test]
1866 fn agent_default_v3_does_not_enable_opw_only_encoder_mode() {
1867 let series_config = UseV3ApiSeriesConfig::default();
1868 let invalid_metrics_primary_override =
1869 selected_metrics_primary_v3_override(true, "http://[::1", false, true, "http://vector.example.com", false);
1870
1871 assert!(!series_v3_can_be_enabled_for_config(
1872 false,
1873 Some(false),
1874 false,
1875 &series_config
1876 ));
1877 assert!(series_v3_can_be_enabled_for_config(
1878 false,
1879 Some(true),
1880 false,
1881 &series_config
1882 ));
1883 assert!(series_v3_can_be_enabled_for_config(
1884 false,
1885 Some(false),
1886 true,
1887 &series_config
1888 ));
1889 assert!(series_v3_can_be_enabled_for_config(
1890 true,
1891 Some(false),
1892 false,
1893 &series_config
1894 ));
1895 assert_eq!(None, invalid_metrics_primary_override);
1896 assert!(series_v3_can_be_enabled_for_config(
1897 false,
1898 invalid_metrics_primary_override,
1899 false,
1900 &series_config
1901 ));
1902 }
1903
1904 #[tokio::test]
1905 async fn create_v3_request_uses_configured_endpoint_uri() {
1906 let encoded = V3Writer::new().finalize().expect("empty V3 payload should encode");
1907 let request = create_v3_request("/api/intake/metrics/custom/series", encoded, CompressionScheme::noop())
1908 .await
1909 .expect("request should be created");
1910
1911 assert_eq!("/api/intake/metrics/custom/series", request.request.uri());
1912 }
1913
1914 #[tokio::test]
1915 async fn create_v3_request_uses_single_stream_body_and_column_telemetry() {
1916 let metrics = vec![Metric::counter("v3.single.stream", 42.0)];
1917 let encoded = encode_v3_metrics_batch(&metrics, &SharedTagSet::default()).expect("metrics should encode to V3");
1918 let expected_payload = encoded.payload.clone();
1919
1920 let request = create_v3_request(V3_SERIES_ENDPOINT_URI, encoded, CompressionScheme::noop())
1921 .await
1922 .expect("request should be created");
1923
1924 for column in &request.stats.columns {
1925 assert_eq!(column.compressed_len, column.bytes.len());
1926 }
1927
1928 let mut expected_body = Vec::new();
1929 {
1930 let mut os = CodedOutputStream::vec(&mut expected_body);
1931 os.write_tag(3, WireType::LengthDelimited).unwrap();
1932 os.write_uint64_no_tag(expected_payload.len() as u64).unwrap();
1933 os.flush().unwrap();
1934 }
1935 expected_body.extend_from_slice(&expected_payload);
1936
1937 assert_eq!(request.request.into_body().into_bytes(), Bytes::from(expected_body));
1938 }
1939
1940 #[tokio::test]
1941 async fn create_v3_request_zstd_body_decodes_to_metric_payload() {
1942 let metrics = vec![Metric::counter("v3.single.stream.zstd", 42.0)];
1943 let encoded = encode_v3_metrics_batch(&metrics, &SharedTagSet::default()).expect("metrics should encode to V3");
1944 let expected_payload = encoded.payload.clone();
1945
1946 let request = create_v3_request(V3_SERIES_ENDPOINT_URI, encoded, CompressionScheme::zstd_default())
1947 .await
1948 .expect("request should be created");
1949
1950 for column in &request.stats.columns {
1951 let expected_compressed_len = compressed_v3_len(&column.bytes, CompressionScheme::zstd_default())
1952 .await
1953 .expect("column compressed size should be measured");
1954 assert_eq!(column.compressed_len, expected_compressed_len);
1955 }
1956
1957 let mut expected_body = Vec::new();
1958 {
1959 let mut os = CodedOutputStream::vec(&mut expected_body);
1960 os.write_tag(3, WireType::LengthDelimited).unwrap();
1961 os.write_uint64_no_tag(expected_payload.len() as u64).unwrap();
1962 os.flush().unwrap();
1963 }
1964 expected_body.extend_from_slice(&expected_payload);
1965
1966 let decoded_body = zstd::stream::decode_all(Cursor::new(request.request.into_body().into_bytes()))
1967 .expect("compressed V3 body should decode");
1968 assert_eq!(decoded_body, expected_body);
1969 }
1970
1971 #[test]
1972 fn v3_drops_non_finite_scalar_points() {
1973 const NUM_POINTS_FIELD_NUMBER: u32 = 15;
1974 const VALUE_SINT64_FIELD_NUMBER: u32 = 17;
1975
1976 let metrics = vec![
1977 Metric::from_parts(
1978 Context::from_static_parts("v3.finite.counter", &[]),
1979 MetricValues::counter([(1, 1.0_f64), (2, f64::NAN), (3, f64::INFINITY), (4, 2.0)]),
1980 MetricMetadata::default(),
1981 ),
1982 Metric::from_parts(
1983 Context::from_static_parts("v3.finite.gauge", &[]),
1984 MetricValues::gauge([(1, 3.0_f64), (2, f64::NAN), (3, 4.0)]),
1985 MetricMetadata::default(),
1986 ),
1987 Metric::from_parts(
1988 Context::from_static_parts("v3.finite.rate", &[]),
1989 MetricValues::rate(
1990 [(1, 30.0_f64), (2, f64::INFINITY), (3, f64::NAN)],
1991 Duration::from_secs(10),
1992 ),
1993 MetricMetadata::default(),
1994 ),
1995 Metric::set("v3.finite.set", "alpha"),
1996 ];
1997
1998 let encoded = encode_v3_metrics_batch(&metrics, &SharedTagSet::default()).expect("metrics should encode to V3");
1999 let column = |field_number| {
2000 encoded
2001 .stats
2002 .columns
2003 .iter()
2004 .find(|column| column.field_number == field_number)
2005 .map(|column| column.bytes.as_slice())
2006 };
2007
2008 assert_eq!(column(NUM_POINTS_FIELD_NUMBER), Some(&[2, 2, 1, 1][..]));
2009 assert_eq!(column(VALUE_SINT64_FIELD_NUMBER), Some(&[2, 4, 6, 8, 6, 2][..]));
2011 assert_eq!(encoded.stats.value_encoding_stats.sint64, 6);
2012 assert_eq!(encoded.stats.value_encoding_stats.float64, 0);
2013 }
2014
2015 #[tokio::test]
2016 async fn v3_serializer_stats_record_agent_style_value_and_column_counters() {
2017 const VALUE_SINT64_FIELD_NUMBER: u32 = 17;
2018
2019 let recorder = TestRecorder::default();
2020 let _local = metrics::set_default_local_recorder(&recorder);
2021 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2022 let metrics = vec![
2023 Metric::gauge("v3.telemetry.zero", [(123, 0.0), (124, 0.0)]),
2024 Metric::counter("v3.telemetry.sint64", [(123, 100.0), (124, 200.0)]),
2025 Metric::gauge("v3.telemetry.float32", [(123, 1.5), (124, 2.25)]),
2026 Metric::gauge("v3.telemetry.float64", [(123, (1i64 << 30) as f64), (124, 1.5)]),
2027 ];
2028 let encoded = encode_v3_metrics_batch(&metrics, &SharedTagSet::default()).expect("metrics should encode to V3");
2029 let request = create_v3_request(V3_SERIES_ENDPOINT_URI, encoded, CompressionScheme::noop())
2030 .await
2031 .expect("request should be created");
2032
2033 let value_sint64_column = request
2034 .stats
2035 .columns
2036 .iter()
2037 .find(|column| column.field_number == VALUE_SINT64_FIELD_NUMBER)
2038 .expect("sint64 value column should be present");
2039 let value_sint64_len = value_sint64_column.bytes.len() as u64;
2040 assert_eq!(value_sint64_column.compressed_len as u64, value_sint64_len);
2041
2042 record_v3_serializer_stats(&serializer_telemetry, &request.stats);
2043
2044 assert_eq!(
2045 recorder.counter(("serializer.v3_values_count", &[("type", "zero")])),
2046 Some(2)
2047 );
2048 assert_eq!(
2049 recorder.counter(("serializer.v3_values_count", &[("type", "sint64")])),
2050 Some(2)
2051 );
2052 assert_eq!(
2053 recorder.counter(("serializer.v3_values_count", &[("type", "float32")])),
2054 Some(2)
2055 );
2056 assert_eq!(
2057 recorder.counter(("serializer.v3_values_count", &[("type", "float64")])),
2058 Some(2)
2059 );
2060 assert_eq!(
2061 recorder.counter((
2062 "serializer.v3_column_size",
2063 &[("column", "ValueSint64"), ("compressed", "uncompressed")]
2064 )),
2065 Some(value_sint64_len)
2066 );
2067 assert_eq!(
2068 recorder.counter((
2069 "serializer.v3_column_size",
2070 &[("column", "ValueSint64"), ("compressed", "compressed")]
2071 )),
2072 Some(value_sint64_len)
2073 );
2074 }
2075
2076 async fn create_v3_test_request(metrics: &[Metric]) -> V3EncodedRequest {
2077 let encoded = encode_v3_metrics_batch(metrics, &SharedTagSet::default()).expect("metrics should encode to V3");
2078 create_v3_request(V3_SERIES_ENDPOINT_URI, encoded, CompressionScheme::noop())
2079 .await
2080 .expect("request should be created")
2081 }
2082
2083 fn test_v3_flush_context<'a>(
2084 ep_config: &'a EndpointConfiguration, payload_limits: V3PayloadLimits,
2085 serializer_telemetry: &'a V3SerializerTelemetry, telemetry: &'a ComponentTelemetry,
2086 ) -> V3FlushContext<'a> {
2087 V3FlushContext {
2088 endpoint_config: ep_config,
2089 payload_limits,
2090 series_endpoint_uri: V3_SERIES_ENDPOINT_URI,
2091 serializer_telemetry,
2092 telemetry,
2093 }
2094 }
2095
2096 #[tokio::test]
2097 async fn v3_payload_requests_split_by_compressed_size_limit() {
2098 let metrics = vec![
2099 Metric::counter("v3.compressed.split.one", 1.0),
2100 Metric::counter("v3.compressed.split.two", 2.0),
2101 ];
2102 let single_request = create_v3_test_request(&metrics[..1]).await;
2103 let combined_request = create_v3_test_request(&metrics).await;
2104 assert!(combined_request.compressed_len > single_request.compressed_len);
2105
2106 let limits = V3PayloadLimits::new(single_request.compressed_len, usize::MAX, 10_000, 10_000);
2107 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2108 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2109 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2110
2111 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2112 let requests = encode_v3_payload_requests(V3_SERIES_ENDPOINT_URI, &metrics, context, "series").await;
2113
2114 assert_eq!(2, requests.len());
2115 assert_eq!(
2116 vec![1, 1],
2117 requests.iter().map(|request| request.event_count).collect::<Vec<_>>()
2118 );
2119 assert!(requests
2120 .iter()
2121 .all(|request| request.request.body().len() <= limits.max_compressed_size));
2122 }
2123
2124 #[tokio::test]
2125 async fn v3_serializer_stats_record_payload_full_split_reason() {
2126 let metrics = vec![
2127 Metric::counter("v3.telemetry.payload_full.one", 1.0),
2128 Metric::counter("v3.telemetry.payload_full.two", 2.0),
2129 ];
2130 let single_request = create_v3_test_request(&metrics[..1]).await;
2131 let limits = V3PayloadLimits::new(single_request.compressed_len, usize::MAX, 10_000, 10_000);
2132 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2133 let recorder = TestRecorder::default();
2134 let _local = metrics::set_default_local_recorder(&recorder);
2135 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2136 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2137
2138 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2139 let requests = encode_v3_payload_requests(V3_SERIES_ENDPOINT_URI, &metrics, context, "series").await;
2140
2141 assert_eq!(2, requests.len());
2142 assert_eq!(
2143 recorder.counter(("serializer.v3_payload_split_reason", &[("reason", "payload_full")])),
2144 Some(1)
2145 );
2146 }
2147
2148 #[tokio::test]
2149 async fn v3_serializer_stats_record_item_too_big_split_reason() {
2150 let metrics = vec![Metric::counter("v3.telemetry.item_too_big", 1.0)];
2151 let request = create_v3_test_request(&metrics).await;
2152 let limits = V3PayloadLimits::new(request.compressed_len - 1, usize::MAX, 10_000, 10_000);
2153 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2154 let recorder = TestRecorder::default();
2155 let _local = metrics::set_default_local_recorder(&recorder);
2156 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2157 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2158
2159 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2160 let requests = encode_v3_payload_requests(V3_SERIES_ENDPOINT_URI, &metrics, context, "series").await;
2161
2162 assert!(requests.is_empty());
2163 assert_eq!(recorder.counter("serializer.v3_item_too_big"), Some(1));
2164 assert_eq!(
2165 recorder.counter(("serializer.v3_payload_split_reason", &[("reason", "item_too_big")])),
2166 Some(1)
2167 );
2168 }
2169
2170 #[tokio::test]
2171 async fn v3_payload_requests_split_by_uncompressed_size_limit() {
2172 let metrics = vec![
2173 Metric::counter("v3.uncompressed.split.one", 1.0),
2174 Metric::counter("v3.uncompressed.split.two", 2.0),
2175 ];
2176 let single_request = create_v3_test_request(&metrics[..1]).await;
2177 let combined_request = create_v3_test_request(&metrics).await;
2178 assert!(combined_request.uncompressed_len > single_request.uncompressed_len);
2179
2180 let limits = V3PayloadLimits::new(usize::MAX, single_request.uncompressed_len, 10_000, 10_000);
2181 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2182 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2183 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2184
2185 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2186 let requests = encode_v3_payload_requests(V3_SERIES_ENDPOINT_URI, &metrics, context, "series").await;
2187
2188 assert_eq!(2, requests.len());
2189 assert_eq!(
2190 vec![1, 1],
2191 requests.iter().map(|request| request.event_count).collect::<Vec<_>>()
2192 );
2193 }
2194
2195 #[test]
2196 fn v3_metric_ranges_split_by_point_limit() {
2197 let metrics = vec![
2198 Metric::counter("v3.points.split.one", [(123, 1.0), (124, 2.0)]),
2199 Metric::counter("v3.points.split.two", [(123, 3.0), (124, 4.0)]),
2200 Metric::counter("v3.points.split.three", 5.0),
2201 ];
2202 let limits = V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 3);
2203 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2204 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2205 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2206 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2207
2208 let ranges = split_v3_metric_ranges_by_point_limit(&metrics, context, "series")
2209 .into_iter()
2210 .collect::<Vec<_>>();
2211
2212 assert_eq!(vec![0..1, 1..3], ranges);
2213 }
2214
2215 #[test]
2216 fn v3_serializer_stats_record_max_points_split_reason() {
2217 let metrics = vec![
2218 Metric::counter("v3.telemetry.max_points.one", [(123, 1.0), (124, 2.0)]),
2219 Metric::counter("v3.telemetry.max_points.two", [(123, 3.0), (124, 4.0)]),
2220 ];
2221 let limits = V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 2);
2222 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2223 let recorder = TestRecorder::default();
2224 let _local = metrics::set_default_local_recorder(&recorder);
2225 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2226 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2227 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2228
2229 let ranges = split_v3_metric_ranges_by_point_limit(&metrics, context, "series")
2230 .into_iter()
2231 .collect::<Vec<_>>();
2232
2233 assert_eq!(vec![0..1, 1..2], ranges);
2234 assert_eq!(
2235 recorder.counter(("serializer.v3_payload_split_reason", &[("reason", "max_points")])),
2236 Some(1)
2237 );
2238 }
2239
2240 #[test]
2241 fn v3_metric_ranges_drop_oversized_metric_after_previous_range() {
2242 let metrics = vec![
2243 Metric::counter("v3.points.oversized.before", [(123, 1.0), (124, 2.0)]),
2244 Metric::counter(
2245 "v3.points.oversized.too_big",
2246 [(123, 3.0), (124, 4.0), (125, 5.0), (126, 6.0)],
2247 ),
2248 Metric::counter("v3.points.oversized.after", 7.0),
2249 ];
2250 let limits = V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 3);
2251 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2252 let recorder = TestRecorder::default();
2253 let _local = metrics::set_default_local_recorder(&recorder);
2254 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2255 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2256 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2257
2258 let ranges = split_v3_metric_ranges_by_point_limit(&metrics, context, "series")
2259 .into_iter()
2260 .collect::<Vec<_>>();
2261
2262 assert_eq!(vec![0..1, 2..3], ranges);
2263 assert_eq!(recorder.counter("serializer.v3_item_too_big"), Some(1));
2264 }
2265
2266 #[test]
2267 fn v3_metric_ranges_skip_zero_point_metrics() {
2268 let metrics = vec![
2269 Metric::counter("v3.points.zero.before", 1.0),
2270 Metric::counter("v3.points.zero.empty", &[] as &[f64]),
2271 Metric::counter("v3.points.zero.after", 2.0),
2272 ];
2273 let limits = V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 10_000);
2274 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2275 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2276 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2277 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2278
2279 let ranges = split_v3_metric_ranges_by_point_limit(&metrics, context, "series")
2280 .into_iter()
2281 .collect::<Vec<_>>();
2282
2283 assert_eq!(vec![0..1, 2..3], ranges);
2284 }
2285
2286 #[tokio::test]
2287 async fn v3_split_flush_uses_payload_request_batch_headers() {
2288 let mut metrics = vec![
2289 Metric::counter("v3.headers.split.one", 1.0),
2290 Metric::counter("v3.headers.split.two", 2.0),
2291 ];
2292 let single_request = create_v3_test_request(&metrics[..1]).await;
2293 let combined_request = create_v3_test_request(&metrics).await;
2294 assert!(combined_request.compressed_len > single_request.compressed_len);
2295
2296 let limits = V3PayloadLimits::new(single_request.compressed_len, usize::MAX, 10_000, 10_000);
2297 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2298 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2299 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2300 let batch_id = Uuid::now_v7();
2301 let (mut payloads_tx, mut payloads_rx) = tokio::sync::mpsc::channel(8);
2302
2303 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2304 encode_and_flush_v3_series_metrics(
2305 context,
2306 &mut metrics,
2307 &mut payloads_tx,
2308 Some(&batch_id),
2309 Some(MetricsPayloadInfo::v3_series()),
2310 )
2311 .await
2312 .expect("V3 metrics should flush");
2313
2314 for expected_seq in 0..2 {
2315 let payload = payloads_rx.recv().await.expect("payload should be emitted");
2316 let Payload::Http(http_payload) = payload else {
2317 panic!("expected HTTP payload");
2318 };
2319 let (_, request) = http_payload.into_parts();
2320 assert_eq!(
2321 batch_id.as_hyphenated().to_string(),
2322 request
2323 .headers()
2324 .get("X-Metrics-Request-ID")
2325 .expect("batch ID header should be present")
2326 .to_str()
2327 .expect("batch ID header should be valid")
2328 );
2329 assert_eq!(
2330 expected_seq.to_string(),
2331 request
2332 .headers()
2333 .get("X-Metrics-Request-Seq")
2334 .expect("batch sequence header should be present")
2335 .to_str()
2336 .expect("batch sequence header should be valid")
2337 );
2338 assert_eq!(
2339 "2",
2340 request
2341 .headers()
2342 .get("X-Metrics-Request-Len")
2343 .expect("batch length header should be present")
2344 .to_str()
2345 .expect("batch length header should be valid")
2346 );
2347 }
2348
2349 assert!(metrics.is_empty());
2350 }
2351
2352 #[tokio::test]
2353 async fn v3_sketch_flush_uses_split_payload_requests() {
2354 let mut metrics = vec![
2355 Metric::distribution("v3.sketch.split.one", [1.0, 2.0, 3.0]),
2356 Metric::distribution("v3.sketch.split.two", [4.0, 5.0, 6.0]),
2357 ];
2358 let single_request = create_v3_test_request(&metrics[..1]).await;
2359 let combined_request = create_v3_test_request(&metrics).await;
2360 assert!(combined_request.compressed_len > single_request.compressed_len);
2361
2362 let limits = V3PayloadLimits::new(single_request.compressed_len, usize::MAX, 10_000, 10_000);
2363 let ep_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2364 let telemetry = ComponentTelemetry::from_builder(&MetricsBuilder::default());
2365 let serializer_telemetry = V3SerializerTelemetry::from_builder(&MetricsBuilder::default());
2366 let batch_id = Uuid::now_v7();
2367 let (mut payloads_tx, mut payloads_rx) = tokio::sync::mpsc::channel(8);
2368
2369 let context = test_v3_flush_context(&ep_config, limits, &serializer_telemetry, &telemetry);
2370 encode_and_flush_v3_sketch_metrics(
2371 context,
2372 &mut metrics,
2373 &mut payloads_tx,
2374 Some(&batch_id),
2375 Some(MetricsPayloadInfo::v3_sketches()),
2376 )
2377 .await
2378 .expect("V3 sketches should flush");
2379
2380 for expected_seq in 0..2 {
2381 let payload = payloads_rx.recv().await.expect("payload should be emitted");
2382 let Payload::Http(http_payload) = payload else {
2383 panic!("expected HTTP payload");
2384 };
2385 let (_, request) = http_payload.into_parts();
2386 assert_eq!(V3_SKETCHES_ENDPOINT_URI, request.uri());
2387 assert_eq!(
2388 expected_seq.to_string(),
2389 request
2390 .headers()
2391 .get("X-Metrics-Request-Seq")
2392 .expect("batch sequence header should be present")
2393 .to_str()
2394 .expect("batch sequence header should be valid")
2395 );
2396 assert_eq!(
2397 "2",
2398 request
2399 .headers()
2400 .get("X-Metrics-Request-Len")
2401 .expect("batch length header should be present")
2402 .to_str()
2403 .expect("batch length header should be valid")
2404 );
2405 }
2406
2407 assert!(metrics.is_empty());
2408 }
2409
2410 #[test]
2411 fn v3_series_metric_unit_refs_are_encoded_sparsely() {
2412 let context = Context::from_static_parts("my.timer.avg", &[]);
2413 let metadata = MetricMetadata::default().with_unit(MetaString::from_static("millisecond"));
2414 let gauge = Metric::from_parts(context, MetricValues::gauge([1.0_f64]), metadata);
2415 let context = Context::from_static_parts("my.counter", &[]);
2416 let no_unit = Metric::from_parts(context, MetricValues::gauge([2.0_f64]), MetricMetadata::default());
2417 let context = Context::from_static_parts("my.timer.max", &[]);
2418 let metadata = MetricMetadata::default().with_unit(MetaString::from_static("millisecond"));
2419 let same_unit = Metric::from_parts(context, MetricValues::gauge([3.0_f64]), metadata);
2420
2421 let payload = encode_v3_metrics_batch(&[gauge, no_unit, same_unit], &SharedTagSet::default())
2422 .expect("V3 metric should encode successfully")
2423 .payload;
2424
2425 let expected_unit_dict = [
2426 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',
2429 ];
2430 assert!(
2431 payload
2432 .windows(expected_unit_dict.len())
2433 .any(|window| window == expected_unit_dict),
2434 "V3 payload should contain DictUnitStr field for 'millisecond', got bytes: {:?}",
2435 payload
2436 );
2437
2438 let expected_unit_ref = [
2439 0xd2, 0x01, 0x02, 0x02, 0x00, ];
2443 assert!(
2444 payload
2445 .windows(expected_unit_ref.len())
2446 .any(|window| window == expected_unit_ref),
2447 "V3 payload should contain UnitRef field for 'millisecond', got bytes: {:?}",
2448 payload
2449 );
2450 }
2451
2452 #[test]
2453 fn v3_sketch_metric_unit_not_encoded() {
2454 let context = Context::from_static_parts("my.histogram", &[]);
2455 let metadata = MetricMetadata::default().with_unit(MetaString::from_static("millisecond"));
2456 let histogram = Metric::from_parts(context, MetricValues::histogram([1.0_f64]), metadata);
2457
2458 let payload = encode_v3_metrics_batch(&[histogram], &SharedTagSet::default())
2459 .expect("V3 sketch metric should encode successfully")
2460 .payload;
2461
2462 assert!(
2463 !payload
2464 .windows(b"millisecond".len())
2465 .any(|window| window == b"millisecond"),
2466 "V3 sketch payload should not contain unit bytes, matching the Agent V3 sketch builder: {:?}",
2467 payload
2468 );
2469 }
2470
2471 #[test]
2472 fn v3_series_promotes_device_and_internal_resource_tags_to_resources() {
2473 let context = Context::from_static_parts(
2474 "series.resources",
2475 &[
2476 "env:prod",
2477 "device:switch1",
2478 "dd.internal.resource:pod:pod-a",
2479 "dd.internal.resource:malformed",
2480 ],
2481 );
2482 let metadata = MetricMetadata::default().with_hostname(Some(Arc::from("host-a")));
2483 let metric = Metric::from_parts(context, MetricValues::gauge([1.0_f64]), metadata);
2484
2485 let payload = encode_v3_metrics_batch(&[metric], &SharedTagSet::default())
2486 .expect("V3 series should encode successfully")
2487 .payload;
2488
2489 assert_contains_bytes(&payload, b"env:prod");
2490 assert!(!contains_bytes(&payload, b"device:switch1"));
2491 assert!(!contains_bytes(&payload, b"dd.internal.resource:pod:pod-a"));
2492 assert!(!contains_bytes(&payload, b"dd.internal.resource:malformed"));
2493
2494 let expected_resource_dict = [
2495 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',
2498 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'-',
2499 b'a',
2500 ];
2501 assert_contains_bytes(&payload, &expected_resource_dict);
2502 }
2503
2504 #[test]
2505 fn v3_series_promotes_additional_and_origin_resource_tags_without_empty_host() {
2506 let context = Context::from_static_parts("series.additional_origin_resources", &["env:prod"])
2507 .with_origin_tags(tag_set(["dd.internal.resource:pod:pod-origin"]));
2508 let additional_tags = SharedTagSet::from(tag_set([
2509 "team:core",
2510 "device:switch1",
2511 "dd.internal.resource:container:container-a",
2512 ]));
2513 let metadata = MetricMetadata::default().with_hostname(Some(Arc::from("")));
2514 let metric = Metric::from_parts(context, MetricValues::gauge([1.0_f64]), metadata);
2515
2516 let payload = encode_v3_metrics_batch(&[metric], &additional_tags)
2517 .expect("V3 series should encode successfully")
2518 .payload;
2519
2520 assert_contains_bytes(&payload, b"env:prod");
2521 assert_contains_bytes(&payload, b"team:core");
2522 assert!(!contains_bytes(&payload, b"device:switch1"));
2523 assert!(!contains_bytes(&payload, b"dd.internal.resource:container:container-a"));
2524 assert!(!contains_bytes(&payload, b"dd.internal.resource:pod:pod-origin"));
2525
2526 let expected_resource_dict = [
2527 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',
2530 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',
2531 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',
2532 ];
2533 assert_contains_bytes(&payload, &expected_resource_dict);
2534 assert!(!contains_bytes(&payload, b"host"));
2535 }
2536
2537 #[test]
2538 fn v3_sketch_keeps_device_and_internal_resource_tags_as_tags() {
2539 let context = Context::from_static_parts(
2540 "sketch.resources",
2541 &["env:prod", "device:switch1", "dd.internal.resource:pod:pod-a"],
2542 );
2543 let metadata = MetricMetadata::default().with_hostname(Some(Arc::from("host-a")));
2544 let metric = Metric::from_parts(context, MetricValues::histogram([1.0_f64]), metadata);
2545
2546 let payload = encode_v3_metrics_batch(&[metric], &SharedTagSet::default())
2547 .expect("V3 sketch should encode successfully")
2548 .payload;
2549
2550 assert_contains_bytes(&payload, b"env:prod");
2551 assert_contains_bytes(&payload, b"device:switch1");
2552 assert_contains_bytes(&payload, b"dd.internal.resource:pod:pod-a");
2553
2554 let expected_resource_dict = [
2555 0x22, 0x0c, 0x04, b'h', b'o', b's', b't', 0x06, b'h', b'o', b's', b't', b'-', b'a',
2558 ];
2559 assert_contains_bytes(&payload, &expected_resource_dict);
2560 }
2561
2562 #[tokio::test]
2563 async fn validation_split_flush_assigns_batch_id_to_carried_metric() {
2564 let v2_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 1, None);
2565 let v2_series_builder = Some(
2566 v2::create_v2_request_builder(MetricsEndpoint::SeriesV2, &v2_endpoint_config)
2567 .await
2568 .expect("V2 request builder should be created"),
2569 );
2570 let v3_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2571 let metrics_builder = MetricsBuilder::default();
2572 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
2573 let serializer_telemetry = V3SerializerTelemetry::from_builder(&metrics_builder);
2574 let (events_tx, events_rx) = tokio::sync::mpsc::channel(1);
2575 let (payloads_tx, mut payloads_rx) = tokio::sync::mpsc::channel(8);
2576
2577 let request_builder_handle = tokio::spawn(run_request_builder(
2578 v2_series_builder,
2579 None,
2580 MetricsEncoderMode::Validation,
2581 MetricsEncoderMode::V2Only,
2582 V3RuntimeConfig {
2583 endpoint_config: v3_endpoint_config,
2584 payload_limits: V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 10_000),
2585 series_endpoint_uri: V3_SERIES_ENDPOINT_URI.to_string(),
2586 shadow_series_endpoint_uri: "/api/intake/metrics/v3beta/series".to_string(),
2587 series_shadow_config: SeriesShadowConfig::new(0.0),
2588 serializer_telemetry,
2589 },
2590 telemetry,
2591 events_rx,
2592 payloads_tx,
2593 Duration::from_millis(10),
2594 false,
2595 ));
2596
2597 let mut events = EventsBuffer::default();
2598 assert!(events
2599 .try_push(Event::Metric(Metric::counter("validation.split.one", 1.0)))
2600 .is_none());
2601 assert!(events
2602 .try_push(Event::Metric(Metric::counter("validation.split.two", 2.0)))
2603 .is_none());
2604 events_tx
2605 .send(events)
2606 .await
2607 .expect("events should be sent to request builder");
2608
2609 let mut flushed_requests = Vec::new();
2610 for _ in 0..4 {
2611 let payload = timeout(Duration::from_secs(1), payloads_rx.recv())
2612 .await
2613 .expect("payload should arrive before timeout")
2614 .expect("payload channel should remain open");
2615 let Payload::Http(http_payload) = payload else {
2616 panic!("expected HTTP payload");
2617 };
2618 let (_, request) = http_payload.into_parts();
2619 let batch_id = request
2620 .headers()
2621 .get("X-Metrics-Request-ID")
2622 .expect("validation batch ID header should be present")
2623 .to_str()
2624 .expect("validation batch ID should be valid header text")
2625 .to_string();
2626 flushed_requests.push((request.uri().to_string(), batch_id));
2627 }
2628
2629 assert_eq!("/api/v2/series", flushed_requests[0].0);
2630 assert_eq!(V3_SERIES_ENDPOINT_URI, flushed_requests[1].0);
2631 assert_eq!("/api/v2/series", flushed_requests[2].0);
2632 assert_eq!(V3_SERIES_ENDPOINT_URI, flushed_requests[3].0);
2633
2634 assert_eq!(flushed_requests[0].1, flushed_requests[1].1);
2635 assert_eq!(flushed_requests[2].1, flushed_requests[3].1);
2636 assert_ne!(flushed_requests[0].1, flushed_requests[2].1);
2637
2638 drop(events_tx);
2639 request_builder_handle
2640 .await
2641 .expect("request builder task should complete")
2642 .expect("request builder should stop cleanly");
2643 }
2644
2645 #[tokio::test]
2646 async fn authoritative_v3_flushes_previous_point_limit_batch() {
2647 let v3_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2648 let recorder = TestRecorder::default();
2649 let _local = metrics::set_default_local_recorder(&recorder);
2650 let metrics_builder = MetricsBuilder::default();
2651 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
2652 let serializer_telemetry = V3SerializerTelemetry::from_builder(&metrics_builder);
2653 let (events_tx, events_rx) = tokio::sync::mpsc::channel(1);
2654 let (payloads_tx, mut payloads_rx) = tokio::sync::mpsc::channel(8);
2655
2656 let request_builder_handle = tokio::spawn(run_request_builder(
2657 None,
2658 None,
2659 MetricsEncoderMode::V3Enabled,
2660 MetricsEncoderMode::V2Only,
2661 V3RuntimeConfig {
2662 endpoint_config: v3_endpoint_config,
2663 payload_limits: V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 3),
2664 series_endpoint_uri: V3_SERIES_ENDPOINT_URI.to_string(),
2665 shadow_series_endpoint_uri: "/api/intake/metrics/v3beta/series".to_string(),
2666 series_shadow_config: SeriesShadowConfig::new(0.0),
2667 serializer_telemetry,
2668 },
2669 telemetry,
2670 events_rx,
2671 payloads_tx,
2672 Duration::from_millis(250),
2673 false,
2674 ));
2675
2676 let mut events = EventsBuffer::default();
2677 assert!(events
2678 .try_push(Event::Metric(Metric::counter(
2679 "authoritative.v3.points.one",
2680 [(123, 1.0), (124, 2.0)]
2681 )))
2682 .is_none());
2683 assert!(events
2684 .try_push(Event::Metric(Metric::counter(
2685 "authoritative.v3.points.two",
2686 [(123, 3.0), (124, 4.0)]
2687 )))
2688 .is_none());
2689 events_tx
2690 .send(events)
2691 .await
2692 .expect("events should be sent to request builder");
2693
2694 let payload = timeout(Duration::from_secs(1), payloads_rx.recv())
2695 .await
2696 .expect("point-limit payload should arrive before timeout")
2697 .expect("payload channel should remain open");
2698 let Payload::Http(http_payload) = payload else {
2699 panic!("expected HTTP payload");
2700 };
2701 let (_, request) = http_payload.into_parts();
2702 assert_eq!(V3_SERIES_ENDPOINT_URI, request.uri());
2703 assert!(timeout(Duration::from_millis(50), payloads_rx.recv()).await.is_err());
2704
2705 let payload = timeout(Duration::from_secs(1), payloads_rx.recv())
2706 .await
2707 .expect("timeout payload should arrive before timeout")
2708 .expect("payload channel should remain open");
2709 let Payload::Http(http_payload) = payload else {
2710 panic!("expected HTTP payload");
2711 };
2712 let (_, request) = http_payload.into_parts();
2713 assert_eq!(V3_SERIES_ENDPOINT_URI, request.uri());
2714 assert_eq!(
2715 recorder.counter(("serializer.v3_payload_split_reason", &[("reason", "max_points")])),
2716 Some(1)
2717 );
2718
2719 drop(events_tx);
2720 request_builder_handle
2721 .await
2722 .expect("request builder task should complete")
2723 .expect("request builder should stop cleanly");
2724 }
2725
2726 #[tokio::test]
2727 async fn authoritative_v3_sketches_flush_previous_point_limit_batch() {
2728 let v3_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2729 let recorder = TestRecorder::default();
2730 let _local = metrics::set_default_local_recorder(&recorder);
2731 let metrics_builder = MetricsBuilder::default();
2732 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
2733 let serializer_telemetry = V3SerializerTelemetry::from_builder(&metrics_builder);
2734 let (events_tx, events_rx) = tokio::sync::mpsc::channel(1);
2735 let (payloads_tx, mut payloads_rx) = tokio::sync::mpsc::channel(8);
2736
2737 let request_builder_handle = tokio::spawn(run_request_builder(
2738 None,
2739 None,
2740 MetricsEncoderMode::V2Only,
2741 MetricsEncoderMode::V3Enabled,
2742 V3RuntimeConfig {
2743 endpoint_config: v3_endpoint_config,
2744 payload_limits: V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 3),
2745 series_endpoint_uri: V3_SERIES_ENDPOINT_URI.to_string(),
2746 shadow_series_endpoint_uri: "/api/intake/metrics/v3beta/series".to_string(),
2747 series_shadow_config: SeriesShadowConfig::new(0.0),
2748 serializer_telemetry,
2749 },
2750 telemetry,
2751 events_rx,
2752 payloads_tx,
2753 Duration::from_millis(250),
2754 false,
2755 ));
2756
2757 let mut events = EventsBuffer::default();
2758 assert!(events
2759 .try_push(Event::Metric(Metric::distribution(
2760 "authoritative.v3.sketch.points.one",
2761 [(123, 1.0), (124, 2.0)]
2762 )))
2763 .is_none());
2764 assert!(events
2765 .try_push(Event::Metric(Metric::distribution(
2766 "authoritative.v3.sketch.points.two",
2767 [(123, 3.0), (124, 4.0)]
2768 )))
2769 .is_none());
2770 events_tx
2771 .send(events)
2772 .await
2773 .expect("events should be sent to request builder");
2774
2775 for expected in ["point-limit", "timeout"] {
2776 let payload = timeout(Duration::from_secs(1), payloads_rx.recv())
2777 .await
2778 .unwrap_or_else(|_| panic!("{expected} sketches payload should arrive before timeout"))
2779 .expect("payload channel should remain open");
2780 let Payload::Http(http_payload) = payload else {
2781 panic!("expected HTTP payload");
2782 };
2783 let (_, request) = http_payload.into_parts();
2784 assert_eq!(V3_SKETCHES_ENDPOINT_URI, request.uri());
2785 }
2786 assert_eq!(
2787 recorder.counter(("serializer.v3_payload_split_reason", &[("reason", "max_points")])),
2788 Some(1)
2789 );
2790
2791 drop(events_tx);
2792 request_builder_handle
2793 .await
2794 .expect("request builder task should complete")
2795 .expect("request builder should stop cleanly");
2796 }
2797
2798 #[tokio::test]
2799 async fn authoritative_v3_does_not_flush_on_v2_boundary() {
2800 let v2_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 1, None);
2801 let v2_series_builder = Some(
2802 v2::create_v2_request_builder(MetricsEndpoint::SeriesV2, &v2_endpoint_config)
2803 .await
2804 .expect("V2 request builder should be created"),
2805 );
2806 let v3_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2807 let metrics_builder = MetricsBuilder::default();
2808 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
2809 let serializer_telemetry = V3SerializerTelemetry::from_builder(&metrics_builder);
2810 let (events_tx, events_rx) = tokio::sync::mpsc::channel(1);
2811 let (payloads_tx, mut payloads_rx) = tokio::sync::mpsc::channel(8);
2812
2813 let request_builder_handle = tokio::spawn(run_request_builder(
2814 v2_series_builder,
2815 None,
2816 MetricsEncoderMode::V3Enabled,
2817 MetricsEncoderMode::V2Only,
2818 V3RuntimeConfig {
2819 endpoint_config: v3_endpoint_config,
2820 payload_limits: V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 10_000),
2821 series_endpoint_uri: V3_SERIES_ENDPOINT_URI.to_string(),
2822 shadow_series_endpoint_uri: "/api/intake/metrics/v3beta/series".to_string(),
2823 series_shadow_config: SeriesShadowConfig::new(0.0),
2824 serializer_telemetry,
2825 },
2826 telemetry,
2827 events_rx,
2828 payloads_tx,
2829 Duration::from_millis(250),
2830 false,
2831 ));
2832
2833 let mut events = EventsBuffer::default();
2834 assert!(events
2835 .try_push(Event::Metric(Metric::counter("authoritative.v3.decouple.one", 1.0)))
2836 .is_none());
2837 assert!(events
2838 .try_push(Event::Metric(Metric::counter("authoritative.v3.decouple.two", 2.0)))
2839 .is_none());
2840 events_tx
2841 .send(events)
2842 .await
2843 .expect("events should be sent to request builder");
2844
2845 let payload = timeout(Duration::from_secs(1), payloads_rx.recv())
2846 .await
2847 .expect("V2 split payload should arrive before timeout")
2848 .expect("payload channel should remain open");
2849 let Payload::Http(http_payload) = payload else {
2850 panic!("expected HTTP payload");
2851 };
2852 let (_, request) = http_payload.into_parts();
2853 assert_eq!("/api/v2/series", request.uri());
2854 assert!(timeout(Duration::from_millis(50), payloads_rx.recv()).await.is_err());
2855
2856 let mut timeout_flush_uris = Vec::new();
2857 for _ in 0..2 {
2858 let payload = timeout(Duration::from_secs(1), payloads_rx.recv())
2859 .await
2860 .expect("timeout payload should arrive before timeout")
2861 .expect("payload channel should remain open");
2862 let Payload::Http(http_payload) = payload else {
2863 panic!("expected HTTP payload");
2864 };
2865 let (_, request) = http_payload.into_parts();
2866 timeout_flush_uris.push(request.uri().to_string());
2867 }
2868 assert_eq!(2, timeout_flush_uris.len());
2869 assert!(timeout_flush_uris.iter().any(|uri| uri == "/api/v2/series"));
2870 assert!(timeout_flush_uris.iter().any(|uri| uri == V3_SERIES_ENDPOINT_URI));
2871
2872 drop(events_tx);
2873 request_builder_handle
2874 .await
2875 .expect("request builder task should complete")
2876 .expect("request builder should stop cleanly");
2877 }
2878
2879 #[tokio::test]
2880 async fn shadow_sampled_series_flush_sends_v2_and_v3_beta_with_same_batch_id() {
2881 let v2_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2882 let v2_series_builder = Some(
2883 v2::create_v2_request_builder(MetricsEndpoint::SeriesV2, &v2_endpoint_config)
2884 .await
2885 .expect("V2 request builder should be created"),
2886 );
2887 let v3_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2888 let metrics_builder = MetricsBuilder::default();
2889 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
2890 let serializer_telemetry = V3SerializerTelemetry::from_builder(&metrics_builder);
2891 let (events_tx, events_rx) = tokio::sync::mpsc::channel(1);
2892 let (payloads_tx, mut payloads_rx) = tokio::sync::mpsc::channel(8);
2893
2894 let request_builder_handle = tokio::spawn(run_request_builder(
2895 v2_series_builder,
2896 None,
2897 MetricsEncoderMode::V2Only,
2898 MetricsEncoderMode::V2Only,
2899 V3RuntimeConfig {
2900 endpoint_config: v3_endpoint_config,
2901 payload_limits: V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 10_000),
2902 series_endpoint_uri: V3_SERIES_ENDPOINT_URI.to_string(),
2903 shadow_series_endpoint_uri: "/api/intake/metrics/v3beta/custom".to_string(),
2904 series_shadow_config: SeriesShadowConfig::new(1.0),
2905 serializer_telemetry,
2906 },
2907 telemetry,
2908 events_rx,
2909 payloads_tx,
2910 Duration::from_millis(10),
2911 false,
2912 ));
2913
2914 let mut events = EventsBuffer::default();
2915 assert!(events
2916 .try_push(Event::Metric(Metric::counter("shadow.sampled", 1.0)))
2917 .is_none());
2918 events_tx
2919 .send(events)
2920 .await
2921 .expect("events should be sent to request builder");
2922
2923 let mut flushed_requests = Vec::new();
2924 for _ in 0..2 {
2925 let payload = timeout(Duration::from_secs(1), payloads_rx.recv())
2926 .await
2927 .expect("payload should arrive before timeout")
2928 .expect("payload channel should remain open");
2929 let Payload::Http(http_payload) = payload else {
2930 panic!("expected HTTP payload");
2931 };
2932 let (metadata, request) = http_payload.into_parts();
2933 let payload_info = *metadata
2934 .get::<MetricsPayloadInfo>()
2935 .expect("metrics payload info should be present");
2936 let batch_id = request
2937 .headers()
2938 .get("X-Metrics-Request-ID")
2939 .expect("shadow batch ID header should be present")
2940 .to_str()
2941 .expect("shadow batch ID should be valid header text")
2942 .to_string();
2943 flushed_requests.push((request.uri().to_string(), payload_info, batch_id));
2944 }
2945
2946 assert_eq!("/api/v2/series", flushed_requests[0].0);
2947 assert_eq!(MetricsPayloadInfo::v2_shadow_series(), flushed_requests[0].1);
2948 assert_eq!("/api/intake/metrics/v3beta/custom", flushed_requests[1].0);
2949 assert_eq!(MetricsPayloadInfo::v3_shadow_series(), flushed_requests[1].1);
2950 assert_eq!(flushed_requests[0].2, flushed_requests[1].2);
2951
2952 drop(events_tx);
2953 request_builder_handle
2954 .await
2955 .expect("request builder task should complete")
2956 .expect("request builder should stop cleanly");
2957 }
2958
2959 #[tokio::test]
2960 async fn shadow_sample_rate_zero_sends_only_v2_without_validation_headers() {
2961 let v2_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2962 let v2_series_builder = Some(
2963 v2::create_v2_request_builder(MetricsEndpoint::SeriesV2, &v2_endpoint_config)
2964 .await
2965 .expect("V2 request builder should be created"),
2966 );
2967 let v3_endpoint_config = EndpointConfiguration::new(CompressionScheme::noop(), 10_000, None);
2968 let metrics_builder = MetricsBuilder::default();
2969 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
2970 let serializer_telemetry = V3SerializerTelemetry::from_builder(&metrics_builder);
2971 let (events_tx, events_rx) = tokio::sync::mpsc::channel(1);
2972 let (payloads_tx, mut payloads_rx) = tokio::sync::mpsc::channel(8);
2973
2974 let request_builder_handle = tokio::spawn(run_request_builder(
2975 v2_series_builder,
2976 None,
2977 MetricsEncoderMode::V2Only,
2978 MetricsEncoderMode::V2Only,
2979 V3RuntimeConfig {
2980 endpoint_config: v3_endpoint_config,
2981 payload_limits: V3PayloadLimits::new(usize::MAX, usize::MAX, 10_000, 10_000),
2982 series_endpoint_uri: V3_SERIES_ENDPOINT_URI.to_string(),
2983 shadow_series_endpoint_uri: "/api/intake/metrics/v3beta/series".to_string(),
2984 series_shadow_config: SeriesShadowConfig::new(0.0),
2985 serializer_telemetry,
2986 },
2987 telemetry,
2988 events_rx,
2989 payloads_tx,
2990 Duration::from_millis(10),
2991 false,
2992 ));
2993
2994 let mut events = EventsBuffer::default();
2995 assert!(events
2996 .try_push(Event::Metric(Metric::counter("shadow.disabled", 1.0)))
2997 .is_none());
2998 events_tx
2999 .send(events)
3000 .await
3001 .expect("events should be sent to request builder");
3002
3003 let payload = timeout(Duration::from_secs(1), payloads_rx.recv())
3004 .await
3005 .expect("payload should arrive before timeout")
3006 .expect("payload channel should remain open");
3007 let Payload::Http(http_payload) = payload else {
3008 panic!("expected HTTP payload");
3009 };
3010 let (_, request) = http_payload.into_parts();
3011 assert_eq!("/api/v2/series", request.uri());
3012 assert!(!request.headers().contains_key("X-Metrics-Request-ID"));
3013 assert!(timeout(Duration::from_millis(50), payloads_rx.recv()).await.is_err());
3014
3015 drop(events_tx);
3016 request_builder_handle
3017 .await
3018 .expect("request builder task should complete")
3019 .expect("request builder should stop cleanly");
3020 }
3021
3022 fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
3023 haystack.windows(needle.len()).any(|window| window == needle)
3024 }
3025
3026 fn assert_contains_bytes(haystack: &[u8], needle: &[u8]) {
3027 assert!(
3028 contains_bytes(haystack, needle),
3029 "expected payload to contain bytes {:?}, got {:?}",
3030 needle,
3031 haystack
3032 );
3033 }
3034
3035 fn tag_set<const N: usize>(tags: [&'static str; N]) -> TagSet {
3036 tags.into_iter().map(Tag::from_static).collect()
3037 }
3038}
3039
3040#[cfg(test)]
3041mod config_smoke {
3042 use datadog_agent_config_testing::config_registry::structs;
3043 use datadog_agent_config_testing::run_config_smoke_tests;
3044 use serde_json::json;
3045
3046 use super::DatadogMetricsConfiguration;
3047 use crate::config::{DatadogRemapper, KEY_ALIASES};
3048
3049 #[tokio::test]
3050 async fn smoke_test() {
3051 run_config_smoke_tests(
3052 structs::DATADOG_METRICS_CONFIGURATION,
3053 &[
3054 "serializer_experimental_use_v3_api.sketches.beta_route",
3055 "serializer_experimental_use_v3_api.sketches.shadow_sample_rate",
3056 "serializer_experimental_use_v3_api.sketches.shadow_sites",
3057 "serializer_experimental_use_v3_api.sketches.use_beta",
3058 ],
3059 json!({}),
3060 |cfg| {
3061 cfg.as_typed::<DatadogMetricsConfiguration>()
3062 .expect("DatadogMetricsConfiguration should deserialize")
3063 },
3064 KEY_ALIASES,
3065 DatadogRemapper::new,
3066 )
3067 .await
3068 }
3069}
3070
3071#[cfg(test)]
3072mod use_v2_api_series_default {
3073 use saluki_config::ConfigurationLoader;
3074 use serde_json::json;
3075
3076 use super::{v2, DatadogMetricsConfiguration};
3077 use crate::{common::datadog::clamp_payload_limits, config::KEY_ALIASES};
3078
3079 #[tokio::test]
3081 async fn defaults_to_true_when_absent() {
3082 let cfg = ConfigurationLoader::default()
3083 .with_key_aliases(KEY_ALIASES)
3084 .add_providers([figment::providers::Serialized::defaults(json!({}))])
3085 .into_generic()
3086 .await
3087 .expect("config should load");
3088 let parsed: DatadogMetricsConfiguration = cfg.as_typed().expect("should deserialize");
3089 assert!(parsed.use_v2_api_series);
3090 }
3091
3092 #[tokio::test]
3093 async fn deserializes_payload_limit_keys() {
3094 let cfg = ConfigurationLoader::default()
3095 .with_key_aliases(KEY_ALIASES)
3096 .add_providers([figment::providers::Serialized::defaults(json!({
3097 "serializer_max_payload_size": 4321,
3098 "serializer_max_uncompressed_payload_size": 8765,
3099 "serializer_max_series_payload_size": 1234,
3100 "serializer_max_series_uncompressed_payload_size": 5678,
3101 }))])
3102 .into_generic()
3103 .await
3104 .expect("config should load");
3105 let parsed: DatadogMetricsConfiguration = cfg.as_typed().expect("should deserialize");
3106
3107 assert_eq!(parsed.max_payload_size, 4321);
3108 assert_eq!(parsed.max_uncompressed_payload_size, 8765);
3109 assert_eq!(parsed.max_series_payload_size, 1234);
3110 assert_eq!(parsed.max_series_uncompressed_payload_size, 5678);
3111 }
3112
3113 #[tokio::test]
3114 async fn deserializes_max_series_points_per_payload() {
3115 let cfg = ConfigurationLoader::default()
3117 .with_key_aliases(KEY_ALIASES)
3118 .add_providers([figment::providers::Serialized::defaults(json!({}))])
3119 .into_generic()
3120 .await
3121 .expect("config should load");
3122 let parsed: DatadogMetricsConfiguration = cfg.as_typed().expect("should deserialize");
3123 assert_eq!(parsed.max_series_points_per_payload, 10_000);
3124 assert_eq!(parsed.v3_payload_limits().max_points_per_payload, 10_000);
3125
3126 let cfg = ConfigurationLoader::default()
3128 .with_key_aliases(KEY_ALIASES)
3129 .add_providers([figment::providers::Serialized::defaults(json!({
3130 "serializer_max_series_points_per_payload": 500,
3131 }))])
3132 .into_generic()
3133 .await
3134 .expect("config should load");
3135 let parsed: DatadogMetricsConfiguration = cfg.as_typed().expect("should deserialize");
3136 assert_eq!(parsed.max_series_points_per_payload, 500);
3137 assert_eq!(parsed.v3_payload_limits().max_points_per_payload, 500);
3138 }
3139
3140 #[test]
3141 fn clamps_series_payload_limit_keys_to_api_limits() {
3142 let (uncompressed_limit, compressed_limit) = clamp_payload_limits(
3143 v2::SERIES_V2_UNCOMPRESSED_SIZE_LIMIT + 1,
3144 v2::SERIES_V2_COMPRESSED_SIZE_LIMIT + 1,
3145 v2::SERIES_V2_UNCOMPRESSED_SIZE_LIMIT,
3146 v2::SERIES_V2_COMPRESSED_SIZE_LIMIT,
3147 );
3148 assert_eq!(uncompressed_limit, v2::SERIES_V2_UNCOMPRESSED_SIZE_LIMIT);
3149 assert_eq!(compressed_limit, v2::SERIES_V2_COMPRESSED_SIZE_LIMIT);
3150
3151 let (uncompressed_limit, compressed_limit) = clamp_payload_limits(
3152 5678,
3153 1234,
3154 v2::SERIES_V2_UNCOMPRESSED_SIZE_LIMIT,
3155 v2::SERIES_V2_COMPRESSED_SIZE_LIMIT,
3156 );
3157 assert_eq!(uncompressed_limit, 5678);
3158 assert_eq!(compressed_limit, 1234);
3159 }
3160}