otel/16: run conformance against Datadog - #44
Conversation
This comment has been minimized.
This comment has been minimized.
| "query": search, | ||
| "from": query.started_at.isoformat(), | ||
| "to": query.ended_at.isoformat(), | ||
| if query.trace_id: |
There was a problem hiding this comment.
The new query.trace_id discovery branch (@otel.trace_id:"…" search) is the only newly-added code path in _lookup with no test coverage. Every other new path added in this PR — arn-based discovery, cursor pagination, repeated-cursor rejection, _native_trace_id follow-up query, and factory access-token handling — has a dedicated test, but no test constructs a TelemetryQuery with trace_id set for the Datadog backend. As a result a regression in the @otel.trace_id query string, or in how matching_trace/_native_trace_id behave when query.trace_id is populated, would ship undetected. Add a test that drives find_trace with TelemetryQuery(..., trace_id=<128-bit id>) and asserts the first search body's filter.query is @otel.trace_id:"<normalized>" and that the correlated full trace is returned.
This comment has been minimized.
This comment has been minimized.
36b3f4a to
128282f
Compare
This comment has been minimized.
This comment has been minimized.
| end_time=end, | ||
| status="ERROR" if outer.get("status") == "error" else "OK", | ||
| status=normalize_status( | ||
| outer.get("status") or attributes.get("otel.status_code"), |
There was a problem hiding this comment.
Status precedence prevents UNSET from ever surfacing, and no UNSET_STATUS disparity is declared.
outer.get("status") or attributes.get("otel.status_code") gives Datadog's coarse top-level status priority over the precise OTel status code. Datadog derives the span-level status field from the error flag, so a span that OpenTelemetry marked UNSET arrives here as status: "ok" → normalize_status → "OK", and the more accurate otel.status_code ("Unset") is never consulted. The whole point of introducing the otel.status_code fallback in this PR is to recover UNSET/OK/ERROR fidelity, but this ordering defeats it.
Because DatadogBackend.feature_disparities is only {SPAN_LINKS} (unlike XRayBackend, which also declares UNSET_STATUS), _matches_span_status requires an exact status match. Requirements assert status: UNSET on real spans (e.g. test-requirements/otel-invocation/otel-invocation-18.yaml:136 and the long-running suites). With the Datadog job now running on every push/PR (previously it was workflow_dispatch-only), those UNSET selectors will fail against live Datadog data.
Fix: prefer the OTel code, e.g.
status=normalize_status(
attributes.get("otel.status_code") or outer.get("status"),
),or, if Datadog genuinely cannot represent UNSET, add BackendFeatureDisparity.UNSET_STATUS to feature_disparities (matching X-Ray). Either way, add a test covering a payload where both top-level status and otel.status_code are present — the current tests only ever set one, so this precedence path is untested.
This comment has been minimized.
This comment has been minimized.
128282f to
c4b0e87
Compare
This comment has been minimized.
This comment has been minimized.
| DD_API_KEY: ${{ secrets.DD_API_KEY }} | ||
| DD_APPLICATION_KEY: ${{ secrets.DD_APPLICATION_KEY }} | ||
| DATADOG_ACCESS_TOKEN: ${{ secrets.DATADOG_ACCESS_TOKEN }} | ||
| DATADOG_OTLP_ENDPOINT: https://otlp.datadoghq.com/v1/traces |
There was a problem hiding this comment.
Datadog OTLP endpoint doubles the signal path for the Python/TypeScript community layers.
DATADOG_OTLP_ENDPOINT is passed via --otel-endpoint, which the community exporter profile maps to OTEL_EXPORTER_OTLP_ENDPOINT in the template (with OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf). For the Python (opentelemetry-exporter-otlp-proto-http) and TypeScript (@opentelemetry/exporter-trace-otlp-proto) auto-instrumentation layers, that variable is a base URL to which the SDK appends the signal path /v1/traces. With the value https://otlp.datadoghq.com/v1/traces, spans are exported to https://otlp.datadoghq.com/v1/traces/v1/traces, which Datadog rejects, so no traces are ingested and these jobs fail with telemetry timeouts.
Note the asymmetry: the Java example (examples/java/.../OtelConformanceHandler.java) reads the same env var and passes it to OtlpHttpSpanExporter.builder().setEndpoint(...), which expects the full signal URL — so Java requires the /v1/traces suffix while Python/TS require its absence. The established Dash0 jobs confirm the base-URL convention: DASH0_OTLP_ENDPOINT: https://ingress.us-west-2.aws.dash0.com (no /v1/traces).
Because a single endpoint value can't satisfy both interpretations, either supply the base host (https://otlp.datadoghq.com) to Python/TS and give Java the full path separately, or have the Java handler honor OTel base-URL semantics. This same value appears in .github/workflows/typescript-opentelemetry-suite.yml:245 and .github/workflows/java-opentelemetry-suite.yml:236.
This comment has been minimized.
This comment has been minimized.
165b06e to
0eaefbb
Compare
| DD_API_KEY: ${{ secrets.DD_API_KEY }} | ||
| DD_APPLICATION_KEY: ${{ secrets.DD_APPLICATION_KEY }} | ||
| DATADOG_ACCESS_TOKEN: ${{ secrets.DATADOG_ACCESS_TOKEN }} | ||
| DATADOG_OTLP_ENDPOINT: https://otlp.datadoghq.com/v1/traces |
There was a problem hiding this comment.
OTLP export endpoint includes /v1/traces, which the Python/TS community exporters double-append.
DATADOG_OTLP_ENDPOINT is passed to --otel-endpoint, which the community profile writes verbatim into OTEL_EXPORTER_OTLP_ENDPOINT (exporters.py:123; template.yaml:87). With OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf, the OTel SDK treats OTEL_EXPORTER_OTLP_ENDPOINT as a base URL and appends the signal path /v1/traces (unlike OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, which is used as-is). So Python and TypeScript will POST to https://otlp.datadoghq.com/v1/traces/v1/traces, which 404s — no traces are ingested and every Datadog case times out. The existing Dash0 job confirms this convention: it sets the base https://ingress.us-west-2.aws.dash0.com (no path) and the SDK appends /v1/traces.
The same value works for Java only because its handler calls OtlpHttpSpanExporter.setEndpoint(...), which expects the full path and does not append. A single shared value cannot satisfy both mechanisms.
Fix: for Python/TS use the base URL https://otlp.datadoghq.com (or set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to the full path), and keep the full /v1/traces path only for Java's setEndpoint.
| timeout-minutes: 40 | ||
| env: | ||
| DATADOG_ACCESS_TOKEN: ${{ secrets.DATADOG_ACCESS_TOKEN }} | ||
| DATADOG_OTLP_ENDPOINT: https://otlp.datadoghq.com/v1/traces |
There was a problem hiding this comment.
Same OTLP double-path issue as the Python suite.
DATADOG_OTLP_ENDPOINT=https://otlp.datadoghq.com/v1/traces flows through --otel-endpoint into OTEL_EXPORTER_OTLP_ENDPOINT (exporters.py:123; typescript/template.yaml:87-89, http/protobuf). The community JS exporter treats that variable as a base URL and appends /v1/traces, producing .../v1/traces/v1/traces and a 404, so no spans reach Datadog and the run times out. Use the base URL https://otlp.datadoghq.com here (Java keeps the full path because its handler passes it straight to OtlpHttpSpanExporter.setEndpoint).
b9187a7 to
e173d79
Compare
e173d79 to
8312578
Compare
| DD_API_KEY: ${{ secrets.DD_API_KEY }} | ||
| DD_APPLICATION_KEY: ${{ secrets.DD_APPLICATION_KEY }} | ||
| DATADOG_ACCESS_TOKEN: ${{ secrets.DATADOG_ACCESS_TOKEN }} | ||
| DATADOG_OTLP_ENDPOINT: https://otlp.datadoghq.com/v1/traces |
There was a problem hiding this comment.
OTLP endpoint includes /v1/traces, which the Python http/protobuf exporter appends to again → doubled path, no spans ingested.
--otel-endpoint "$DATADOG_OTLP_ENDPOINT" is mapped to OTEL_EXPORTER_OTLP_ENDPOINT (the general OTLP endpoint) by CommunityExporterProfile.configure (exporters.py:123), and this template hard-codes OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf. For the general endpoint variable, the OpenTelemetry Python HTTP exporter unconditionally appends the signal path /v1/traces, so https://otlp.datadoghq.com/v1/traces becomes https://otlp.datadoghq.com/v1/traces/v1/traces. Exports then fail and the Datadog backend times out finding no correlated trace.
Note the existing Dash0 job supplies a path-less base (https://ingress.us-west-2.aws.dash0.com) precisely because the SDK appends /v1/traces. The Java example is unaffected because its handler calls OtlpHttpSpanExporter.setEndpoint() with the full URL (used verbatim, no path appended) — so the same value is correct for Java but wrong here.
Fix: use the base URL for the Python (and TypeScript) jobs:
| DATADOG_OTLP_ENDPOINT: https://otlp.datadoghq.com/v1/traces | |
| DATADOG_OTLP_ENDPOINT: https://otlp.datadoghq.com |
(or set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT instead, which is used as-is).
| timeout-minutes: 40 | ||
| env: | ||
| DATADOG_ACCESS_TOKEN: ${{ secrets.DATADOG_ACCESS_TOKEN }} | ||
| DATADOG_OTLP_ENDPOINT: https://otlp.datadoghq.com/v1/traces |
There was a problem hiding this comment.
OTLP endpoint includes /v1/traces, which the Node http/protobuf exporter appends to again → doubled path, no spans ingested.
--otel-endpoint "$DATADOG_OTLP_ENDPOINT" is mapped to OTEL_EXPORTER_OTLP_ENDPOINT (exporters.py:123) and this template hard-codes OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf. The @opentelemetry/exporter-trace-otlp-proto exporter appends /v1/traces to the general endpoint, so https://otlp.datadoghq.com/v1/traces resolves to https://otlp.datadoghq.com/v1/traces/v1/traces, exports fail, and the backend times out. The Dash0 job uses a path-less base for this reason; the Java handler is unaffected because it calls setEndpoint() with the full URL verbatim.
Fix: use the base URL for the TypeScript job:
| DATADOG_OTLP_ENDPOINT: https://otlp.datadoghq.com/v1/traces | |
| DATADOG_OTLP_ENDPOINT: https://otlp.datadoghq.com |
(or set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, which is used as-is).
4135714 to
eb64e85
Compare
Codex AI review
Reviewed commit |
Claude AI reviewI reviewed only the PR diff (backend/normalizer/polling/redaction changes plus the three suite workflows, Java exporter/template, docs, and tests) against the base revision. The Python-side implementation is careful and well unit-tested (pagination cursor loops, retryable 429 handling, redacted HTTP error bodies, nested-attribute flattening, 128-bit trace IDs, status normalization). Unit coverage is good. I found one integration-level correctness issue and two residual risks worth noting. Finding: hardcoded OTLP endpoint includes
|
Stacked on #41.
Summary
DATADOG_ACCESS_TOKEN, JSON:API request envelopes, cursor pagination, correlation discovery, and full-trace retrievalStateful long-running workflows remain unchanged because Dash0 does not run there either.
Secrets
DATADOG_ACCESS_TOKENauthenticates Datadog API queriesDATADOG_OTLP_HEADERSsupplies the Datadog OTLP intake headersVerification
hatch run test:all(313 passed)hatch run types:checkhatch fmt --check packages scriptshatch run yaml:lint packages/aws-durable-execution-conformance-tests-otel/test-requirements2.1.1-SNAPSHOT