Add OTLP/HTTP support to MockTracerAgent and migrate OTLP AspNetCore tests off Docker - #9097
Add OTLP/HTTP support to MockTracerAgent and migrate OTLP AspNetCore tests off Docker#9097chojomok wants to merge 23 commits into
Conversation
Execution-Time Benchmarks Report ⏱️Execution-time results for samples comparing This PR (9097) and master. ✅ No regressions detected |
BenchmarksBenchmark execution time: 2026-09-11 21:25:47 Comparing candidate commit 007df8e in PR branch Found 0 performance improvements and 43 performance regressions! Performance is the same for 28 metrics, 1 unstable metrics, 108 known flaky benchmarks, 18 flaky benchmarks without significant changes.
|
| var relevantSpanIds = otlpSpans.Select(s => s.SpanId).ToHashSet(); | ||
| var tracesRequests = new JArray( | ||
| Fixture.Agent.OtlpTraceRequests | ||
| .Where(r => r.Spans.Any(s => relevantSpanIds.Contains(s.SpanId))) | ||
| .Select(r => JToken.Parse(JsonFormatter.Default.Format(r.Raw)))); | ||
|
|
||
| foreach (var scopeSpan in tracesRequests.SelectTokens($"$..{names.ScopeSpans}[*]")) | ||
| { | ||
| if (scopeSpan["spans"] is JArray spans) | ||
| { | ||
| scopeSpan["spans"] = new JArray( | ||
| spans.Where(s => relevantSpanIds.Contains(HexString.ToHexString(Convert.FromBase64String(s[names.SpanId]!.ToString()))))); | ||
| } | ||
| } |
There was a problem hiding this comment.
uh, feels like we shouldn't need this to be so complicated. It should just follow our general mockspan code flow.
9d37363 to
88e79dc
Compare
7c26fbb to
9f89fbe
Compare
87236e0 to
a1e10f7
Compare
Documents the plan to add OTLP/HTTP (JSON + protobuf) trace decoding to MockTracerAgent per the RFC, and to migrate the OtlpAspNetCore* test suites off the Docker ddapm-test-agent onto it as end-to-end validation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MockTracerAgent (in TestHelpers) needs the generated OTLP trace proto types to decode OTLP/HTTP payloads, but TestHelpers can't depend on Datadog.Trace.Tests where they previously lived. Relocate the generated bindings and their vendored .proto sources so both projects can use them; Datadog.Trace.Tests already project-references TestHelpers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Routes /v1/traces to a new decoder that parses both OTLP/HTTP JSON and
protobuf into the official ExportTraceServiceRequest type via
Google.Protobuf, then maps it into a typed MockOtlp* DTO tree
(MockOtlpTraceRequest -> MockOtlpResourceSpans -> MockOtlpScopeSpans ->
MockOtlpSpan) plus a flattened OtlpSpans view, mirroring the existing
Spans/TraceRequestHeaders/WaitForSpansAsync conventions. /v1/metrics
and /v1/logs are captured raw (no decode) so they never reach the
MessagePack decoder. Responses are protocol-correct: {} for JSON,
zero-byte body for protobuf.
The tracer's OTLP/JSON exporter encodes trace/span/parent IDs as hex
(not the standard base64 protobuf-JSON mapping), so JSON requests get
a pre-pass converting those fields to base64 before Google.Protobuf's
JsonParser runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers protobuf decode, JSON decode with hex-to-base64 ID normalization, gzip-compressed bodies, protocol-correct response shapes, unsupported content-type errors, /v1/metrics and /v1/logs raw capture (isolation from trace decoding), coexistence of Datadog and OTLP requests on the same agent, and WaitForOtlpSpansAsync. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Points the OTLP AspNetCore suites at the fixture's existing in-process MockTracerAgent instead of the Docker ddapm-test-agent, using the new OTLP/HTTP decoding support: ConfigureOtlpExport now takes the mock agent's own /v1/traces URL, set once the agent (and its port) exists via a new onAgentCreated hook on AspNetCoreTestFixture.TryStartApp. Test-case isolation mirrors the existing non-OTLP flow exactly: MockTracerAgent.WaitForOtlpSpansAsync filters by a minDateTime captured before each request, rather than clearing any shared session state. Added OtlpSpanFilters (mirroring SpanFilters) so the warm-up alive-check request's span can be excluded the same way the Datadog protocol path already excludes it. The snapshot pipeline reuses OtlpSnapshotHelper unchanged, bridging each captured MockOtlpTraceRequest back to the OTLP JSON wire shape via Google.Protobuf's JsonFormatter (always camelCase/base64, regardless of which wire protocol the request arrived over) and OtlpFieldNames.For (isJson: true). All existing snapshots pass unmodified. OpenTelemetrySdkTests/OpenTelemetryWebRequestTests/OpenTelemetryHttpClientTests are untouched and remain on the Docker test agent (gRPC/metrics/logs coverage, and traces-only suites left as optional follow-up). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
global:: isn't used elsewhere in the codebase outside a handful of unrelated System.* attribute usages; alias the OTLP proto types instead, matching the existing OtlpSpan/OtlpStatusCode convention from OtlpTracesProtobufSerializerTests.cs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- MockHeaders.GetValue: use TryGetValue instead of GetValues, which throws InvalidOperationException when a header is absent -- a request to /v1/traces with no Content-Type would crash instead of getting the intended 400 response. - Add MockOtlpAttributeValueKind.Empty and handle both a null KeyValue.Value and AnyValue.ValueOneofCase.None, instead of throwing NRE/NotSupportedException for legal-but-empty OTLP attribute values. - MockOtlpJsonIdNormalizer: throw a clear FormatException for a malformed hex ID instead of silently leaving it unconverted, which previously surfaced as a confusing "invalid base64" error from Google.Protobuf's JsonParser further downstream. - Use names.SpanId instead of a hardcoded "spanId" literal in the AspNetCore snapshot-bridge span filter, matching every other lookup in the same method. - Normalize is null/is not null usage in the new OTLP code paths. - Extract the WaitForSpansAsync/WaitForOtlpSpansAsync polling loop (deadline, 16ms clock-skew tolerance, operationName filtering) into a shared private WaitForSpansCoreAsync<TSpan>, removing the near-duplicate implementation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DateTimeOffset.UnixEpoch was added in .NET Core 2.1+ and isn't available on the net48 target Datadog.Trace.TestHelpers also builds for, breaking CI compilation across every dependent test project. Construct the epoch directly instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the JSON-envelope reconstruction in OtlpAspNetCoreTestBase (correlate span IDs against re-parsed JSON, trim JArrays by decoding base64 IDs back to hex) with two additions: - MockTracerAgent.WaitForOtlpTraceRequestsAsync: like WaitForOtlpSpansAsync, but returns the MockOtlpTraceRequests that produced the matching spans, each already trimmed to just those spans, instead of a flat span list. The ID correlation this requires now happens once, in one place, on the typed protobuf model (hex comparison) rather than per-test-file after a JSON round-trip. - OtlpSnapshotHelper.MergeMockOtlpTraceRequests / GetAttributeStringValue(OtlpSpan, ...): typed counterparts to MergeDatadogRequests/GetAttributeStringValue(JToken, ...), merging and sorting multiple requests on the protobuf model before ever touching JSON, instead of after formatting. This also removes the need to stash/restore each span's real start time around normalization, since sorting by real timestamps now happens before NormalizeSpans replaces them with a placeholder. The original JToken-based OtlpSnapshotHelper members are unchanged and still used by suites reading raw JSON from the Docker ddapm-test-agent (e.g. OpenTelemetrySdkTests) -- these are additive, not a replacement. Verified against all 108 OtlpAspNetCoreMvc31Tests/OtlpAspNetCoreMinimalApisTests cases: all pass with zero snapshot changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…f Docker
Alternate approach to the additive one in otlp-http-mocktracer: instead
of keeping the JToken-based OtlpSnapshotHelper methods alongside new
typed ones, update them in place (MergeDatadogRequests and
GetAttributeStringValue now take the typed MockOtlp model; the old
JToken overloads and the unused SetAttributeStringValue are deleted).
OpenTelemetryWebRequestTests and OpenTelemetryHttpClientTests are fully
migrated off the Docker ddapm-test-agent onto MockTracerAgent -- both
verified passing locally against unmodified snapshots.
OpenTelemetrySdkTests.SubmitsOtlpTraces is migrated too (its sibling
methods SubmitsOtlpMetrics/SubmitsOtlpRuntimeMetrics/SubmitsOtlpLogs
stay on Docker: gRPC + metrics/logs decoding are still non-goals for
MockTracerAgent's OTLP support), but its existing snapshots do NOT yet
match:
- doubleValue formatting differs ("1" vs "1.0") -- Google.Protobuf's
JsonFormatter renders whole-number doubles without a trailing ".0",
the old ddapm-agent rendering (or the scrubber pipeline) apparently
preserved it.
- Span content/order mismatch in the non-Datadog-SDK (otelTracesEnabled)
branch -- span identity looks scrambled relative to the verified
file, needs investigation (likely a merge/sort or scope-grouping
difference between the old JToken-array-of-requests shape and the
new typed-then-formatted approach for that branch specifically).
Still TODO before this is mergeable:
- Root-cause and fix both diffs above.
- Decide whether to regenerate OpenTelemetrySdkTests.SubmitsOtlpTraces*
snapshots (likely required for the doubleValue formatting change,
which is a legitimate representation difference, not a bug) vs. fix
in code (the span-order issue is likely a real bug to fix, not a
snapshot update).
- OpenTelemetrySdkTests.SubmitsOtlpTraces only spot-tested with
packageVersion="" (default); not yet run across the full
GetOtlpTracesTestData() matrix (multiple OTel SDK package versions).
- Not yet pushed / no PR opened for this branch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two real bugs found while validating the migration, both fixed: - MergeDatadogRequests's default span sort used StringComparer.Ordinal, but the original JToken-based implementation used a bare OrderBy (culture-aware default comparer). Ordinal reorders names that differ only by the case of a leading letter (e.g. "SomeSpan" vs "some.name"), which looked like a real span-identity mismatch against existing snapshots. Dropped the explicit comparer to match the original default exactly. - AddProtobufToJsonScrubbers (which converts protobuf's enum-as-string rendering, e.g. "SPAN_KIND_INTERNAL", to the int form existing snapshots expect) was only called for http/protobuf rows. Since MockTracerAgent always re-serializes via Google.Protobuf's JsonFormatter regardless of which wire protocol the request arrived over, that mismatch now shows up on every row, not just protobuf ones -- call it unconditionally, matching every other migrated suite. Also added a scrubber for a real formatting difference: JsonFormatter renders a whole-number double as "1" rather than the "1.0" existing snapshots expect (both are valid representations of the same double value). All 8 SubmitsOtlpTraces rows (all protocol/backup/semantics combinations) now pass against the existing, unmodified snapshots. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Keep wording close to main where it already existed; tighten new comments to be proportional to the code they explain. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
a1e10f7 to
641fa0f
Compare
WaitForOtlpTraceRequestsAsync(count: 1) only checked that some export had landed, not that the whole trace had. Pass the real expected count (8 for WebRequestTests/HttpClientTests, 38 for SdkTests) and assert against it, matching the snapshots and the WaitForSpansAsync pattern.
Summary of changes
MockTracerAgent:/v1/tracesdecodes into a typedMockOtlpSpan/MockOtlpTraceRequestDTO tree via the officialExportTraceServiceRequestprotobuf type;/v1/metrics//v1/logsare captured raw (no decode), so they never reach the MessagePack decoder.Datadog.Trace.TestsintoDatadog.Trace.TestHelperssoMockTracerAgentcan use them.WaitForOtlpSpansAsync,OtlpSpanFilters, and protocol-correct responses ({}for JSON, zero-byte body for protobuf), following the existingWaitForSpansAsync/SpanFilters/TraceRequestHeadersconventions.OtlpAspNetCoreMvc21Tests/OtlpAspNetCoreMvc31Tests/OtlpAspNetCoreMinimalApisTestssuites off the Dockerddapm-test-agentonto this new in-process infrastructure.Reason for change
HTTP OTLP integration tests currently send payloads to the Docker
ddapm-test-agentand normalize/snapshot raw JSON. The in-processMockTracerAgentalready receives Datadog traces, stores test-friendly DTOs, and provides async wait helpers. Supporting OTLP/HTTP in the same mock agent gives tests typed spans instead of transport-specific snapshots, and removes a Docker dependency from the OTLP AspNetCore HTTP suites.Implementation details
Design doc:
docs/superpowers/specs/2026-08-21-otlp-http-mocktracer-design.mdHandlePotentialOtlpTracesdecodesContent-Type: application/x-protobufviaExportTraceServiceRequest.Parser.ParseFrom, andapplication/jsonvia a hex-to-base64 ID pre-pass (the tracer's OTLP/JSON exporter encodes IDs as hex, but the standard OTLP JSON mapping expects base64) followed byGoogle.Protobuf'sJsonParser.OtlpAspNetCoreTestBasenow points the sample app's OTLP export at the fixture's existingMockTracerAgentinstance (via a newonAgentCreatedhook onAspNetCoreTestFixture.TryStartApp, since the agent's port isn't known until the agent is created) instead of the Dockerddapm-test-agent. Test-case isolation mirrors the existing non-OTLP flow:WaitForOtlpSpansAsyncfilters by aminDateTimecaptured before each request, and a newOtlpSpanFilterslist (mirroringSpanFilters) excludes the warm-up/alive-checkspan the same way the Datadog-protocol path already does.OtlpSnapshotHelperunchanged, bridging each capturedMockOtlpTraceRequestback to the OTLP JSON wire shape viaGoogle.Protobuf'sJsonFormatter(always camelCase/base64, regardless of which wire protocol the request arrived over) andOtlpFieldNames.For(isJson: true).OpenTelemetrySdkTests/OpenTelemetryWebRequestTests/OpenTelemetryHttpClientTestsare untouched and remain on the Docker test agent (gRPC/metrics/logs coverage, and traces-only suites left as optional follow-up).Test coverage
Datadog.Trace.Tests/Agent/MockOtlpTraceDecodingTests.cs: protobuf decode, JSON decode with hex→base64 ID normalization, gzip, response shapes, unsupported content-type, metrics/logs isolation, Datadog+OTLP coexistence,WaitForOtlpSpansAsync.OtlpAspNetCoreMvc31Tests(54 cases, all feature-flag/OTel-semantics variants) — all pass against the existing, unmodified Verify snapshots.OtlpAspNetCoreMinimalApisTests(54 cases) — all pass against existing snapshots.OtlpAspNetCoreMvc21Tests— not run locally (sample targetsnetcoreapp2.1, not installed on this dev machine); should be verified in CI.Other details
This PR was developed with heavy AI assistance (Claude Code): the design was brainstormed and written up as a spec, the implementation (decoding pipeline, test migration, and code-review fixes) was written and iterated by the AI agent, with human review and direction throughout. Please review accordingly.