Commit 014e5d0
Implement Client-Side Stats (CSS) 1.2.0 (#8420)
## Summary of changes
Updates the existing client-side-stats implementation to match version
1.2.0 [as defined in the
RFC](https://datadoghq.atlassian.net/wiki/spaces/APM/pages/6378947571/Client-Side+Stats+v1.2.0)
and as [implemented in the
agent](https://github.com/DataDog/datadog-agent/blob/c1d67a906f4c594654600760da1eea4c8037471a/pkg/proto/datadog/trace/stats.proto#L83).
## Reason for change
Our implementation is severely lagging the latest implementation in the
agent. This hasn't been a big deal, as it's not documented and not
enabled by default, but we'd like to fix the implementation to make it
usable.
## Implementation details
This was driven almost entirely by 🤖, by comparing our existing
implementation to the RFC, and also taking the agent/go implementation
as the canonical implementation.
> Implementing this in .NET highlighted a number of missing aspects in
the RFC, which I've raised elsewhere, and aim to get incorporated into
the RFC.
At a high level, the PR contains the following changes:
- Stats Wire Format & Serialization
- **Added new aggregation dimensions**: `SpanKind`, `IsTraceRoot` (as
Trilean), `HTTPMethod`, `HTTPEndpoint`, `GRPCStatusCode`,
`ServiceSource`, `PeerTags` to both the aggregation key and the msgpack
wire format
- **Fixed `GRPCStatusCode` type**: Changed from `int` to `string` to
match Go agent's wire format (agent was returning 400 Bad Request in
system tests)
- **gRPC status code extraction**: Checks 4 tag names in priority order
(`rpc.grpc.status_code`, `grpc.code`, `rpc.grpc.status.code`,
`grpc.status.code`)
- **Stochastic rounding**: `Hits`, `Errors`, `Duration`, `TopLevelHits`
accumulated as `double` (weighted by sampling rate) then rounded
probabilistically to `int64` for serialization
- **Duration weighting**: Durations are now multiplied by sampling
weight (`1/rate`), matching Go agent behavior
- **Default env**: Serializes `"unknown-env"` when environment is not
configured
- **`git_commit_sha`**: Added as optional field in stats payload
- **`Service`**: Added as top-level field in stats payload
- **Empty bucket suppression**: `HasHits()` check prevents sending
payloads with zero-hit buckets (stale keys retained for sketch reuse)
- Bucket Timing
- **10-second alignment**: Bucket `Start` timestamps aligned to
10-second boundaries (`ts - ts % 10_000_000_000`) matching Go tracer's
`alignTs`
- **Removed unused `StartTime`** property (only `Start` as aligned
nanoseconds)
- Agent Discovery (`/info` Endpoint)
- **`peer_tags`**: Parsed, sorted, deduplicated; used for peer tag
extraction on client/producer/consumer spans
- **`span_kinds_stats_computed`**: Parsed to override eligible span
kinds
- **`obfuscation_version`**: Parsed for obfuscation negotiation
- **Trace filters**: Parsed `filter_tags`, `filter_tags_regex`,
`ignore_resources` from `/info`
- Trace Filtering
- **`TraceFilter` implementation**: Evaluates agent-configured filters
(exact tags, regex tags, resource patterns) against root spans before
stats computation
- **Tag-only filters**: Handles filter entries that match on tag key
presence without a specific value
- SQL Obfuscation
- **Operator splitters**: Added `* / = < > ! & ^ % ~ ? @ : #` as token
splitters (matching Go agent's `go-sqllexer` `isOperator()`) so queries
like `WHERE id='1'` are properly obfuscated
- **Whitespace normalization**: Post-processing pass adds spaces around
comparison operators (`=`, `<`, `>`, `!`) adjacent to `?` placeholders,
matching Go agent's normalizer output (e.g., `id='1'` → `id = ?`)
- **Obfuscation gating**: Only runs when `obfuscation_version` is
negotiated with agent; sends `Datadog-Obfuscation-Version` header
- Peer Tags
- **IP quantization**: Peer tag values run through
`IpAddressObfuscationUtil.QuantizePeerIpAddresses()` replacing
non-allowed IPs with `"blocked-ip-address"`
- **Base service handling**: Internal/missing-kind spans with
non-default service name use `_dd.base_service:{serviceName}` as sole
peer tag
- **FNV-1a hashing**: Peer tags hashed with null-byte separators for
aggregation key
- Other
- **No retries for stats**: Stats sends are fire-and-forget (retry limit
= 0)
- **Synthetics detection**: Uses `StartsWith("synthetics")` prefix
matching (not exact match)
- **Mock agent updates**: Test mock agent returns `obfuscation_version`
in `/info` response
## Test coverage
There's a _lot_ going on in this PR, because we were so far behind. I
_could_ split this into implementing individual features, but there
would be a lot of duplication between PRs, and it didn't seem like it
would be that easy to track. At least with this big bang we can compare
directly against the system tests etc.
The existing system tests for stats computation were checked, and made
to pass (which identified a number of hidden expectations which will be
added to the RFC). I'll create a PR to enable these in the system-tests
repo
## Other details
Part of a stack
- #8417
- #8418
There are still some _theoretical_ gaps between the go implementation
and the .NET implementation, but I _think_ these are non-issues in
_most_ cases:
| Area | Gap | Impact |
|------|-----|--------|
| gRPC status code normalization | Go agent normalizes string statuses
(e.g., `"CANCELLED"` → `"1"`); .NET passes raw tag value | Stats
mismatch if gRPC library uses string-form status codes |
| HTTP status code tags | Go agent checks both `http.status_code` and
`http.response.status_code` (OTel convention); .NET only checks
`http.status_code` | OTel spans using newer convention would get `0` in
.NET |
| Duration precision truncation | Go agent uses float bit masking; .NET
uses integer shifting — both target ~10 bits but may produce slightly
different values | Minor histogram differences |
| SQL obfuscation | Go agent uses full tokenizer + normalizer; .NET uses
character-level splitter with targeted normalization around comparison
operators only | Complex SQL with unusual formatting may produce
different resource strings |
| `HTTP_method` / `HTTP_endpoint` | .NET always populates from span
tags; Go agent only populates from newer OTel pipeline paths | Creates
different aggregation keys — Go groups all methods/routes together in
default path |
| `_top_level` metric | Go checks both `_top_level` and `_dd.top_level`;
.NET only checks `_dd.top_level` | Spans using older metric name would
be missed by .NET |
| `span_derived_primary_tags` | Still in Go agent code but implemented
in v1.3.0 RFC, which was reverted; removed from .NET | No current
impact; may need to re-add if spec reverts back |
There is another big elephant in the room, which is perf. The peer tags,
in particular, currently requires a _bunch_ of allocation. I'd rather
defer trying to fight against that to another PR if possible, unless
anyone has some clear ideas 😄
Another aspect I'm not sure about is how this interacts with
@zacharycmontoya's recent work to publish OTLP stats. I took a random
guess and fought the refactoring, but need to verify it.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Lucas Pimentel <lucas.pimentel@datadoghq.com>1 parent 5925926 commit 014e5d0
33 files changed
Lines changed: 1638 additions & 233 deletions
File tree
- tracer
- src/Datadog.Trace
- Agent
- DiscoveryService
- TraceSamplers
- LibDatadog/DataPipeline
- Processors
- test
- Datadog.Trace.TestHelpers
- Stats
- Datadog.Trace.Tests
- Agent
- Sampling
- TraceProcessors
- benchmarks/Benchmarks.Trace
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
47 | 46 | | |
48 | 47 | | |
49 | 48 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
98 | 99 | | |
99 | 100 | | |
100 | 101 | | |
101 | | - | |
| 102 | + | |
102 | 103 | | |
103 | 104 | | |
104 | 105 | | |
105 | | - | |
| 106 | + | |
106 | 107 | | |
107 | | - | |
| 108 | + | |
| 109 | + | |
108 | 110 | | |
109 | 111 | | |
110 | 112 | | |
| |||
138 | 140 | | |
139 | 141 | | |
140 | 142 | | |
141 | | - | |
| 143 | + | |
142 | 144 | | |
143 | 145 | | |
144 | | - | |
145 | 146 | | |
146 | 147 | | |
147 | 148 | | |
| |||
218 | 219 | | |
219 | 220 | | |
220 | 221 | | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
221 | 227 | | |
222 | 228 | | |
223 | 229 | | |
| |||
441 | 447 | | |
442 | 448 | | |
443 | 449 | | |
| 450 | + | |
444 | 451 | | |
445 | | - | |
| 452 | + | |
446 | 453 | | |
447 | 454 | | |
448 | 455 | | |
| 456 | + | |
449 | 457 | | |
450 | 458 | | |
451 | 459 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
77 | | - | |
| 77 | + | |
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
81 | | - | |
| 81 | + | |
82 | 82 | | |
83 | | - | |
| 83 | + | |
| 84 | + | |
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
| |||
92 | 93 | | |
93 | 94 | | |
94 | 95 | | |
95 | | - | |
| 96 | + | |
96 | 97 | | |
97 | 98 | | |
98 | | - | |
99 | 99 | | |
100 | 100 | | |
101 | 101 | | |
| |||
293 | 293 | | |
294 | 294 | | |
295 | 295 | | |
| 296 | + | |
296 | 297 | | |
297 | | - | |
| 298 | + | |
298 | 299 | | |
299 | 300 | | |
300 | 301 | | |
| 302 | + | |
301 | 303 | | |
302 | 304 | | |
303 | 305 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
| 27 | + | |
28 | 28 | | |
29 | | - | |
| 29 | + | |
30 | 30 | | |
31 | 31 | | |
Lines changed: 15 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
| 9 | + | |
8 | 10 | | |
9 | 11 | | |
10 | 12 | | |
| |||
24 | 26 | | |
25 | 27 | | |
26 | 28 | | |
27 | | - | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
28 | 33 | | |
29 | 34 | | |
30 | 35 | | |
| |||
41 | 46 | | |
42 | 47 | | |
43 | 48 | | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
44 | 52 | | |
45 | 53 | | |
46 | 54 | | |
| |||
84 | 92 | | |
85 | 93 | | |
86 | 94 | | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
87 | 101 | | |
Lines changed: 31 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
Lines changed: 21 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
348 | 348 | | |
349 | 349 | | |
350 | 350 | | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
351 | 368 | | |
352 | 369 | | |
353 | 370 | | |
| |||
436 | 453 | | |
437 | 454 | | |
438 | 455 | | |
439 | | - | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
440 | 460 | | |
441 | 461 | | |
442 | 462 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
| 19 | + | |
20 | 20 | | |
21 | 21 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
50 | 51 | | |
51 | 52 | | |
52 | 53 | | |
| 54 | + | |
| 55 | + | |
53 | 56 | | |
54 | 57 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
65 | | - | |
66 | | - | |
| 65 | + | |
| 66 | + | |
67 | 67 | | |
0 commit comments