OTLP trace metrics export#6834
Conversation
…te injection Phase 2 — OTLP trace metrics export: tests/otel/test_otlp_trace_metrics.py validates that all four dd.trace.span.* metrics appear on /v1/metrics when DD_TRACE_OTEL_METRICS_ENABLED=true (new APM_TRACING_OTLP_METRICS scenario). Phase 3 — W3C TraceState ot.th injection: tests/parametric/test_tracestate_ot_th.py validates that an ot=th:<hex> vendor entry is present in the outgoing tracestate for sampled spans. Supporting changes: - Add apm_tracing_otlp_metrics scenario (scenarios/__init__.py) - Add get_otel_metrics() to OpenTelemetryInterfaceValidator (interfaces) - Add otlp_trace_metrics and w3c_tracestate_ot_probability_threshold features - Mark both tests as missing_feature in manifests/nodejs.yml Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
|
ot.th injection adds an extra vendor entry to the tracestate, which changes the expected member count in several parametric tests. Mark those tests as bugs until the 32-member eviction logic is implemented. Also remove the missing_feature marker for test_tracestate_ot_th since ot.th is now implemented in the nodejs tracer. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Single dd.trace.span.duration histogram replaces the previous four metrics. Data points are split by (error, dd.top_level) matrix; error attr only added when error=true. Rename DD_TRACE_OTEL_METRICS_ENABLED to DD_TRACE_OTEL_STATS_COMPUTATION_ENABLED in scenario env. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Adds a parametric test that enables client-computed span stats and verifies the tracer exports the dd.trace.span.duration histogram (delta temporality) to the test agent's OTLP HTTP receiver with the expected resource attributes. Co-authored-by: Cursor <[email protected]>
🎉 All green!🧪 All tests passed 🔗 Commit SHA: c35abb2 | Docs | Datadog PR Page | Give us feedback! |
Co-authored-by: Munir Abdinur <[email protected]>
Co-authored-by: Munir Abdinur <[email protected]>
- Restructure tests around traces.span.sdk.metrics.duration and the final FR -> test-class mapping; drive export off OTel config (OTEL_METRIC_EXPORT_INTERVAL). - Assert telemetry.sdk.name/telemetry.sdk.language resource attributes and relax the process-tag check to any one known dd.<key> tag. - Use test_agent.get_v06_stats_requests() (tracestats convention) for the native-stats XOR check; drop the bespoke poller. - Fix install_ddtrace.sh to keep only the .tgz path from `bun pm pack` output. - Drop stale per-test nodejs manifest skips that referenced renamed classes. Co-authored-by: Cursor <[email protected]>
Enable tests/parametric/test_otlp_trace_metrics.py for nodejs now that dd-trace-js implements the trace-metrics contract, skipping the three cases nodejs cannot satisfy: sampling independence, DD_HOSTNAME-based host.name, and different-service top-level tagging. Co-authored-by: Cursor <[email protected]>
…dejs test_fr02_4_native_stats_no_otlp exercises the native /v0.6/stats path (OTLP disabled, native enabled), which nodejs has not implemented (consistent with test_library_tracestats). Co-authored-by: Cursor <[email protected]>
… cadence Move service identity assertions to InstrumentationScope attributes (service.name, service.version, deployment.environment.name), add a multi-service partitioning case, drive timing via the internal _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL, and enable the suite for python. Co-authored-by: Cursor <[email protected]>
Update the OTLP trace-metrics parametric tests to the corrected attribute model: service.name/service.version/deployment.environment.name are resource attributes (configured default service), all data points share a single InstrumentationScope, and a span whose service differs from the default carries service.name on its data point. Replaces the per-service scope partitioning assertions. Co-authored-by: Cursor <[email protected]>
The OTLP trace-metrics payload no longer carries a `dd-trace` InstrumentationScope (redundant with the telemetry.sdk.* resource attributes). Update the spec docstring and FR06 tests to assert no scope is emitted. Co-authored-by: Cursor <[email protected]>
…RICS_ENABLED Update OTLP trace-metrics assertions to the datadog.*/_datadog.* attribute prefix and rename the enablement env var to OTEL_TRACES_SPAN_METRICS_ENABLED. Co-authored-by: Cursor <[email protected]>
datadog.* is now the required Datadog attribute prefix, so the guard asserts no short dd.*-prefixed attributes leak instead. Co-authored-by: Cursor <[email protected]>
…DK release; simplify end-state assertions Add a module-level missing_feature skip for test_otlp_trace_metrics.py in python.yml and nodejs.yml (existing per-test skips retained so re-enabling is a one-line change once the feature ships). Remove the silently-passing fixed-bucket-layout test (its guard no-oped when a library emitted no bounds) and its dead constant, condense docstrings, and add failure messages that capture the actual payload state for easier debugging. Co-authored-by: Cursor <[email protected]>
Revert the cosmetic docstring/comment rewordings from the previous commit so the change stays focused on its purpose. The functional improvements are retained: the silently-passing fixed-bucket-layout test stays removed and the assertion failure messages that capture the actual payload are kept. Co-authored-by: Cursor <[email protected]>
Re-add test_fr09_3_fixed_bucket_layout (and EXPLICIT_BOUNDS_SECONDS) so it now requires explicitBounds to be present and match the fixed spanmetrics layout instead of no-opping when bounds are absent. Trim obvious narrating comments and drop docstring references to the removed InstrumentationScope checks. Co-authored-by: Cursor <[email protected]>
…ource attribute Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
| [ | ||
| { | ||
| **_BASE_ENVVARS, | ||
| "OTEL_TRACES_SPAN_METRICS_ENABLED": "false", |
There was a problem hiding this comment.
What happens if both are true? Which one wins? This could use a test case
There was a problem hiding this comment.
Or maybe it's worth setting the env variable OTEL_TRACES_SPAN_METRICS_ENABLED=true in the test_fr02_3_otlp_suppresses_native_stats test case if OTLP wins
| "library_env", | ||
| [{**_BASE_ENVVARS, "OTEL_TRACES_EXPORTER": "none", "DD_METRICS_OTEL_ENABLED": "true"}], | ||
| ) | ||
| def test_fr01_5_disabled_when_traces_exporter_not_otlp( |
There was a problem hiding this comment.
The test name describes the scenario as "traces_exporter_not_otlp" but that is already tested in test_fr01_1_enabled_explicit. I suggest renaming this because what you're actually doing is disabling traces altogether with OTEL_TRACES_EXPORTER=none. You could also add a test that disables tracing via the DD environment variable, so DD_APM_TRACING_ENABLED=false
| def test_fr01_5_disabled_when_traces_exporter_not_otlp( | |
| def test_fr01_5_disabled_when_tracing_is_disabled( |
|
|
||
| metrics = test_agent.wait_for_num_otlp_metrics(num=1) | ||
| attrs = _data_point_attrs(_duration_data_points(metrics)[0]) | ||
| assert attrs.get("span.kind") == "server", f"Expected span.kind=server, got attrs: {attrs}" |
There was a problem hiding this comment.
Should the value of the span.kind attribute match the casing of span metrics connector (in this case SERVER)? See the README here
| metrics = test_agent.wait_for_num_otlp_metrics(num=1) | ||
| attrs = _data_point_attrs(_duration_data_points(metrics)[0]) | ||
| assert attrs.get("http.response.status_code") == 200, ( | ||
| f"Expected http.response.status_code == 200 (typed int), got attrs: {attrs}" |
There was a problem hiding this comment.
Are we mixing and matching attribute types? Or should all attributes on the time-series have string values?
There was a problem hiding this comment.
FWIW, I asked Claude to take a look at the Span Metrics Connector to understand how the 'dimensions' configuration copies span attributes to the resulting time series. If an attribute with the matching name exists on the span, then it is copied as-is to the timeseries, so an example of ip.port with an IntValue would be copied as an IntValue onto the resulting metrics timeseries
| ), f"Expected deployment environment=prod, got: {resource_attrs}" | ||
|
|
||
| # The span uses the configured default service, so its data point omits service.name. | ||
| assert SERVICE not in _data_point_services(metrics), ( |
There was a problem hiding this comment.
I think we should always emit service.name on the time series as a metric attribute, since that follows behavior of SMC. I think OpenTelemetry users would expect the metric itself to have all the metric attributes to fully describe the spans, which includes the service name
|
|
||
| metrics = test_agent.wait_for_num_otlp_metrics(num=1) | ||
| attrs = _data_point_attrs(_duration_data_points(metrics)[0]) | ||
| assert attrs.get("span.name") == "/users", f"Expected span.name=/users, got attrs: {attrs}" |
There was a problem hiding this comment.
If we want to add assertions for datadog.resource.name, we should also assert that it's not included here. Otherwise, we can remove all assertions on this name since it's no longer mentioned in the RFC
| assert attrs.get("span.name") == "/users", f"Expected span.name=/users, got attrs: {attrs}" | |
| assert attrs.get("span.name") == "/users", f"Expected span.name=/users, got attrs: {attrs}" | |
| assert "datadog.resource.name" not in attrs, f"datadog.resource.name must be absent: {attrs}" |
|
|
||
| metrics = test_agent.wait_for_num_otlp_metrics(num=1) | ||
| attrs = _data_point_attrs(_duration_data_points(metrics)[0]) | ||
| assert attrs.get("http.request.method") == "GET", f"Expected http.request.method=GET, got attrs: {attrs}" |
There was a problem hiding this comment.
By default, the span metrics connector only emits the following attributes:
- service.name
- span.name
- span.kind
- status.code
- (feature-gate) collector.instance.id
- (feature-gate) otel.status_code
In our DD_TRACE_OTEL_SEMANTICS_ENABLED mode we are emitting additional fields like env and these HTTP ones. Is the intention that even in OTel semantics mode we emit the full set of attributes our own stats computation needs? If so, can we consider a configuration flag to not emit a user-specified set of attribute keys?
…-dotnet#8921 Replaces the module-level missing_feature skip with v3.50.0 (the version that first shipped OTLP trace-metrics export via #8826) so CI runs the parametric suite added in #6834 against a specific dd-trace-dotnet branch via the PR title branch override, to check that the PR removing the OtelTracesSpanMetricsEnabled mixed-mode path in TracerManagerFactory still satisfies the FR01 enablement tests. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Motivation
Datadog tracing libraries can export client-computed span aggregate metrics in OTLP format as a histogram (
traces.span.sdk.metrics.duration), complementing OTLP trace export. This is the basis for RED metrics (Requests / Errors / Duration) derived from the histogram:count{*}count{error:true}count{datadog.span.top_level:true}This PR adds black-box parametric coverage for that feature: parametric apps run with various configurations, spans are generated, and the resulting OTLP metrics (and native v0.6 stats, where relevant) are captured and asserted on. It lands the test cases called out in the RFC (SEMCON-1093).
What the tests validate
tests/parametric/test_otlp_trace_metrics.py— 42 tests grouped by functional requirement, under thePARAMETRICscenario (@features.client_side_stats_supported):Datadog-Client-Computed-Statsheader)sservice.nameon a data point when a span's service differs from the default, hostname, telemetry SDKDD_TRACE_OTEL_SEMANTICS_ENABLED) — only OpenTelemetry attributes, nodatadog.*/dd.*End-state focus: each test exercises one behaviour and asserts on the final exported payload (not on transitional state). Assertions are shape-tolerant where a value is library-dependent — FR03 checks
unit, delta temporality and0 < sum < 60; FR09 checkscount,min == maxandsum(bucketCounts)for a single span. Flush cadence is fixed at 10s and shortened in tests via the internal_DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL(ms). Tests pin HTTP/JSON export, the transport common to all libraries.Cross-tracer parity (documented, not asserted):
sum/min/maxare exact in dd-trace-js (sketch scalars) but sketch-approximated in libdatadog/dd-trace-py;host.namesource differs (libdatadog honorsDD_HOSTNAME, dd-trace-js usesos.hostname()), so the hostname test asserts presence only.Activation
The SDK implementations ship in future library releases, so these tests cannot pass against currently released libraries in CI. They are therefore skipped for all languages via a module-level
missing_featureentry in every manifest.pythonandnodejsadditionally keep their per-test skips (library-specific gaps such as nodejs native v0.6 stats); leaving those in place means re-enabling the suite once the feature ships is simply removing the single module-level line.Supporting changes
utils/build/docker/nodejs/parametric/server.js,utils/build/docker/nodejs/install_ddtrace.sh— parametric test-client support for the OTLP trace-metrics path.utils/docker_fixtures/_test_agent.py— polling helpers (wait_for_num_otlp_metrics,wait_for_num_v06_stats) to accommodate OTLP/native export latency.What to review
Reviewer checklist