Report stats span collapses over telemetry (client-side stats) - #12070
Report stats span collapses over telemetry (client-side stats)#12070dougqh wants to merge 7 commits into
Conversation
This comment has been minimized.
This comment has been minimized.
🟡 Java Benchmark SLOs — Performance SLO warning (near threshold)
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
|
@codex review |
💡 Codex ReviewWhen AGENTS.md reference: AGENTS.md:L37-L44 When When additional tags are enabled and If a producer reads the old peer-tag schema and is then paused for more than one reporting interval—for example during a long scheduling or GC pause—this reset discards ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
c1916aa to
302b882
Compare
There was a problem hiding this comment.
More details
The new collapse counters preserve per-reason accumulation and delta-drain behavior under repeated updates, including non-positive counts; the guarded collector path also leaves pending deltas intact when telemetry capacity is full. No diff-only behavioral regression was identified. Full Gradle tests could not start because this sandbox lacks the required Java 25 toolchain.
📊 Validated against 5 scenarios · Open Bits AI session
🤖 Datadog Autotest · Commit 545e3f0 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 545e3f03c9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Thanks @codex. Reconciling against the current branch — this review was posted against
Items 1–3 are resolved; item 4 is a documented accepted limitation. |
|
To use Codex here, create a Codex account and connect to github. |
The client-side trace-stats engine already emits a statsd metric (datadog.tracer.stats.collapsed_spans) whenever spans collapse under a cardinality/length limit or a whole-key table-drop. That signal is invisible when no dogstatsd sink is configured, since the aggregator's HealthMetrics is NO_OP in that case. Add a parallel telemetry counter, stats.collapsed_spans (namespace tracers), tagged by collapse reason (collapsed:<field>, collapsed:peer_tags, collapsed:additional_metric_tags, oversized:additional_metric_tags, collapsed:whole_key). It is fed directly at each reset/drop site alongside the existing HealthMetrics call, so it is independent of the statsd sink, and drained by CoreMetricCollector like the baggage counters. The reason set is bounded by construction, so the dynamic per-reason map cannot itself grow unboundedly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ookup The collapsed:whole_key counter is incremented once per dropped span on the aggregator thread (Aggregator, findOrInsert == null branch), unlike every other collapse reason which is batched once per reporting cycle. Under a cardinality explosion the aggregate table pins at cap and every arriving span is dropped, turning that path hot on an already-bottlenecked thread -- yet it went through a ConcurrentHashMap.computeIfAbsent lookup on StatsMetrics per span. Pre-create the whole_key TaggedCounter at construction (still registered in the reason map so the telemetry drain picks it up unchanged) and add a dedicated onWholeKeyCollapse() that increments the cached reference directly. The hot path no longer touches the map; the batched reasons still route through onCollapsedSpans as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getValueAndReset() resets a counter's delta baseline, so calling it before the offer() succeeds means a full metricsQueue silently drops that delta forever -- exactly when it matters most (telemetry backing up because the agent is unreachable). Check remainingCapacity() before reading any counter and stop the drain early instead; the untouched counters are collected on the next cycle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
545e3f0 to
6431932
Compare
bric3
left a comment
There was a problem hiding this comment.
LGTM but I have a concern on the method names. The code explicitly mentions it's mirroring on the same signal, yet the method names are different.
Moreover the generated doc is still awkward, in particular in StatsMetrics.java.
| healthMetrics.onTagCardinalityBlocked(COLLAPSED_STATSD_TAG, totalCollapsed); | ||
| StatsMetrics.getInstance().onCollapsedSpans(COLLAPSED_STATSD_TAG[0], totalCollapsed); |
There was a problem hiding this comment.
question: Why method are named differently, this raises question when reading the code, yet, it seems the sgtats are emitted on the same "conditions".
I suggest aligning method names, unless there's a valid reason not to.
There was a problem hiding this comment.
Fair point — juxtaposed like this they do read a little oddly. The names differ on purpose: each mirrors the vocabulary of the sink it feeds, and the two sinks are named independently.
HealthMetrics.onTagCardinalityBlocked(...)is one of HealthMetrics'onXevent callbacks; that subsystem's vocabulary is "blocked" (the offending tag value is blocked/collapsed to the sentinel), and its statsd tags arecollapsed:/oversized:.StatsMetrics.onCollapsedSpans(...)mirrors the telemetry metric it increments,stats.collapsed_spans— the collapse reason rides in the tag rather than the method name.
So both fire under the same condition because they report the same underlying event to two sinks, but renaming either to match the other would make that method name diverge from the name of the thing it actually reports. I'd rather keep each aligned to its own metric/event name.
Open to a different split if you feel strongly — but I don't think there's a single verb that reads naturally against both stats.collapsed_spans and the health *_blocked family at once.
…istry The span-metric registry holds one entry per instrumentation name and can exceed the 1024-entry telemetry queue on its own. Draining it before the small, fixed set of client-side stats collapse counters could fill the queue and starve those counters indefinitely. Collect them first so they are always emitted; the pre-read capacity guard still protects their delta baseline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
StatsMetrics is a process-wide singleton; these tests left per-reason deltas behind, leaking stats.collapsed_spans metrics into CoreMetricCollectorTest when they shared a Gradle worker and making its exact span-metric count assertion order-dependent. Reset every counter in @AfterEach. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What Does This Do?
Report client-side trace-stats span collapses over telemetry as a new
stats.collapsed_spanscounter (namespacetracers), mirroring the existing statsd metricdatadog.tracer.stats.collapsed_spans.Motivation
Required by the cardinality limits & span-derived tags RFCs
The stats engine already counts every span collapse — cardinality-limit collapses, length ("oversized") collapses, and whole-key table drops — and emits them to statsd. But that path is invisible when no dogstatsd sink is configured: the aggregator's
HealthMetricsisHealthMetrics.NO_OPin that case (CoreTracer.java:735), so the counts are simply dropped. Telemetry is always wired, so surfacing the same signal there makes cardinality-collapse visibility independent of statsd.Additional Notes
Implementation Details
StatsMetricsholder (ininternal-api, alongsideBaggageMetrics): a per-reasonAtomicLongcounter in aConcurrentHashMap, drained byCoreMetricCollectoras acountmetric — same pattern as the baggage counters.CoreHandlers,PeerTagSchema,AdditionalTagsSchema, and the whole-key drop inAggregator), right next to the existingHealthMetricscall — deliberately not hooked insideTracerHealthMetrics, so it fires even when statsd is off.collapsed:<field>,collapsed:peer_tags,collapsed:additional_metric_tags,oversized:additional_metric_tags,collapsed:whole_key.Cardinality safety
The per-reason map is dynamic, but the reason set is bounded by construction (9 core fields + peer_tags + the two additional-tags reasons + whole_key) — the cardinality limits this metric reports on are the very thing that bound it. So the map itself cannot blow up.
Testing
StatsMetricsTest(JUnit 5): accumulation, drain-delta semantics, per-reason isolation, non-positive no-op.AdditionalTagsSchemaTest: a new case asserting the reset site feeds the telemetry counter (not just statsd).Stacked on #11402 (client-side additional metric tags) — shares the schema/handler reset sites introduced there.
🤖 Generated with Claude Code