feat(data-pipeline): add agentless stats export - #2309
Conversation
# Motivation Perform stats aggregation and export directly to the public intake when agentless tracing is enabled # Changes * Add paramateres on the TraceExporterBuilder * agentless_stats_endpoint - the fully formed URL. The API key is derived from the agentless export parameter * Refactor the stats exporter to add a path that wraps tje ClientStatsPayload into a top-level StatsPayloas * Hardcode the agent_version to libdd version + a libdatadog suffix to be able to differentiate metrics origin
📚 Documentation Check Results📦
|
🔒 Cargo Deny Results📦
|
|
BenchmarksComparisonBenchmark execution time: 2026-08-05 15:01:48 Comparing candidate commit 823e943 in PR branch Found 3 performance improvements and 7 performance regressions! Performance is the same for 95 metrics, 10 unstable metrics.
|
Artifact Size Benchmark Reportaarch64-alpine-linux-musl
aarch64-unknown-linux-gnu
libdatadog-x64-windows
libdatadog-x86-windows
x86_64-alpine-linux-musl
x86_64-unknown-linux-gnu
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fc1ff2cd86
ℹ️ 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".
| if let (Some(stats_url), Some(bucket_size)) = ( | ||
| self.agentless_stats_endpoint.as_ref(), | ||
| self.stats_bucket_size, | ||
| ) { |
There was a problem hiding this comment.
Feed agentless traces into the stats concentrator
When this branch is taken, set_agentless_stats_endpoint already requires agentless_config to be present, but send_trace_chunks_inner returns through the agentless path at trace_exporter/mod.rs:786-795 before stats::process_traces_for_stats is called. Therefore spans are never added to the concentrator created here, so the worker flushes an empty concentrator and agentless stats are never exported for the exact configuration this feature enables.
Useful? React with 👍 / 👎.
| // `client_computed` is always set to `true` since the stats were computed by the | ||
| // tracer/client, not the Agent | ||
| client_computed: true, | ||
| split_payload: false, |
There was a problem hiding this comment.
Split agentless stats before sending to intake
For high-cardinality clients, a single flush can contain more than 4000 grouped stats: the concentrator default whole-key limit is 7000 (span_concentrator/mod.rs:123-145). Always setting split_payload false and wrapping the entire client payload in one StatsPayload sends oversized direct-intake payloads instead of matching the Agent's splitter, so agentless stats can be rejected or dropped for those workloads.
Useful? React with 👍 / 👎.
| StatsDestination::Agentless(target) => { | ||
| build_agentless_request(&self.meta, sequence, buckets, target)? | ||
| } |
There was a problem hiding this comment.
Forward the obfuscation marker for agentless stats
With stats-obfuscation enabled, the agentless builder forces obfuscation on and send() calls send_payload(..., true) for obfuscated buckets, but this match arm discards the obfuscated flag and build_agentless_request never adds datadog-obfuscation-version (the Agent arm does). Agentless intake receives obfuscated resource names without the marker for any obfuscated payload.
Useful? React with 👍 / 👎.
| stats = StatsComputationStatus::Enabled { | ||
| stats_concentrator: concentrator, | ||
| worker_handle, | ||
| }; |
There was a problem hiding this comment.
Skip agent-info reconciliation for agentless stats
This puts agentless stats in StatsComputationStatus::Enabled but leaves otlp_stats_enabled false. send_trace_chunks_async still calls check_agent_info() in agentless mode, and that function only skips the agent gate when otlp_stats_enabled is true; if the process has cached AgentInfo from another exporter, handle_stats_enabled can stop this worker when that unrelated agent does not advertise /v0.6/stats. In mixed agent/agentless processes, the first send can therefore shut off the direct-intake stats worker.
Useful? React with 👍 / 👎.
| buckets: Vec<pb::ClientStatsBucket>, | ||
| target: &AgentlessStatsTarget, | ||
| ) -> anyhow::Result<StatsRequest> { | ||
| let mut client_payload = encode_stats_payload(meta, sequence, buckets); |
There was a problem hiding this comment.
Preserve container identity in agentless stats
This reuses encode_stats_payload, which still clears container_id and tags because the Agent normally enriches those fields later. In the agentless path there is no Agent, and StatsMetadata is built without the builder's container_id, so containerized deployments send direct stats without container identity or resolved tags.
Useful? React with 👍 / 👎.
| #[cfg(feature = "compression")] | ||
| let compression = CompressionStrategy::Zstd { level: 1 }; | ||
| #[cfg(not(feature = "compression"))] | ||
| let compression = CompressionStrategy::None; |
There was a problem hiding this comment.
Use gzip for stats intake payloads
When compression is enabled this sends Content-Encoding: zstd, and without it sends raw msgpack, but the Agent sender for the same /api/v0.2/stats route always posts gzipped msgpack with Content-Encoding: gzip (see pkg/trace/writer/stats.go in datadog-agent). If the stats intake follows that contract, these agentless stats requests can be rejected despite passing the local mock.
Useful? React with 👍 / 👎.
Motivation
Perform stats aggregation and export directly to the public intake when agentless tracing is enabled
Changes