Skip to content

Commit 909a5e2

Browse files
committed
feat(metrics): make authoritative v3 batching point-aware
1 parent 61c31fa commit 909a5e2

2 files changed

Lines changed: 83 additions & 20 deletions

File tree

lib/saluki-components/src/encoders/datadog/metrics/mod.rs

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,15 @@ impl DatadogMetricsConfiguration {
403403
self.additional_tags = Some(additional_tags);
404404
self
405405
}
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+
}
406415
}
407416

408417
#[async_trait]
@@ -432,12 +441,7 @@ impl EncoderBuilder for DatadogMetricsConfiguration {
432441
V3_SERIES_ENDPOINT_URI.to_string()
433442
};
434443
let shadow_series_endpoint_uri = self.v3_api.series.beta_route.clone();
435-
let payload_limits = V3PayloadLimits::new(
436-
self.max_series_payload_size,
437-
self.max_series_uncompressed_payload_size,
438-
self.max_metrics_per_payload,
439-
self.max_series_points_per_payload,
440-
);
444+
let payload_limits = self.v3_payload_limits();
441445

442446
let v2_endpoint_config = EndpointConfiguration::new(
443447
v2_compression_scheme,
@@ -710,6 +714,8 @@ async fn run_request_builder(
710714
let mut v3_series_metrics =
711715
(series_mode.needs_v3() || v3_runtime_config.series_shadow_config.is_enabled()).then(Vec::<Metric>::new);
712716
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;
713719

714720
let mut series_batch_id = None;
715721
let mut sketches_batch_id = None;
@@ -748,20 +754,23 @@ async fn run_request_builder(
748754

749755
// Figure out which endpoint the metric belongs to, and grab the relevant V2 builder/V3 storage.
750756
let endpoint = MetricsEndpoint::from_metric(&metric);
751-
let (endpoint_mode, maybe_v2_builder, maybe_v3_metrics, batch_id) = match endpoint {
757+
let (endpoint_mode, maybe_v2_builder, maybe_v3_metrics, v3_points, batch_id) = match endpoint {
752758
MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2 => (
753759
series_mode,
754760
&mut v2_series_builder,
755761
&mut v3_series_metrics,
762+
&mut v3_series_points,
756763
&mut series_batch_id,
757764
),
758765
MetricsEndpoint::Sketches => (
759766
sketches_mode,
760767
&mut v2_sketch_builder,
761768
&mut v3_sketch_metrics,
769+
&mut v3_sketch_points,
762770
&mut sketches_batch_id,
763771
),
764772
};
773+
let metric_point_count = metric.values().len();
765774
let is_series = matches!(endpoint, MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2);
766775
let series_shadow_active = is_series
767776
&& matches!(endpoint_mode, MetricsEncoderMode::V2Only)
@@ -780,6 +789,7 @@ async fn run_request_builder(
780789
if should_buffer_v3 {
781790
if let Some(metrics) = maybe_v3_metrics {
782791
metrics.push(metric.clone());
792+
*v3_points += metric_point_count;
783793
}
784794
}
785795

@@ -810,9 +820,13 @@ async fn run_request_builder(
810820
let result =
811821
encode_v2_metrics(builder, metric, &telemetry, &mut payloads_tx, active_batch_id, v2_payload_info).await?;
812822
v2_encoded = result.encoded();
813-
if should_buffer_v3 && !result.encoded() {
823+
if should_buffer_v3
824+
&& !result.encoded()
825+
&& !matches!(endpoint_mode, MetricsEncoderMode::V3Enabled)
826+
{
814827
if let Some(metrics) = maybe_v3_metrics {
815828
let _ = metrics.pop();
829+
*v3_points = v3_points.saturating_sub(metric_point_count);
816830
}
817831
}
818832

@@ -821,8 +835,8 @@ async fn run_request_builder(
821835
false
822836
};
823837

824-
// If we flushed via V2, or we've hit our max metrics per payload limit in pure V3 mode, we need to flush our V3 metrics
825-
// as well.
838+
// Validation and shadow payloads must keep V2/V3 batch boundaries aligned. Authoritative V3 can
839+
// batch independently, so it flushes on V3-specific limits instead of following V2 flushes.
826840
let v3_payload_info = match endpoint {
827841
MetricsEndpoint::SeriesV1 | MetricsEndpoint::SeriesV2 => {
828842
if series_shadow_active {
@@ -838,15 +852,33 @@ async fn run_request_builder(
838852
let should_flush_v3 = match endpoint_mode {
839853
MetricsEncoderMode::V2Only => series_shadow_active && v2_flushed,
840854
MetricsEncoderMode::V3Enabled => {
841-
v2_flushed || v3_flush_context.payload_limits.should_flush_metric_count_limit(v3_metrics)
855+
if v3_flush_context.payload_limits.point_count_exceeds_limit(*v3_points)
856+
&& v3_metrics.len() > 1
857+
{
858+
v3_flush_context
859+
.serializer_telemetry
860+
.record_split_reason(V3PayloadSplitReason::MaxPoints);
861+
split_metric = v3_metrics.pop();
862+
*v3_points = v3_points.saturating_sub(metric_point_count);
863+
true
864+
} else {
865+
v3_flush_context.payload_limits.should_flush_point_count_limit(*v3_points)
866+
|| v3_flush_context
867+
.payload_limits
868+
.should_flush_metric_count_limit(v3_metrics)
869+
}
842870
}
843871
MetricsEncoderMode::Validation => v2_flushed,
844872
};
845873
if should_flush_v3 {
846-
// V2 flushes the previous batch without the current metric (the metric
847-
// that triggered the flush is re-encoded into the next V2 batch). Pop it
848-
// from V3 before flushing so both batches cover the same set of metrics.
849-
split_metric = if v2_flushed { v3_metrics.pop() } else { None };
874+
if !matches!(endpoint_mode, MetricsEncoderMode::V3Enabled) {
875+
// V2 flushes the previous batch without the current metric. Pop it
876+
// from V3 before flushing so both batches cover the same set of metrics.
877+
split_metric = if v2_flushed { v3_metrics.pop() } else { None };
878+
if split_metric.is_some() {
879+
*v3_points = v3_points.saturating_sub(metric_point_count);
880+
}
881+
}
850882
let flush_context = if series_shadow_active {
851883
v3_shadow_flush_context
852884
} else {
@@ -861,6 +893,7 @@ async fn run_request_builder(
861893
v3_payload_info,
862894
)
863895
.await?;
896+
*v3_points = 0;
864897
true
865898
} else {
866899
false
@@ -869,12 +902,24 @@ async fn run_request_builder(
869902
false
870903
};
871904

872-
// A validation flush completes the current V2/V3 pair. If V2 carried the current metric into the
873-
// next batch, keep the V3 copy and assign that next pair a fresh validation ID.
874-
if endpoint_mode.needs_batch_id() && (v2_flushed || v3_flushed) {
905+
if matches!(endpoint_mode, MetricsEncoderMode::V3Enabled) {
906+
// Authoritative V3 may flush the previous point-limit batch while keeping the current metric
907+
// buffered for the next V3 payload.
908+
if let Some(m) = split_metric.take() {
909+
let point_count = m.values().len();
910+
if let Some(metrics) = maybe_v3_metrics {
911+
metrics.push(m);
912+
*v3_points += point_count;
913+
}
914+
}
915+
} else if endpoint_mode.needs_batch_id() && (v2_flushed || v3_flushed) {
916+
// A validation flush completes the current V2/V3 pair. If V2 carried the current metric into
917+
// the next batch, keep the V3 copy and assign that next pair a fresh validation ID.
875918
*batch_id = if let Some(m) = split_metric.take() {
919+
let point_count = m.values().len();
876920
if let Some(metrics) = maybe_v3_metrics {
877921
metrics.push(m);
922+
*v3_points += point_count;
878923
}
879924
Some(Uuid::now_v7())
880925
} else {
@@ -886,8 +931,10 @@ async fn run_request_builder(
886931
series_shadow_state.reset();
887932
*batch_id = if let Some(m) = split_metric.take() {
888933
if series_shadow_state.ensure_decision(series_shadow_config) {
934+
let point_count = m.values().len();
889935
if let Some(metrics) = maybe_v3_metrics {
890936
metrics.push(m);
937+
*v3_points += point_count;
891938
}
892939
Some(Uuid::now_v7())
893940
} else {
@@ -908,8 +955,10 @@ async fn run_request_builder(
908955
if v2_encoded {
909956
if let Some(m) = metric_for_next_shadow_batch {
910957
if series_shadow_state.ensure_decision(series_shadow_config) {
958+
let point_count = m.values().len();
911959
if let Some(metrics) = maybe_v3_metrics {
912960
metrics.push(m);
961+
*v3_points += point_count;
913962
}
914963
*batch_id = Some(Uuid::now_v7());
915964
}
@@ -958,7 +1007,7 @@ async fn run_request_builder(
9581007
tag_series.then(MetricsPayloadInfo::v3_series)
9591008
};
9601009
if let Some(metrics) = &mut v3_series_metrics {
961-
if v2_series_flush_succeeded {
1010+
if v2_series_flush_succeeded || matches!(series_mode, MetricsEncoderMode::V3Enabled) {
9621011
// Shadow series use the V3 beta route; normal V3 series use the configured authoritative route.
9631012
let flush_context = if series_shadow_active {
9641013
v3_shadow_flush_context
@@ -976,10 +1025,12 @@ async fn run_request_builder(
9761025
{
9771026
error!(error = %e, "Failed to flush V3 series metrics: {}", e);
9781027
}
1028+
v3_series_points = 0;
9791029
} else {
9801030
// Validation/shadow V3 must not outlive a failed V2 baseline flush.
9811031
warn!("Failed to flush V2 series metrics, skipping V3 series flush.");
9821032
metrics.clear();
1033+
v3_series_points = 0;
9831034
}
9841035
}
9851036
if series_mode.needs_batch_id() {
@@ -1003,7 +1054,7 @@ async fn run_request_builder(
10031054

10041055
let v3_sketches_payload_info = tag_sketches.then(MetricsPayloadInfo::v3_sketches);
10051056
if let Some(metrics) = &mut v3_sketch_metrics {
1006-
if v2_sketches_flush_succeeded {
1057+
if v2_sketches_flush_succeeded || matches!(sketches_mode, MetricsEncoderMode::V3Enabled) {
10071058
if let Err(e) = encode_and_flush_v3_sketch_metrics(
10081059
v3_flush_context,
10091060
metrics,
@@ -1015,9 +1066,11 @@ async fn run_request_builder(
10151066
{
10161067
error!(error = %e, "Failed to flush V3 sketch metrics: {}", e);
10171068
}
1069+
v3_sketch_points = 0;
10181070
} else {
10191071
warn!("Failed to flush V2 sketch metrics, skipping V3 sketch flush.");
10201072
metrics.clear();
1073+
v3_sketch_points = 0;
10211074
}
10221075
}
10231076
if sketches_mode.needs_batch_id() {
@@ -2738,6 +2791,7 @@ mod use_v2_api_series_default {
27382791
.expect("config should load");
27392792
let parsed: DatadogMetricsConfiguration = cfg.as_typed().expect("should deserialize");
27402793
assert_eq!(parsed.max_series_points_per_payload, 10_000);
2794+
assert_eq!(parsed.v3_payload_limits().max_points_per_payload, 10_000);
27412795

27422796
// Explicit value should round-trip.
27432797
let cfg = ConfigurationLoader::default()
@@ -2750,6 +2804,7 @@ mod use_v2_api_series_default {
27502804
.expect("config should load");
27512805
let parsed: DatadogMetricsConfiguration = cfg.as_typed().expect("should deserialize");
27522806
assert_eq!(parsed.max_series_points_per_payload, 500);
2807+
assert_eq!(parsed.v3_payload_limits().max_points_per_payload, 500);
27532808
}
27542809

27552810
#[test]

lib/saluki-components/src/encoders/datadog/metrics/v3/payload.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ impl V3PayloadLimits {
3737
pub(crate) fn should_flush_metric_count_limit(self, metrics: &[Metric]) -> bool {
3838
metrics.len() >= self.max_metrics_per_payload
3939
}
40+
41+
pub(crate) fn should_flush_point_count_limit(self, data_point_count: usize) -> bool {
42+
data_point_count >= self.max_points_per_payload
43+
}
44+
45+
pub(crate) fn point_count_exceeds_limit(self, data_point_count: usize) -> bool {
46+
data_point_count > self.max_points_per_payload
47+
}
4048
}
4149

4250
/// Encoded V3 request with measured payload sizes.

0 commit comments

Comments
 (0)