fix(dogstatsd): preserve cardinality for aggregated set metrics - #980
fix(dogstatsd): preserve cardinality for aggregated set metrics#980jaideeppyne wants to merge 2 commits into
Conversation
When client-side aggregation is enabled, flush_aggregated_metrics() re-emits each metric with its cardinality. For count and gauge the flushed object is the original metric, so cardinality survives, but SetMetric.get_data() rebuilt fresh MetricAggregators without passing cardinality, so it defaulted to None. As a result the |card:<value> tag was silently stripped from set metrics under aggregation, while count and gauge kept it. This completes DataDog#929 (which added cardinality to the flush loops but did not cover the SetMetric.get_data() path). Pass cardinality=self.cardinality when rebuilding the per-value aggregators. Adds a regression test asserting gauge, count and set all carry their cardinality tag when aggregation is enabled; it fails without this change.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b29c828199
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return [ | ||
| MetricAggregator(self.name, self.tags, self.rate, MetricType.SET, value) | ||
| MetricAggregator( | ||
| self.name, self.tags, self.rate, MetricType.SET, value, cardinality=self.cardinality |
There was a problem hiding this comment.
Separate set values by cardinality before re-emitting
When two set() calls use the same metric name and tags but different per-call cardinalities during one aggregation window, Aggregator.get_context() places them in the same SetMetric; this line then assigns the first call's self.cardinality to every collected value. For example, values submitted with cardinality="low" and then cardinality="high" are both emitted as card:low, incorrectly controlling origin-tag enrichment for the second value. Include cardinality in the aggregation context or retain it per value rather than applying one cardinality to the entire set.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch, thanks. Confirmed: Aggregator.get_context() keyed contexts on name+tags only, so two set() calls with the same name+tags but different per-call cardinalities in one aggregation window landed in the same SetMetric, and every collected value was re-emitted with whichever cardinality created the context first (low then high → both low).
Fixed in 934e484 by including the resolved cardinality in the aggregation context key, so distinct cardinalities form distinct metric objects and each flushes with its own cardinality. The key is unchanged when no cardinality is set, so existing contexts are preserved. Since the fix lives in the shared add_metric path, it also corrects the same latent collision for count/gauge. (The sampled path — histogram/distribution/timing — has the analogous keying but is out of scope for this PR.)
Added regression tests in test_aggregator.py: same name+tags with low then high now yields two SetMetrics and both cardinalities appear on the flushed lines (both fail without the fix, pass with it).
|
Thanks for the review. This is a pre-existing property of client-side aggregation rather than something this change introduces, so I've kept the fix scoped to the reported bug. The aggregation context is keyed on name + tags only ( Separating aggregated metrics by cardinality would mean adding cardinality to the aggregation context key in |
Aggregator.get_context() keyed contexts only on name+tags, so two submissions with the same name and tags but different per-call cardinalities within one aggregation window collided into a single metric object. On flush every collected value was re-emitted with the cardinality of whichever call created the context first (e.g. a set() with cardinality='low' followed by cardinality='high' emitted both under 'low'). Include the resolved cardinality in the aggregation context key so distinct cardinalities form distinct metric objects and each flushes with its own cardinality. The key is left unchanged when no cardinality is set, preserving existing contexts. This applies to the shared add_metric path (count, gauge and set). Add regression tests covering distinct values and the same value under two cardinalities. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
When client-side aggregation is enabled,
setmetrics silently drop theircardinalitytag on the wire, whilecountandgaugekeep it.Why
flush_aggregated_metrics()re-emits each aggregated metric withcardinality=m.cardinality. For count and gauge the flushed object is the original metric, so its cardinality survives. ButSetMetric.get_data()rebuilds freshMetricAggregators per value without passingcardinality, so it defaults toNoneand the|card:<value>tag is stripped:This completes #929, which added
cardinality=m.cardinalityto the flush loops inbase.py(intending cardinality to flow through all aggregated metrics) but did not cover theSetMetric.get_data()path — sosetis the one metric type where the feature is broken.How
Pass
cardinality=self.cardinalitywhen rebuilding the per-value aggregators:After the fix the set packet is
s:v|s|card:high.Tests
Added
test_aggregated_metrics_with_cardinality_when_aggregation_enabled(mirrors the existing sampled-metrics cardinality test): sends a gauge, count and set withcardinality="high"under aggregation and asserts all three packets carry|card:high. It fails onmaster(the set packet isset:value|s) and passes with the fix. The fulltests/unit/dogstatsd/test_statsd.pysuite stays green (126 passed, 1 skipped).Disclosure: this change was prepared with AI assistance and reviewed/verified by me before submission.