Skip to main content

saluki_components/encoders/datadog/metrics/
mod.rs

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/// Encoding mode for a metrics endpoint.
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125enum MetricsEncoderMode {
126    /// Send V2 payloads only.
127    V2Only,
128    /// V3 is enabled for at least one endpoint; generate tagged V2 and V3 payloads so each endpoint
129    /// receives the protocol version configured for it.
130    V3Enabled,
131    /// Send both V2 and V3 payloads simultaneously with a shared batch ID for backend validation.
132    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/// Datadog Metrics encoder.
204///
205/// Generates Datadog metrics payloads for the Datadog platform.
206#[derive(Clone, Deserialize, Facet)]
207#[cfg_attr(test, derive(Debug, PartialEq, serde::Serialize))]
208pub struct DatadogMetricsConfiguration {
209    /// Maximum number of input metrics to encode into a single request payload.
210    ///
211    /// This applies both to the series and sketches endpoints.
212    ///
213    /// Defaults to 10,000.
214    #[serde(
215        rename = "serializer_max_metrics_per_payload",
216        default = "default_max_metrics_per_payload"
217    )]
218    max_metrics_per_payload: usize,
219
220    /// Maximum compressed size, in bytes, of generic payloads.
221    ///
222    /// This applies to V1 JSON series payloads and sketch payloads, matching the Datadog Agent's generic payload
223    /// builder. V2 series payloads use `serializer_max_series_payload_size` instead. The effective value is clamped to
224    /// the Agent's default intake-safe limit of 2,621,440 bytes, so larger configured values do not allow payloads that
225    /// intake may reject. If set to `0`, every non-empty compressed payload exceeds the limit and is dropped during
226    /// flush.
227    ///
228    /// Defaults to 2,621,440 bytes.
229    #[serde(rename = "serializer_max_payload_size", default = "default_max_payload_size")]
230    max_payload_size: usize,
231
232    /// Maximum uncompressed size, in bytes, of generic payloads.
233    ///
234    /// This applies to V1 JSON series payloads and sketch payloads, matching the Datadog Agent's generic payload
235    /// builder. V2 series payloads use `serializer_max_series_uncompressed_payload_size` instead. The effective value
236    /// is clamped to the Agent's default intake-safe limit of 4,194,304 bytes, so larger configured values do not allow
237    /// payloads that intake may reject. Values smaller than the minimum endpoint framing size prevent the request
238    /// builder from starting.
239    ///
240    /// Defaults to 4,194,304 bytes.
241    #[serde(
242        rename = "serializer_max_uncompressed_payload_size",
243        default = "default_max_uncompressed_payload_size"
244    )]
245    max_uncompressed_payload_size: usize,
246
247    /// Maximum compressed size, in bytes, of a V2 series payload.
248    ///
249    /// This applies only when `use_v2_api.series` is `true`. V1 series and sketches use `serializer_max_payload_size`
250    /// instead. The effective value is clamped to the V2 series API limit of 512,000 bytes, so larger configured values
251    /// do not allow payloads that intake would reject. High-throughput workloads may increase this up to that API limit
252    /// to reduce request count, at the cost of larger individual requests. If set to `0`, every non-empty compressed
253    /// payload exceeds the limit and is dropped during flush.
254    ///
255    /// Defaults to 512,000 bytes.
256    #[serde(
257        rename = "serializer_max_series_payload_size",
258        default = "default_max_series_payload_size"
259    )]
260    max_series_payload_size: usize,
261
262    /// Maximum uncompressed size, in bytes, of a V2 series payload.
263    ///
264    /// This applies only when `use_v2_api.series` is `true`. V1 series and sketches use
265    /// `serializer_max_uncompressed_payload_size` instead. The effective value is clamped to the V2 series API limit of
266    /// 5,242,880 bytes, so larger configured values do not allow payloads that intake would reject. This limit protects
267    /// the encoder before compression, so compressed payload size may still force a separate flush. Values smaller than
268    /// the minimum endpoint framing size prevent the request builder from starting.
269    ///
270    /// Defaults to 5,242,880 bytes.
271    #[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    /// Maximum number of data points, across all series, to encode into a single series request payload.
278    ///
279    /// This applies only to series metrics (counters, gauges, rates, sets) and not to sketch metrics (histograms,
280    /// distributions). A single metric series may contribute multiple data points when it carries more than one
281    /// timestamp/value pair. When encoding an input would cause the running data point total to exceed this limit, the
282    /// current payload is flushed first and the input is placed in the next payload.
283    ///
284    /// Defaults to 10,000.
285    #[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    /// Flush timeout for pending requests, in seconds.
292    ///
293    /// When the destination has written metrics to the in-flight request payload, but it has not yet reached the
294    /// payload size limits that would force the payload to be flushed, the destination will wait for a period of time
295    /// before flushing the in-flight request payload. This allows for the possibility of other events to be processed
296    /// and written into the request payload, thereby maximizing the payload size and reducing the number of requests
297    /// generated and sent overall.
298    ///
299    /// Defaults to 2 seconds.
300    #[serde(default = "default_flush_timeout_secs")]
301    flush_timeout_secs: u64,
302
303    /// Compression kind to use for the request payloads.
304    ///
305    /// Defaults to `zstd`.
306    #[serde(
307        rename = "serializer_compressor_kind",
308        default = "default_serializer_compressor_kind"
309    )]
310    compressor_kind: String,
311
312    /// Compressor level to use when the compressor kind is `zstd`.
313    ///
314    /// Defaults to 3.
315    #[serde(
316        rename = "serializer_zstd_compressor_level",
317        default = "default_zstd_compressor_level"
318    )]
319    zstd_compressor_level: i32,
320
321    /// Whether to use the V2 API for series metrics.
322    ///
323    /// When `true` (the default), series metrics are sent to the V2 protobuf endpoint (`/api/v2/series`). When
324    /// `false`, series metrics are sent to the legacy V1 JSON endpoint (`/api/v1/series`). Sketch metrics always use
325    /// the V2 endpoint (`/api/beta/sketches`) regardless of this setting.
326    ///
327    /// Defaults to `true`.
328    #[serde(default = "default_use_v2_api_series")]
329    use_v2_api_series: bool,
330
331    /// Whether to log metric payload contents before encoding.
332    ///
333    /// This logs decoded metric objects, not the encoded JSON/protobuf HTTP body.
334    ///
335    /// Defaults to `false`.
336    #[serde(default = "default_log_payloads")]
337    log_payloads: bool,
338
339    /// Additional tags to apply to all forwarded metrics.
340    #[serde(default, skip)]
341    #[facet(opaque)]
342    additional_tags: Option<SharedTagSet>,
343
344    /// V3 API configuration for per-endpoint V3 support.
345    ///
346    /// Configures which endpoints receive V3 payloads and whether validation mode is enabled.
347    #[serde(rename = "serializer_experimental_use_v3_api", default)]
348    v3_api: V3ApiConfig,
349
350    /// Agent-compatible V3 API configuration for series metrics.
351    #[serde(flatten)]
352    use_v3_api_series: UseV3ApiSeriesConfig,
353
354    /// ADP safety gate for authoritative V3 series metrics.
355    ///
356    /// Defaults to `false`.
357    #[serde(default, rename = "data_plane_metrics_v3_series_enabled")]
358    data_plane_metrics_v3_series_enabled: bool,
359
360    /// Enables routing all metrics to Observability Pipelines Worker.
361    #[serde(default, rename = "observability_pipelines_worker_metrics_enabled")]
362    observability_pipelines_worker_metrics_enabled: bool,
363
364    /// Endpoint of the Observability Pipelines Worker instance to route metrics to.
365    #[serde(default, rename = "observability_pipelines_worker_metrics_url")]
366    observability_pipelines_worker_metrics_url: String,
367
368    /// Enables V3 series metrics when routing to Observability Pipelines Worker.
369    ///
370    /// Defaults to `false`.
371    #[serde(default, rename = "observability_pipelines_worker_metrics_use_v3_api_series")]
372    observability_pipelines_worker_metrics_use_v3_api_series: bool,
373
374    /// Enables routing all metrics to Vector.
375    #[serde(default, rename = "vector_metrics_enabled")]
376    vector_metrics_enabled: bool,
377
378    /// Endpoint of the Vector instance to route metrics to.
379    #[serde(default, rename = "vector_metrics_url")]
380    vector_metrics_url: String,
381
382    /// Enables V3 series metrics when routing to Vector.
383    ///
384    /// Deprecated in favor of `observability_pipelines_worker.metrics.use_v3_api.series`.
385    ///
386    /// Defaults to `false`.
387    #[serde(default, rename = "vector_metrics_use_v3_api_series")]
388    vector_metrics_use_v3_api_series: bool,
389
390    /// Additional endpoints that metrics may be dual-shipped to.
391    #[serde(default)]
392    additional_endpoints: AdditionalEndpoints,
393}
394
395impl DatadogMetricsConfiguration {
396    /// Creates a new `DatadogMetricsConfiguration` from the given configuration.
397    pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
398        Ok(config.as_typed()?)
399    }
400
401    /// Sets additional tags to be applied uniformly to all metrics forwarded by this destination.
402    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        // Derive the encoding mode for each metric type from the configuration.
458        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            // We always give ourselves a minimum flush timeout of 10ms to allow for some very minimal amount of
533            // batching, while still practically flushing things almost immediately.
534            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        // TODO: How do we properly represent the requests we can generate that may be sitting around in-flight?
564        //
565        // Theoretically, we'll end up being limited by the size of the downstream forwarder's interconnect, and however
566        // many payloads it will buffer internally... so realistically the firm limit boils down to the forwarder itself
567        // but we'll have a hard time in the forwarder knowing the maximum size of any given payload being sent in, which
568        // then makes it hard to calculate a proper firm bound even though we know the rest of the values required to
569        // calculate the firm bound.
570        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            // Capture the size of the "split re-encode" buffers in the request builders, which is where we keep owned
579            // versions of metrics that we encode in case we need to actually re-encode them during a split operation.
580            .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        // Spawn our request builder task.
622        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 the events sender, which signals the request builder task to stop.
666        drop(events_tx);
667
668        // Continue draining the payloads receiver until it is closed.
669        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        // Request build task should now be stopped.
676        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
688/// Logs the decoded contents of a metric prior to encoding.
689///
690/// This logs the metric object itself, not the encoded JSON/protobuf HTTP body.
691fn 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                    // A series metric whose points are all non-finite would encode to a series with no points, which
756                    // intake rejects as an empty value set. Drop it whole rather than emit an empty series.
757                    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                    // Figure out which endpoint the metric belongs to, and grab the relevant V2 builder/V3 storage.
764                    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                    // Store a copy of the metric in `maybe_v3_metrics` if it's present.
794                    //
795                    // We have to do this before encoding because `RequestBuilder::encode` consumes the metric. This also means we'll
796                    // need to _remove_ the metric if encoding fails.
797                    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                    // Attempt encoding the metric for V2 if configured.
805                    //
806                    // If the metric couldn't be encoded (too big, some other issue), the call returns `false` which is
807                    // our signal to remove the metric from `maybe_v3_metrics` (if we added it), since we know now that
808                    // the metric wasn't encoded for V2 and we want our V2/V3 payload batches to be consistent in
809                    // validation mode.
810                    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                    // If V2 flushes while this batch is not shadowed, the current metric may start the next V2 batch.
821                    // Keep a clone so we can add it to the next V3 shadow batch if that next batch samples in.
822                    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                    // Validation and shadow payloads must keep V2/V3 batch boundaries aligned. Authoritative V3 can
848                    // batch independently, so it flushes on V3-specific limits instead of following V2 flushes.
849                    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                                // V2 flushes the previous batch without the current metric. Pop it
888                                // from V3 before flushing so both batches cover the same set of metrics.
889                                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                        // Authoritative V3 may flush the previous point-limit batch while keeping the current metric
921                        // buffered for the next V3 payload.
922                        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                        // A validation flush completes the current V2/V3 pair. If V2 carried the current metric into
931                        // the next batch, keep the V3 copy and assign that next pair a fresh validation ID.
932                        *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                        // A shadow flush completes the current sampled pair. The next V2 batch gets a new shadow sample
944                        // decision, so only carry the split metric into V3 if that next batch samples in.
945                        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                        // This V2 batch was not shadowed, but the flushed metric may have started the next V2 batch.
966                        // Re-sample for that next batch and seed the V3 buffer only if the new decision samples in.
967                        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 we're not already pending a flush, we'll start the countdown.
987                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                // Flush any pending series metrics.
998                // Timeout flushes complete the current batch, so reuse the existing shadow decision instead of sampling
999                // a new one for metrics that are already buffered.
1000                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                        // Shadow series use the V3 beta route; normal V3 series use the configured authoritative route.
1026                        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                        // Validation/shadow V3 must not outlive a failed V2 baseline flush.
1045                        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                // Flush any pending sketch metrics.
1059                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            // Event buffers channel has been closed, and we have no pending flushing, so we're all done.
1098            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    // Encode the metric. If we get it back, that means the current request is full, and we need to
1129    // flush it before we can try to encode the metric again... so we'll hold on to it in that case
1130    // before flushing and trying to encode it again.
1131    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    // Now try to encode the metric again. If it fails again, we'll just log it because it shouldn't
1144    // be possible to fail at this point, otherwise we would have already caught that the first
1145    // time.
1146    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            // TODO: Increment a counter here that metrics were dropped due to a flush failure.
1186            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            // The encoded request is too large and this range cannot be split any further.
1331            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        // Retry this oversized range as two smaller ranges, preserving the original metric order.
1348        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            // The Agent drops zero-point V3 metrics before writing them.
1403            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            // This metric exceeds the point limit by itself, so it cannot fit in any V3 payload request.
1415            // Close the current range before dropping this oversized metric.
1416            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            // This metric fits by itself, but not together with the current range.
1437            // Adding this metric would overflow the current range, so start a new range at this metric.
1438            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
1462/// Converts a `Uuid` to a `HeaderValue`.
1463fn 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
1468/// Converts a `usize` to a `HeaderValue`.
1469fn 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    // Attach the validation batch UUID and sequence headers if present.
1480    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
1502// Encodes a batch of metrics to V3 columnar format.
1503fn 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
1518/// Writes a single metric to the V3 writer.
1519fn 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    // Tags - chain instrumented + additional + origin tags
1534    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    // Resources - extract host and, for series, promoted resource tags.
1547    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    // Origin metadata
1578    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    // Points based on metric type
1600    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                // Scale by interval as done in V2
1614                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            // Set values are already converted to count in the iterator
1624            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                // Convert histogram to DDSketch
1654                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
1691/// Creates a V3 HTTP request from encoded payload data.
1692async fn create_v3_request(
1693    endpoint_uri: &str, mut encoded: V3EncodedMetrics, compression_scheme: CompressionScheme,
1694) -> Result<V3EncodedRequest, GenericError> {
1695    // Keep the wire payload as one continuous compressed stream. Per-column compressed sizes are measured
1696    // independently for telemetry.
1697    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        // Values are 1, 2, 3, 4, 3 (rate scaled by 10s), and 1 (set cardinality), encoded as sint64.
2010        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, // field 25, length-delimited.
2427            0x0c, // field payload length: varint string length + string bytes.
2428            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, // field 26, length-delimited.
2440            0x02, // packed field payload length.
2441            0x02, 0x00, // sparse unit refs for metrics 1 and 3 only: refs [1, 1] -> deltas [1, 0].
2442        ];
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, // field 4, length-delimited.
2496            0x25, // field payload length.
2497            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, // field 4, length-delimited.
2528            0x34, // field payload length.
2529            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, // field 4, length-delimited.
2556            0x0c, // field payload length.
2557            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    /// `use_v2_api_series` defaults to `true`, preserving V2 protobuf behavior when the flag is absent.
3080    #[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        // Default should be 10,000.
3116        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        // Explicit value should round-trip.
3127        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}