Skip to content

feat(opentelemetry): OTLP trace metrics support#8206

Merged
mabdinur merged 68 commits into
masterfrom
ida613/otlp-trace-metrics-poc
Jul 2, 2026
Merged

feat(opentelemetry): OTLP trace metrics support#8206
mabdinur merged 68 commits into
masterfrom
ida613/otlp-trace-metrics-poc

Conversation

@ida613

@ida613 ida613 commented May 1, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds OTLP span metrics export and extends the existing trace stats aggregation key with new dimensions (span.kind, rpc.response.status_code, origin) to bring dd-trace-js to parity with other Datadog SDKs.

Motivation

When OTEL_TRACES_EXPORTER=otlp is active, the Datadog Agent no longer receives /v0.6/stats — span metrics that APM depends on are lost. This PR adds a native JS implementation that aggregates span stats and emits them as a traces.span.sdk.metrics.duration delta histogram to /v1/metrics. It is a bridge until the libdatadog-based concentrator lands in dd-trace-js; the aggregation key and wire format are kept in sync with libdatadog to make that a drop-in swap.

Additional Notes

Activation: OTEL_TRACES_SPAN_METRICS_ENABLED=true, or auto-enabled when both OTEL_TRACES_EXPORTER=otlp and DD_METRICS_OTEL_ENABLED=true are set.

Mutual exclusion: the OTLP and native /v0.6/stats export paths are mutually exclusive. When the OTLP exporter is injected, the native SpanStatsExporter is never constructed and onInterval uses else if to enforce this structurally.

New aggregation dimensions (both modes): span.kind, origin, and rpc.response.status_code (from grpc.status.code, emitted as string per libdatadog semantics) are now always included in SpanAggKey. The native /v0.6/stats wire payload is unchanged; only the JS-side bucketing is more granular.

Agent coordination: _dd.stats_computed: "true" is added as a resource attribute on OTLP trace exports when active, preventing the Agent's OTLP receiver from double-counting. Datadog-Client-Computed-Stats: yes is still sent on the native path.

Histogram: count/sum/min/max are DDSketch exact scalars. Bucket distribution is projected onto fixed explicit bounds matching libdatadog's EXPLICIT_BOUNDS_SECONDS.

Code structure: all OTLP-specific code lives under packages/dd-trace/src/opentelemetry/metrics/. SpanStatsProcessor receives the exporter via DI from opentracing/tracer.js; span_stats.js has no imports from opentelemetry/.

OTel semantics mode: when DD_TRACE_OTEL_SEMANTICS_ENABLED=true, dd.* attributes are omitted from trace metrics data points.

Known gaps (tracked separately): cross-service entry spans not tagged top-level (FR06.3); p0 traces not dropped when client stats are active (FR09.1).

Tests: test/span_stats.spec.js, test/opentelemetry/metrics/otlp_span_stats_exporter.spec.js, test/opentelemetry/metrics/otlp_span_stats_transformer.spec.js. Cross-tracer coverage via the system-tests parametric suite.

@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Overall package size

Self size: 6.45 MB
Deduped: 7.52 MB
No deduping: 7.52 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.2.0 | 104.26 kB | 843.44 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented May 1, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 90.64%
Overall Coverage: 88.03% (+0.04%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: aa76c17 | Docs | Datadog PR Page | Give us feedback!

@codecov

codecov Bot commented May 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.58537% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.74%. Comparing base (d1aa54a) to head (aa76c17).
⚠️ Report is 22 commits behind head on master.

Files with missing lines Patch % Lines
...ckages/dd-trace/src/opentelemetry/metrics/index.js 91.66% 2 Missing ⚠️
packages/dd-trace/src/opentracing/tracer.js 50.00% 2 Missing ⚠️
packages/dd-trace/src/opentelemetry/trace/index.js 50.00% 1 Missing ⚠️
packages/dd-trace/src/span_processor.js 66.66% 1 Missing ⚠️
packages/dd-trace/src/span_stats.js 97.91% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8206      +/-   ##
==========================================
+ Coverage   93.69%   93.74%   +0.05%     
==========================================
  Files         889      897       +8     
  Lines       50864    51540     +676     
  Branches    11834    12036     +202     
==========================================
+ Hits        47657    48318     +661     
- Misses       3207     3222      +15     
Flag Coverage Δ
aiguard 34.89% <69.23%> (-0.07%) ⬇️
aiguard-integration 41.80% <69.23%> (+0.01%) ⬆️
apm-bucket-0 34.86% <69.23%> (-0.07%) ⬇️
apm-bucket-1 40.35% <69.23%> (-0.07%) ⬇️
apm-bucket-2 37.35% <69.23%> (-0.07%) ⬇️
apm-capabilities-tracing 48.79% <96.58%> (+0.48%) ⬆️
apm-integrations-aerospike 33.14% <69.23%> (-0.07%) ⬇️
apm-integrations-confluentinc-kafka-javascript 40.04% <69.23%> (-0.07%) ⬇️
apm-integrations-couchbase 33.26% <69.23%> (-0.23%) ⬇️
apm-integrations-http 41.97% <69.23%> (-0.15%) ⬇️
apm-integrations-kafkajs 40.27% <69.23%> (-0.07%) ⬇️
apm-integrations-next 29.52% <69.23%> (-0.06%) ⬇️
apm-integrations-prisma 35.05% <69.23%> (-0.07%) ⬇️
apm-integrations-tedious 33.91% <69.23%> (-0.07%) ⬇️
appsec 57.69% <69.23%> (+0.32%) ⬆️
appsec-express_fastify_graphql 53.68% <69.23%> (-0.17%) ⬇️
appsec-integration 36.45% <69.23%> (+0.19%) ⬆️
appsec-kafka_ldapjs_lodash 43.60% <69.23%> (-0.07%) ⬇️
appsec-mongodb-core_mongoose_mysql 48.75% <69.23%> (-0.13%) ⬇️
appsec-next 28.02% <69.23%> (-0.05%) ⬇️
appsec-node-serialize_passport_postgres 47.92% <69.23%> (-0.15%) ⬇️
appsec-sourcing_stripe_template 45.48% <69.23%> (-0.14%) ⬇️
debugger 44.38% <69.23%> (+0.01%) ⬆️
instrumentations-bucket-0 28.17% <69.23%> (-0.06%) ⬇️
instrumentations-bucket-1 37.42% <69.23%> (-0.07%) ⬇️
instrumentations-bucket-10 40.36% <69.23%> (-0.14%) ⬇️
instrumentations-bucket-11 27.97% <69.23%> (-0.06%) ⬇️
instrumentations-bucket-12 28.69% <69.23%> (-0.06%) ⬇️
instrumentations-bucket-13 27.80% <69.23%> (-0.06%) ⬇️
instrumentations-bucket-2 30.25% <69.23%> (-0.06%) ⬇️
instrumentations-bucket-3 35.92% <69.23%> (-0.07%) ⬇️
instrumentations-bucket-4 28.58% <69.23%> (+0.43%) ⬆️
instrumentations-bucket-5 36.29% <69.23%> (-0.06%) ⬇️
instrumentations-bucket-6 38.27% <69.23%> (-0.07%) ⬇️
instrumentations-bucket-7 36.02% <69.23%> (-0.05%) ⬇️
instrumentations-bucket-8 36.97% <69.23%> (-0.07%) ⬇️
instrumentations-bucket-9 39.47% <69.23%> (-0.14%) ⬇️
instrumentations-instrumentation-couchbase 46.48% <ø> (-0.07%) ⬇️
instrumentations-integration-esbuild 25.07% <69.23%> (+0.19%) ⬆️
llmobs-ai_anthropic_bedrock 39.54% <69.23%> (-0.06%) ⬇️
llmobs-google-genai_langchain_vertex-ai 36.98% <69.23%> (-0.05%) ⬇️
llmobs-openai 39.59% <69.23%> (-0.07%) ⬇️
llmobs-sdk 43.61% <69.23%> (-0.08%) ⬇️
master-coverage 93.74% <96.58%> (?)
openfeature 37.78% <69.23%> (+0.01%) ⬆️
openfeature-unit 50.47% <83.33%> (+0.07%) ⬆️
platform-core_esbuild_instrumentations-misc 23.51% <69.23%> (+0.14%) ⬆️
platform-integration 47.39% <59.52%> (-0.04%) ⬇️
platform-shimmer_unit-guardrails_webpack 19.02% <0.00%> (+0.12%) ⬆️
plugins-bucket-0 36.39% <69.23%> (-0.01%) ⬇️
plugins-bucket-1 39.61% <69.23%> (+<0.01%) ⬆️
plugins-bucket-11 38.47% <69.23%> (-0.07%) ⬇️
plugins-bucket-17 39.16% <69.23%> (+0.08%) ⬆️
plugins-bucket-18 42.02% <69.23%> (-0.06%) ⬇️
plugins-bucket-19 39.57% <69.23%> (-0.07%) ⬇️
plugins-bucket-20 43.27% <69.23%> (-0.07%) ⬇️
plugins-bucket-4 37.73% <69.23%> (-0.07%) ⬇️
plugins-bullmq_cassandra_cookie 39.77% <69.23%> (-0.08%) ⬇️
plugins-cookie-parser_crypto_dd-trace-api 33.24% <69.23%> (-0.07%) ⬇️
plugins-fetch_fs_generic-pool 36.07% <69.23%> (-0.07%) ⬇️
plugins-google-cloud-pubsub_grpc_handlebars 43.11% <69.23%> (-0.07%) ⬇️
plugins-hapi_hono_ioredis 37.80% <69.23%> (-0.07%) ⬇️
plugins-jest_knex_langgraph 32.62% <69.23%> (-0.07%) ⬇️
plugins-ldapjs_light-my-request_limitd-client 27.87% <69.23%> (-0.12%) ⬇️
plugins-lodash_mariadb_memcached 35.19% <69.23%> (-0.07%) ⬇️
plugins-mongodb_mongodb-core_mongoose 36.32% <69.23%> (-0.07%) ⬇️
plugins-multer_mysql_mysql2 35.16% <69.23%> (-0.07%) ⬇️
plugins-nats_node-serialize_opensearch 37.16% <69.23%> (-0.07%) ⬇️
plugins-passport-http_pino_postgres 35.54% <69.23%> (-0.07%) ⬇️
plugins-process_pug_redis 34.28% <69.23%> (+0.15%) ⬆️
plugins-undici_url_valkey 35.90% <69.23%> (-0.06%) ⬇️
plugins-vm_winston_ws 37.57% <69.23%> (-0.07%) ⬇️
profiling 43.63% <69.23%> (-0.06%) ⬇️
serverless-aws-sdk-aws-sdk 33.31% <69.23%> (-0.06%) ⬇️
serverless-aws-sdk-bedrockruntime 32.18% <69.23%> (-0.06%) ⬇️
serverless-aws-sdk-client 37.16% <ø> (-0.05%) ⬇️
serverless-aws-sdk-dynamodb 34.16% <69.23%> (-0.01%) ⬇️
serverless-aws-sdk-eventbridge 27.27% <69.23%> (-0.05%) ⬇️
serverless-aws-sdk-kinesis 37.42% <69.23%> (-0.06%) ⬇️
serverless-aws-sdk-lambda 34.61% <69.23%> (-0.06%) ⬇️
serverless-aws-sdk-s3 32.61% <69.23%> (-0.06%) ⬇️
serverless-aws-sdk-serverless-peer-service 39.49% <69.23%> (-0.06%) ⬇️
serverless-aws-sdk-sns 38.27% <69.23%> (-0.06%) ⬇️
serverless-aws-sdk-sqs 38.01% <69.23%> (-0.06%) ⬇️
serverless-aws-sdk-stepfunctions 33.20% <69.23%> (-0.06%) ⬇️
serverless-aws-sdk-util 47.95% <ø> (ø)
serverless-bucket-0 39.47% <69.23%> (+<0.01%) ⬆️
serverless-lambda 34.36% <69.23%> (-0.07%) ⬇️
test-optimization-cucumber 52.31% <69.23%> (-0.16%) ⬇️
test-optimization-cypress 49.42% <69.23%> (-0.16%) ⬇️
test-optimization-jest 55.35% <69.23%> (-0.23%) ⬇️
test-optimization-mocha 53.39% <69.23%> (-0.20%) ⬇️
test-optimization-playwright-playwright-atr 43.34% <69.23%> (-0.17%) ⬇️
test-optimization-playwright-playwright-efd 43.62% <69.23%> (-0.17%) ⬇️
test-optimization-playwright-playwright-final-status 43.97% <69.23%> (+0.12%) ⬆️
test-optimization-playwright-playwright-impacted-tests 43.17% <69.23%> (-0.25%) ⬇️
test-optimization-playwright-playwright-reporting 43.27% <69.23%> (-0.14%) ⬇️
test-optimization-playwright-playwright-test-management 44.73% <69.23%> (-0.13%) ⬇️
test-optimization-playwright-playwright-test-span 44.52% <69.23%> (-0.19%) ⬇️
test-optimization-selenium 45.27% <69.23%> (-0.19%) ⬇️
test-optimization-testopt 48.02% <69.23%> (-0.19%) ⬇️
test-optimization-vitest 51.01% <69.23%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pr-commenter

pr-commenter Bot commented May 1, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-07-01 19:56:05

Comparing candidate commit aa76c17 in PR branch ida613/otlp-trace-metrics-poc with baseline commit d1aa54a in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 2246 metrics, 40 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:appsec-appsec-enabled-20

  • unstable execution_time [-194.502ms; +189.904ms] or [-5.263%; +5.138%]

scenario:appsec-appsec-enabled-24

  • unstable execution_time [-180.395ms; +184.047ms] or [-6.878%; +7.017%]

scenario:appsec-appsec-enabled-26

  • unstable execution_time [-212.155ms; +208.473ms] or [-8.414%; +8.268%]

scenario:appsec-appsec-enabled-with-attacks-26

  • unstable execution_time [-161.362ms; +154.618ms] or [-5.611%; +5.376%]

scenario:appsec-control-20

  • unstable execution_time [-147.372ms; +157.221ms] or [-8.408%; +8.970%]

scenario:appsec-control-24

  • unstable execution_time [-102868.281µs; +104198.781µs] or [-8.498%; +8.608%]

scenario:appsec-control-26

  • unstable execution_time [-114.368ms; +118.054ms] or [-9.446%; +9.750%]

scenario:debugger-line-probe-with-snapshot-default-24

  • unstable cpu_user_time [-2.593s; +6.234s] or [-25.296%; +60.815%]
  • unstable execution_time [-2.591s; +6.240s] or [-23.590%; +56.820%]
  • unstable instructions [-22.4G instructions; +52.9G instructions] or [-26.543%; +62.814%]
  • unstable max_rss_usage [-8.492MB; +17.948MB] or [-5.142%; +10.868%]
  • unstable throughput [-1545.216op/s; +669.506op/s] or [-47.836%; +20.726%]

scenario:debugger-line-probe-with-snapshot-default-26

  • unstable cpu_user_time [-1814.624ms; +589.115ms] or [-19.476%; +6.323%]
  • unstable execution_time [-1824.568ms; +605.213ms] or [-18.209%; +6.040%]
  • unstable instructions [-16.4G instructions; +5.4G instructions] or [-21.127%; +6.989%]
  • unstable throughput [-135.957op/s; +400.385op/s] or [-4.146%; +12.211%]

scenario:debugger-line-probe-with-snapshot-minimal-24

  • unstable cpu_user_time [-1705.071ms; +2964.585ms] or [-21.398%; +37.205%]
  • unstable execution_time [-1747.995ms; +3006.958ms] or [-20.218%; +34.779%]
  • unstable instructions [-14.9G instructions; +25.3G instructions] or [-23.062%; +39.001%]
  • unstable max_rss_usage [-6.740MB; +10.170MB] or [-4.257%; +6.424%]
  • unstable throughput [-900.054op/s; +499.148op/s] or [-23.597%; +13.087%]

scenario:debugger-line-probe-with-snapshot-minimal-26

  • unstable cpu_user_time [-2739.664ms; +2856.294ms] or [-27.508%; +28.679%]
  • unstable execution_time [-2736.059ms; +2875.248ms] or [-25.697%; +27.004%]
  • unstable instructions [-25409.3M instructions; +26118.8M instructions] or [-30.479%; +31.330%]
  • unstable max_rss_usage [-9191.145KB; +9151.145KB] or [-5.714%; +5.689%]
  • unstable throughput [-641.885op/s; +586.862op/s] or [-20.481%; +18.726%]

scenario:debugger-line-probe-without-snapshot-24

  • unstable cpu_user_time [-2623.795ms; +3775.151ms] or [-29.093%; +41.860%]
  • unstable execution_time [-2662.963ms; +3834.234ms] or [-27.359%; +39.392%]
  • unstable instructions [-22.4G instructions; +32.6G instructions] or [-30.236%; +44.004%]
  • unstable max_rss_usage [-9.665MB; +14.050MB] or [-5.964%; +8.670%]
  • unstable throughput [-1116.197op/s; +789.681op/s] or [-31.858%; +22.539%]

scenario:debugger-line-probe-without-snapshot-26

  • unstable cpu_user_time [-2137.010ms; +3333.436ms] or [-22.782%; +35.536%]
  • unstable execution_time [-2225.150ms; +3447.872ms] or [-22.099%; +34.243%]
  • unstable instructions [-18.5G instructions; +29.4G instructions] or [-23.697%; +37.723%]
  • unstable max_rss_usage [-7.039MB; +10.796MB] or [-4.444%; +6.816%]
  • unstable throughput [-738.926op/s; +485.402op/s] or [-22.630%; +14.866%]

scenario:dogstatsd-with-tags-20

  • unstable cpu_user_time [-508.628ms; +187.481ms] or [-10.271%; +3.786%]
  • unstable execution_time [-506.530ms; +184.131ms] or [-10.077%; +3.663%]
  • unstable throughput [-62374.607op/s; +175156.263op/s] or [-3.736%; +10.492%]

scenario:plugin-graphql-long-with-depth-on-max-20

  • unstable max_rss_usage [-42.883MB; +6.482MB] or [-22.475%; +3.397%]

@ida613 ida613 closed this May 20, 2026
@mabdinur mabdinur reopened this Jun 2, 2026
@mabdinur
mabdinur force-pushed the ida613/otlp-trace-metrics-poc branch from 00d2998 to f8ccbb4 Compare June 3, 2026 15:36
@mabdinur mabdinur changed the title [WIP] feat(opentelemetry): OTLP trace metrics support feat(opentelemetry): OTLP trace metrics support Jun 3, 2026
@mabdinur

Copy link
Copy Markdown
Contributor

This commit sets _dd.stats_computed: 'true' as an OTLP resource attribute on trace exports when otlpTraceMetricsEnabled — alongside the existing Datadog-Client-Computed-Stats header on the native agent path, this tells Datadog Agent OTLP receivers to skip their concentrator and prevents double-counted APM metrics; the attribute is set unconditionally regardless of OTel semantics mode.

ida613 and others added 18 commits June 15, 2026 20:06
Export client-computed span stats as OTLP metrics (dd.trace.span.hits,
dd.trace.span.errors, dd.trace.span.top_level_hits, dd.trace.span.duration)
via a new OtlpStatsExporter alongside the existing Datadog /v0.6/stats
exporter.

Enabled via DD_TRACE_OTEL_METRICS_ENABLED=true, or auto-enabled when both
OTEL_TRACES_EXPORTER=otlp and OTEL_METRICS_EXPORTER=otlp are set. URL and
protocol are derived from the OTLP trace export configuration.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
traceMetrics is a computed aggregate derived from OTEL_TRACES_EXPORTER,
OTEL_METRICS_EXPORTER, and DD_TRACE_OTEL_METRICS_ENABLED — not a raw
user-facing key — so it belongs in INTERNAL_RUNTIME_PROPERTIES alongside
sampler and stableConfig.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…ndpoint

When hostname is an unbracketed IPv6 address (e.g. ::1), the defaultOtlpBase
is http://::1:4318 which is not a valid URL. The new URL() call in the
traceMetrics block was the first code path to actually parse the string,
causing a TypeError that crashed config construction.

Wrap the URL derivation in a try/catch so that a malformed traces endpoint
falls back to the localhost default without throwing.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…entry

The eslint-config-names-sync rule verifies that every leaf property in
TracerOptions (index.d.ts) has a matching configurationNames entry in
supported-configurations.json. The entry for DD_TRACE_OTEL_METRICS_ENABLED
only had internalPropertyName, which is not checked by the rule.

Adding configurationNames: ["traceMetricsEnabled"] ties the two files
together and satisfies the lint check. Regenerated config types to match.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…ics contract

Update the OTLP trace-metrics export to match the agreed RFC/system-test contract:

- Rename the enablement env var to OTEL_CLIENT_STATS_COMPUTATION_ENABLED and add
  DD_TRACE_OTEL_SEMANTICS_ENABLED (OTel-semantics mode: emit only OTel attributes, no dd.*).
- Emit a single histogram named traces.span.sdk.metrics.duration.
- Map dimensions to OTel attributes (span.name, span.kind, http.*, rpc.* from grpc tags) and
  convey errors via OTel status.code; default mode also adds dd.operation.name, dd.span.type,
  dd.origin and dd.span.top_level.
- Add telemetry.sdk.{name,language,version} resource attributes and emit process tags as dd.<key>
  (default mode only); gate all dd.* resource attributes behind default mode.
- Drive the flush/export cadence from OTEL_METRIC_EXPORT_INTERVAL and drop the
  _DD_TRACE_STATS_WRITER_INTERVAL override.
- Read grpc.status.code from span.metrics (numeric) with a meta fallback.

Update unit tests accordingly and regenerate config types.

Co-authored-by: Cursor <[email protected]>
…Scope

Partition span-stats data points by service so one OTLP payload can carry
multiple services, each as its own InstrumentationScope with service.name,
service.version and deployment.environment.name. These move off the resource,
which now only carries SDK identity, host.name and dd.* attributes.

Fix the trace-metrics flush cadence at 10s (no longer driven by
OTEL_METRIC_EXPORT_INTERVAL); the internal _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL
overrides it in tests only.

Co-authored-by: Cursor <[email protected]>
The generic env applier only reads DD_/OTEL_ prefixed vars, so the
internal _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL (which starts with _DD_)
was never wired into config. Read it explicitly so the test-only flush
cadence override takes effect.

Co-authored-by: Cursor <[email protected]>
…DDSketch

Derive the OTLP duration histogram from each group's DDSketch into the
spanmetrics-connector default bounds (in seconds), and drop the duplicate
exact-cell accumulator in span_stats. Each group now emits at most two data
points (ok/error) with a per-group dd.span.top_level heuristic, mirroring
libdatadog.

Co-authored-by: Cursor <[email protected]>
Move service.name/service.version/deployment.environment.name onto the OTLP
resource (the configured default service), emit a single InstrumentationScope,
and add service.name as a data-point attribute only when a span's service
differs from the configured default. Thread DD_SERVICE through the processor so
the transformer can compare against it.

Co-authored-by: Cursor <[email protected]>
The exported OTLP metrics no longer carry an InstrumentationScope: a `dd-trace`
scope (name/version) is redundant with the resource's telemetry.sdk.* attributes.
The single scopeMetrics omits the scope field.

Co-authored-by: Cursor <[email protected]>
…_METRICS_ENABLED

Rename the OTLP trace-metric attributes from dd.* to datadog.* (operation.name,
span.type, span.top_level, origin, runtime_id, datadog.<process tags>) and rename
the enablement env var OTEL_CLIENT_STATS_COMPUTATION_ENABLED ->
OTEL_TRACES_SPAN_METRICS_ENABLED.

Co-authored-by: Cursor <[email protected]>
…when trace metrics enabled

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@mabdinur
mabdinur force-pushed the ida613/otlp-trace-metrics-poc branch from 4fbb818 to 393a8e9 Compare June 16, 2026 00:20
… otelSemanticsEnabled to types

span_format.js: rebase conflict resolved to branch's addTag refactor which no longer exists in
master — revert to explicit typeof checks while keeping the FR06.3 BUG comment.

index.d.ts: rename traceMetricsEnabled -> otlpTraceMetricsEnabled to match supported-configurations.json;
add otelSemanticsEnabled (DD_TRACE_OTEL_SEMANTICS_ENABLED). Fixes eslint-config-names-sync errors.

Regenerate generated-config-types.d.ts from updated inputs.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f306149aa5

ℹ️ 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".

Comment thread packages/dd-trace/src/span_stats.js Outdated
…o status name

The dd gRPC plugin sets grpc.status.code as a numeric integer via
span.setTag, which span_format.js routes into span.metrics rather than
span.meta. SpanAggKey was reading meta only, so rpcStatusCode was always
empty for real gRPC spans.

Now falls back to span.metrics[GRPC_STATUS_CODE] and translates the
integer to the canonical status name (OK, NOT_FOUND, etc.) using the
gRPC status code table. Meta string takes priority when both are present.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@mabdinur

mabdinur commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 74aa08110f

ℹ️ 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".

Comment thread packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js Outdated
@mabdinur

mabdinur commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@codex review

…LP metrics

When a bucket contained both top-level and measured non-top-level spans,
the heuristic (topLevelHits === hits) always resolved to false, causing
the OTLP histogram to be emitted as datadog.span.top_level=false and
dropping top-level traffic from APM metrics.

Adding topLevel as a dimension to SpanAggKey causes top-level and
non-top-level spans to bucket separately. Each bucket is now always
purely top-level or purely non-top-level, so the attribute is always
accurate. The native stats path is unaffected because toJSON() omits
topLevel; the Agent merges groups with identical key fields, preserving
the same Hits/TopLevelHits totals.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 74aa08110f

ℹ️ 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".

Comment thread packages/dd-trace/src/config/index.js
@mabdinur

mabdinur commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1fc02ff129

ℹ️ 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".

Comment thread packages/dd-trace/src/span_stats.js Outdated
@mabdinur

mabdinur commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1fc02ff129

ℹ️ 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".

Comment thread packages/dd-trace/src/span_stats.js Outdated
mabdinur and others added 5 commits July 1, 2026 15:10
…tats regression

The previous fix added topLevel to SpanAggKey to separate top-level and
non-top-level spans into distinct buckets. This created a real regression
in the native /v0.6/stats path: the Agent's mergeDuplicates() correctly
sums Hits/Errors/Duration from duplicate rows but silently drops
TopLevelHits from the merged-away entry. If the non-top-level row is
processed first and becomes the canonical, TopLevelHits from the
top-level row is lost.

Fix: revert topLevel from SpanAggKey (no more duplicate rows). Instead,
split SpanAggStats into four distributions (topLevelOk, topLevelError,
nonTopLevelOk, nonTopLevelError). The native stats path merges them at
export time so toJSON() produces the same combined OkSummary/ErrorSummary
as before. The OTLP path emits separate data points per top-level status
with the correct datadog.span.top_level attribute. OTel-semantics mode
merges the distributions (no top-level attribute to distinguish them).

Also adds SpanKind, Origin, and RpcStatusCode to the native stats
toJSON() payload so the Agent receives these new dimensions.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
… use GRPCStatusCode key

toJSON() now returns an array of up to 2 rows (top-level row first, non-top-level
row second). #toLegacyPayload uses flatMap to flatten them. This eliminates the
merge-time DDSketch allocation and ensures TopLevelHits is always non-zero on
the top-level row, so the Agent's mergeDuplicates retains it as the canonical entry.

Duration and Errors are derived from distribution .sum/.count, removing the
redundant this.duration and this.errors accumulators.

GRPCStatusCode matches the agent's msgpack decoder key (confirmed from
pkg/proto/pbgo/trace/stats_gen.go).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…nstants

- Remove descriptive/narrating comments throughout; keep only non-obvious constraints
- Move GRPC_STATUS_NAMES from span_stats.js into constants.js
- Rename #toLegacyPayload -> #v06Payload
- Remove section comment from ext/tags.js

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
… origin from aggregation key

ORIGIN_KEY now only populates SpanAggKey.synthetics. The origin string field
is removed from SpanAggKey, toString(), and the v0.6 payload (Origin is not
a field the agent decodes). In the OTLP path, datadog.origin='synthetics' is
emitted when aggKey.synthetics is true.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@gh-worker-ownership-write-b05516
gh-worker-ownership-write-b05516 Bot removed the request for review from a team July 1, 2026 19:45
@mabdinur

mabdinur commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa76c17d9e

ℹ️ 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".

@mabdinur

mabdinur commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa76c17d9e

ℹ️ 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".

Comment thread packages/dd-trace/src/span_stats.js
@mabdinur
mabdinur merged commit 8750f7f into master Jul 2, 2026
650 checks passed
@mabdinur
mabdinur deleted the ida613/otlp-trace-metrics-poc branch July 2, 2026 14:51
@dd-octo-sts dd-octo-sts Bot mentioned this pull request Jul 2, 2026
dd-octo-sts Bot pushed a commit that referenced this pull request Jul 2, 2026
* feat(span-stats): add OTLP metrics export for span stats

Export client-computed span stats as OTLP metrics (dd.trace.span.hits,
dd.trace.span.errors, dd.trace.span.top_level_hits, dd.trace.span.duration)
via a new OtlpStatsExporter alongside the existing Datadog /v0.6/stats
exporter.

Enabled via DD_TRACE_OTEL_METRICS_ENABLED=true, or auto-enabled when both
OTEL_TRACES_EXPORTER=otlp and OTEL_METRICS_EXPORTER=otlp are set. URL and
protocol are derived from the OTLP trace export configuration.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* test(config): register traceMetrics as internal runtime property

traceMetrics is a computed aggregate derived from OTEL_TRACES_EXPORTER,
OTEL_METRICS_EXPORTER, and DD_TRACE_OTEL_METRICS_ENABLED — not a raw
user-facing key — so it belongs in INTERNAL_RUNTIME_PROPERTIES alongside
sampler and stableConfig.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): guard traceMetrics URL derivation against invalid OTLP endpoint

When hostname is an unbracketed IPv6 address (e.g. ::1), the defaultOtlpBase
is http://::1:4318 which is not a valid URL. The new URL() call in the
traceMetrics block was the first code path to actually parse the string,
causing a TypeError that crashed config construction.

Wrap the URL derivation in a try/catch so that a malformed traces endpoint
falls back to the localhost default without throwing.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore(config): regenerate config types for DD_TRACE_OTEL_METRICS_ENABLED

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): add configurationNames to DD_TRACE_OTEL_METRICS_ENABLED entry

The eslint-config-names-sync rule verifies that every leaf property in
TracerOptions (index.d.ts) has a matching configurationNames entry in
supported-configurations.json. The entry for DD_TRACE_OTEL_METRICS_ENABLED
only had internalPropertyName, which is not checked by the rule.

Adding configurationNames: ["traceMetricsEnabled"] ties the two files
together and satisfies the lint check. Regenerated config types to match.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* bug fix

* update post RFC discussion

* bring implementation bug fix

* clean how cinfgs are set

* feat(otlp-trace-metrics): align span-stats export with the trace-metrics contract

Update the OTLP trace-metrics export to match the agreed RFC/system-test contract:

- Rename the enablement env var to OTEL_CLIENT_STATS_COMPUTATION_ENABLED and add
  DD_TRACE_OTEL_SEMANTICS_ENABLED (OTel-semantics mode: emit only OTel attributes, no dd.*).
- Emit a single histogram named traces.span.sdk.metrics.duration.
- Map dimensions to OTel attributes (span.name, span.kind, http.*, rpc.* from grpc tags) and
  convey errors via OTel status.code; default mode also adds dd.operation.name, dd.span.type,
  dd.origin and dd.span.top_level.
- Add telemetry.sdk.{name,language,version} resource attributes and emit process tags as dd.<key>
  (default mode only); gate all dd.* resource attributes behind default mode.
- Drive the flush/export cadence from OTEL_METRIC_EXPORT_INTERVAL and drop the
  _DD_TRACE_STATS_WRITER_INTERVAL override.
- Read grpc.status.code from span.metrics (numeric) with a meta fallback.

Update unit tests accordingly and regenerate config types.

Co-authored-by: Cursor <[email protected]>

* feat(otlp-trace-metrics): report service identity per InstrumentationScope

Partition span-stats data points by service so one OTLP payload can carry
multiple services, each as its own InstrumentationScope with service.name,
service.version and deployment.environment.name. These move off the resource,
which now only carries SDK identity, host.name and dd.* attributes.

Fix the trace-metrics flush cadence at 10s (no longer driven by
OTEL_METRIC_EXPORT_INTERVAL); the internal _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL
overrides it in tests only.

Co-authored-by: Cursor <[email protected]>

* fix(otlp-trace-metrics): apply internal flush interval override

The generic env applier only reads DD_/OTEL_ prefixed vars, so the
internal _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL (which starts with _DD_)
was never wired into config. Read it explicitly so the test-only flush
cadence override takes effect.

Co-authored-by: Cursor <[email protected]>

* feat(otlp-span-stats): emit fixed explicit-bounds histogram from the DDSketch

Derive the OTLP duration histogram from each group's DDSketch into the
spanmetrics-connector default bounds (in seconds), and drop the duplicate
exact-cell accumulator in span_stats. Each group now emits at most two data
points (ok/error) with a per-group dd.span.top_level heuristic, mirroring
libdatadog.

Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): carry service identity as resource attributes

Move service.name/service.version/deployment.environment.name onto the OTLP
resource (the configured default service), emit a single InstrumentationScope,
and add service.name as a data-point attribute only when a span's service
differs from the configured default. Thread DD_SERVICE through the processor so
the transformer can compare against it.

Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): drop redundant dd-trace InstrumentationScope

The exported OTLP metrics no longer carry an InstrumentationScope: a `dd-trace`
scope (name/version) is redundant with the resource's telemetry.sdk.* attributes.
The single scopeMetrics omits the scope field.

Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): datadog.* attribute prefix and OTEL_TRACES_SPAN_METRICS_ENABLED

Rename the OTLP trace-metric attributes from dd.* to datadog.* (operation.name,
span.type, span.top_level, origin, runtime_id, datadog.<process tags>) and rename
the enablement env var OTEL_CLIENT_STATS_COMPUTATION_ENABLED ->
OTEL_TRACES_SPAN_METRICS_ENABLED.

Co-authored-by: Cursor <[email protected]>

* feat(otlp): set _dd.stats_computed resource attribute on OTLP traces when trace metrics enabled

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(span-stats): use timer.unref?.() for Electron compatibility

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(rebase): restore extractRootTags; rename traceMetricsEnabled; add otelSemanticsEnabled to types

span_format.js: rebase conflict resolved to branch's addTag refactor which no longer exists in
master — revert to explicit typeof checks while keeping the FR06.3 BUG comment.

index.d.ts: rename traceMetricsEnabled -> otlpTraceMetricsEnabled to match supported-configurations.json;
add otelSemanticsEnabled (DD_TRACE_OTEL_SEMANTICS_ENABLED). Fixes eslint-config-names-sync errors.

Regenerate generated-config-types.d.ts from updated inputs.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(span-stats): wire OTLP metrics endpoint/protocol and trim dead code

The crash fix: SpanStatsProcessor read config.otelMetricsUrl/otelMetricsProtocol,
which Config never set (dropped in 40014ae), so `new URL(undefined)` threw
ERR_INVALID_URL and crashed tracer init whenever OTLP trace metrics were enabled.
Read the canonical OTEL_EXPORTER_OTLP_METRICS_ENDPOINT/OTEL_EXPORTER_OTLP_METRICS_PROTOCOL
directly instead of introducing redundant alias properties.

Also fix the dead auto-enable check: `this.otelMetricsEnabled` does not exist
(the property is DD_METRICS_OTEL_ENABLED), so `undefined === true` made the
"auto-enable when OTLP traces + OTEL metrics are on" path never trigger.

Minimize the diff vs master without changing behavior:
- drop 4 unused SpanAggStats fields (errorDuration/topLevel*) and their test
- collapse the duplicate JSON/protobuf transformer methods into transform()
- remove two `// BUG` WIP narration comments (reverts the comment-only
  span_format.js hunk; tracked separately)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): privatize internals, trim telemetry, simplify transformer

- Make _drainBuckets / _toLegacyPayload true private (#) — neither
  crosses the class boundary; _ prefix implied false publicness
- Guard SpanStatsExporter construction behind !otlpTraceMetricsEnabled
  so it is never instantiated when the OTLP path is active
- Replace #errorStatus() / #boolAttr() one-shot methods with inline
  literals and a module-level ERROR_STATUS_ATTR constant to avoid
  per-call allocations
- sketchToFixedHistogram now returns number[] directly; #pushPoint
  references EXPLICIT_BOUNDS_SECONDS from the module constant
- Remove this.recordTelemetry calls from OtlpStatsExporter.export —
  not part of the OTLP trace-metrics spec
- Rewrite whitebox _drainBuckets test as blackbox: assert buckets are
  empty after onInterval() instead of calling the private method

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix configs

Co-authored-by: Munir Abdinur <[email protected]>

* chore: regenerate config types after supported-configurations.json update

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): use otelSemanticsEnabled config key instead of DD_TRACE_OTEL_SEMANTICS_ENABLED

Our branch maps DD_TRACE_OTEL_SEMANTICS_ENABLED to the internal property
otelSemanticsEnabled via supported-configurations.json internalPropertyName.
The merged master code was reading config.DD_TRACE_OTEL_SEMANTICS_ENABLED
directly, which was undefined in our config layout.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(test): update span.spec.js to use otelSemanticsEnabled config key

The test was setting config.DD_TRACE_OTEL_SEMANTICS_ENABLED but span.js
now reads config.otelSemanticsEnabled (the internal property name mapped
from DD_TRACE_OTEL_SEMANTICS_ENABLED via supported-configurations.json).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): use string default for _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL

Schema requires default to be a string or null, not a number literal.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): update description for _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL to match registry

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): use short description for _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL

The description field maps to Short Description in the config registry.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): remove description field from _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL

No other int entry with allowed field uses description; may be mutually exclusive in schema.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(otlp-span-stats): address Codex review comments

- Use dd-trace VERSION (not app version) for telemetry.sdk.version resource attribute
- Pass OTEL_EXPORTER_OTLP_METRICS_HEADERS and OTEL_EXPORTER_OTLP_METRICS_TIMEOUT
  to OtlpStatsExporter so authenticated/custom endpoints work correctly
- Fix index.d.ts doc: env var is OTEL_TRACES_SPAN_METRICS_ENABLED and
  auto-enable condition is DD_METRICS_OTEL_ENABLED (not OTEL_METRICS_EXPORTER=otlp)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(lint): fix import order and no-useless-undefined in span_stats and otlp-span-stats

- Move ../../../version import before ./constants to satisfy import/order rule
- Remove explicit = undefined default for headers param (unicorn/no-useless-undefined)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* test(config): cover _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL override branch

Adds test that exercises the setAndTrack call inside the conditional
that reads the internal flush interval override from the environment.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): align DD_TRACE_OTEL_SEMANTICS_ENABLED with registry definition

The config registry has this entry as a plain boolean with default "false"
and no internalPropertyName. Revert our custom mapping so the entry matches
the registry exactly — the validator compares against the registered definition.

All code that previously accessed config.otelSemanticsEnabled now reads
config.DD_TRACE_OTEL_SEMANTICS_ENABLED directly; the destructuring alias
in span_stats.js preserves the otelSemanticsEnabled local variable name.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(lint): add configurationNames for otelSemanticsEnabled to satisfy eslint-config-names-sync

The rule requires that every option name in index.d.ts has a corresponding
entry in supported-configurations.json (as a key, configurationNames value,
or internalPropertyName). Adding configurationNames: ["otelSemanticsEnabled"]
to DD_TRACE_OTEL_SEMANTICS_ENABLED satisfies this while keeping default: "false"
to match the registry. The generator uses configurationNames[0] as the config
key, so code reverts to config.otelSemanticsEnabled.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(config): remove internalPropertyName and unnecessary configurationNames

Per reviewer feedback:
- Remove internalPropertyName from _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL; the flush
  interval is read directly via getEnvironmentVariable() in #applyCalculated
- Remove configurationNames/internalPropertyName from OTEL_TRACES_SPAN_METRICS_ENABLED
  and drop otlpTraceMetricsEnabled as a programmatic option from index.d.ts; use
  this.OTEL_TRACES_SPAN_METRICS_ENABLED directly in #applyCalculated instead
- Remove configurationNames from DD_TRACE_OTEL_SEMANTICS_ENABLED and drop
  otelSemanticsEnabled as a programmatic option from index.d.ts; all callers
  now read config.DD_TRACE_OTEL_SEMANTICS_ENABLED directly

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): resolve Unknown Config properties for otlpTraceMetricsEnabled/ddTraceMetricsOtelFlushInterval

- Map setAndTrack to OTEL_TRACES_SPAN_METRICS_ENABLED (a declared config key)
  instead of the undeclared otlpTraceMetricsEnabled alias; all call sites
  updated to read config.OTEL_TRACES_SPAN_METRICS_ENABLED directly
- Move _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL reading from #applyCalculated
  into span_stats.js; import getEnvironmentVariable there directly — removes
  the undeclared ddTraceMetricsOtelFlushInterval setAndTrack write
- Update tests to use the env-var key names and remove the now-irrelevant
  config override test

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(lint): remove blank line before closing brace in config spec

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* feat(config): generate GeneratedEnvVarConfig interface for env var types

- Extract getBaseType helper from getTypeForEntry to share base type computation
- Add getEnvVarType that only adds undefined when there is no registered default
- Add generateEnvVarConfigTypes to map every env var name (canonical + aliases) to its resolved type
- Append GeneratedEnvVarConfig interface to generated-config-types.d.ts

Rationale: Callers of getValueFromEnvSources need per-env-var typed return values instead of the full config property union, enabling type-safe lookups by literal env var name.

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* fix(span-stats): address PR review comments

- Gate OTLP-only SpanAggKey dimensions (origin, spanKind, rpcMethod,
  rpcStatusCode) on otlpTraceMetricsEnabled to avoid inflating legacy
  span stats aggregation key cardinality
- Thread _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL through the typed config
  system (via setAndTrack/getValueFromEnvSources) instead of reading
  the raw env var directly in SpanStatsProcessor
- Add config tests covering OTEL_TRACES_SPAN_METRICS_ENABLED auto-enable
  logic (both conditions, explicit override)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(span-stats): pass otlpEnabled=true in transformer test bucket helper

makeBucket is used exclusively by OtlpStatsTransformer tests, so spans
must be keyed with otlpEnabled=true to populate the OTLP-gated fields
(origin, spanKind, rpcMethod, rpcStatusCode).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* style(span-stats): minor style and readability cleanups

- Extract flush interval to variable before setAndTrack call
- Remove unnecessary quotes on property key
- Tighten test description wording

Rationale: Small consistency and readability improvements from PR review

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* fix(span-stats): remove rpc.method from otlp span stats aggregation key

- Drop grpc.method.name from SpanAggKey and OtlpStatsTransformer
- rpc.method inflates aggregation key cardinality without sufficient benefit
- Update all affected test assertions

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* refactor(span-stats): remove stale comments and clarify TODO

- Remove redundant inline comments in otlp-span-stats transformer
- Replace misleading comment about OTLP-only dimensions with a TODO
  noting origin and spanKind should eventually be included in legacy
  client stats aggregation

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* Apply suggestion from @mabdinur

* refactor(span-stats): remove redundant inline comments

- Drop comments that restate what the code already shows
- Keep code self-documenting per project style guidelines

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* fix(opentelemetry): fix max-len lint violation in span_processor.js

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): fix max-len lint violation in span_processor.js

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore: add otlp-span-stats exporter to CODEOWNERS

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(opentelemetry): encapsulate OTLP span stats in opentelemetry/metrics

Move OtlpStatsExporter and OtlpStatsTransformer into opentelemetry/metrics/
so the opentelemetry package is self-contained ahead of potential extraction
into its own npm package.

Key changes:
- Move exporters/otlp-span-stats/{index,transformer}.js to
  opentelemetry/metrics/otlp_span_stats_{exporter,transformer}.js
- Move buildResourceAttributes from span_stats.js to opentelemetry/metrics/index.js;
  add createOtlpSpanStatsExporter factory there
- Wire OtlpStatsExporter via DI: opentracing/tracer.js creates it when
  OTEL_TRACES_SPAN_METRICS_ENABLED and passes it through SpanProcessor to
  SpanStatsProcessor — span_stats.js no longer imports from opentelemetry/
- config/index.js mirrors OTEL_TRACES_SPAN_METRICS_ENABLED into
  stats.DD_TRACE_STATS_COMPUTATION_ENABLED so downstream checks are unified
- Remove otlpEnabled flag from SpanAggKey/SpanBuckets — origin, spanKind,
  rpcStatusCode are always populated
- Remove OTEL-specific check from AgentExporter (relies on mirrored flag)
- Remove CODEOWNERS entry for deleted exporters/otlp-span-stats/ path
- Move tests to test/opentelemetry/metrics/

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): align grpc stats with libdatadog and enforce mutual exclusion

- Move GRPC_STATUS_CODE constant to ext/tags.js
- Emit rpc.response.status_code as string (aligns with libdatadog kv_str)
- Use else if in onInterval to make native and OTLP export mutually exclusive

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(opentelemetry): trim JSDoc, privatize transformer, drop empty description

- Make OtlpStatsExporter#transformer a private field (#transformer)
- Remove empty description field from histogram metric
- Trim redundant @param prose in exporter and transformer JSDoc
- Use GRPC_STATUS_CODE import in transformer spec instead of string literal

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): suppress self-instrumentation spans from OTLP exporter requests

Wrap sendPayload's HTTP request in legacyStorage.run({ noop: true }) so the
tracer does not instrument its own outbound connections to the OTLP collector.
Without this, tcp.connect client spans for /v1/metrics requests were fed into
the traces.span.sdk.metrics.duration histogram, displacing real span data points
and inflating counts in the system-tests parametric suite.

Same pattern used by exporters/common/request.js and exporters/common/agents.js.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* test(opentelemetry): improve patch coverage for span stats OTLP export

- Inline URL parsing into OtlpHttpExporterBase constructor; remove setUrl
  (the if(telemetryTags !== undefined) branch was dead — telemetryTags is
  always undefined when setUrl was called from the constructor, and no
  external caller ever invoked it post-construction)
- Add tests for buildResourceAttributes (sdk identity, runtime-id, OTel-
  semantics mode) and createOtlpSpanStatsExporter
- Add tests for HTTP error response and request error paths in sendPayload

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* feat(opentelemetry): emit raw grpc.status.code name for rpc.response.status_code

Prefer the meta status NAME string over the numeric metrics tag and emit it
upper-cased to rpc.response.status_code, aligning with the OTel gRPC semantic
conventions (canonical status name) without any code<->name mapping.

* fix(opentelemetry): restore setUrl method removed by dead-code cleanup

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore(opentelemetry): trim redundant comments in grpc status mapping

Co-authored-by: Cursor <[email protected]>

* refactor(opentelemetry): read grpc.status.code from meta only

Drop the numeric metrics fallback; the gRPC status code is the canonical status
NAME and is read from span meta.

Co-authored-by: Cursor <[email protected]>

* fix(opentelemetry): translate numeric grpc.status.code from metrics to status name

The dd gRPC plugin sets grpc.status.code as a numeric integer via
span.setTag, which span_format.js routes into span.metrics rather than
span.meta. SpanAggKey was reading meta only, so rpcStatusCode was always
empty for real gRPC spans.

Now falls back to span.metrics[GRPC_STATUS_CODE] and translates the
integer to the canonical status name (OK, NOT_FOUND, etc.) using the
gRPC status code table. Meta string takes priority when both are present.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): split agg key by top-level to fix mixed-bucket OTLP metrics

When a bucket contained both top-level and measured non-top-level spans,
the heuristic (topLevelHits === hits) always resolved to false, causing
the OTLP histogram to be emitted as datadog.span.top_level=false and
dropping top-level traffic from APM metrics.

Adding topLevel as a dimension to SpanAggKey causes top-level and
non-top-level spans to bucket separately. Each bucket is now always
purely top-level or purely non-top-level, so the attribute is always
accurate. The native stats path is unaffected because toJSON() omits
topLevel; the Agent merges groups with identical key fields, preserving
the same Hits/TopLevelHits totals.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): track per-top-level distributions to fix native stats regression

The previous fix added topLevel to SpanAggKey to separate top-level and
non-top-level spans into distinct buckets. This created a real regression
in the native /v0.6/stats path: the Agent's mergeDuplicates() correctly
sums Hits/Errors/Duration from duplicate rows but silently drops
TopLevelHits from the merged-away entry. If the non-top-level row is
processed first and becomes the canonical, TopLevelHits from the
top-level row is lost.

Fix: revert topLevel from SpanAggKey (no more duplicate rows). Instead,
split SpanAggStats into four distributions (topLevelOk, topLevelError,
nonTopLevelOk, nonTopLevelError). The native stats path merges them at
export time so toJSON() produces the same combined OkSummary/ErrorSummary
as before. The OTLP path emits separate data points per top-level status
with the correct datadog.span.top_level attribute. OTel-semantics mode
merges the distributions (no top-level attribute to distinguish them).

Also adds SpanKind, Origin, and RpcStatusCode to the native stats
toJSON() payload so the Agent receives these new dimensions.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(opentelemetry): split native stats rows by top-level status; use GRPCStatusCode key

toJSON() now returns an array of up to 2 rows (top-level row first, non-top-level
row second). #toLegacyPayload uses flatMap to flatten them. This eliminates the
merge-time DDSketch allocation and ensures TopLevelHits is always non-zero on
the top-level row, so the Agent's mergeDuplicates retains it as the canonical entry.

Duration and Errors are derived from distribution .sum/.count, removing the
redundant this.duration and this.errors accumulators.

GRPCStatusCode matches the agent's msgpack decoder key (confirmed from
pkg/proto/pbgo/trace/stats_gen.go).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore(opentelemetry): minimize comments, move GRPC_STATUS_NAMES to constants

- Remove descriptive/narrating comments throughout; keep only non-obvious constraints
- Move GRPC_STATUS_NAMES from span_stats.js into constants.js
- Rename #toLegacyPayload -> #v06Payload
- Remove section comment from ext/tags.js

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): restrict ORIGIN_KEY to synthetics boolean; remove origin from aggregation key

ORIGIN_KEY now only populates SpanAggKey.synthetics. The origin string field
is removed from SpanAggKey, toString(), and the v0.6 payload (Origin is not
a field the agent decodes). In the OTLP path, datadog.origin='synthetics' is
emitted when aggKey.synthetics is true.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore: remove pr_description.md

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Munir <[email protected]>
Co-authored-by: Cursor <[email protected]>
@dd-octo-sts dd-octo-sts Bot mentioned this pull request Jul 2, 2026
rochdev pushed a commit that referenced this pull request Jul 2, 2026
* feat(span-stats): add OTLP metrics export for span stats

Export client-computed span stats as OTLP metrics (dd.trace.span.hits,
dd.trace.span.errors, dd.trace.span.top_level_hits, dd.trace.span.duration)
via a new OtlpStatsExporter alongside the existing Datadog /v0.6/stats
exporter.

Enabled via DD_TRACE_OTEL_METRICS_ENABLED=true, or auto-enabled when both
OTEL_TRACES_EXPORTER=otlp and OTEL_METRICS_EXPORTER=otlp are set. URL and
protocol are derived from the OTLP trace export configuration.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* test(config): register traceMetrics as internal runtime property

traceMetrics is a computed aggregate derived from OTEL_TRACES_EXPORTER,
OTEL_METRICS_EXPORTER, and DD_TRACE_OTEL_METRICS_ENABLED — not a raw
user-facing key — so it belongs in INTERNAL_RUNTIME_PROPERTIES alongside
sampler and stableConfig.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): guard traceMetrics URL derivation against invalid OTLP endpoint

When hostname is an unbracketed IPv6 address (e.g. ::1), the defaultOtlpBase
is http://::1:4318 which is not a valid URL. The new URL() call in the
traceMetrics block was the first code path to actually parse the string,
causing a TypeError that crashed config construction.

Wrap the URL derivation in a try/catch so that a malformed traces endpoint
falls back to the localhost default without throwing.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore(config): regenerate config types for DD_TRACE_OTEL_METRICS_ENABLED

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): add configurationNames to DD_TRACE_OTEL_METRICS_ENABLED entry

The eslint-config-names-sync rule verifies that every leaf property in
TracerOptions (index.d.ts) has a matching configurationNames entry in
supported-configurations.json. The entry for DD_TRACE_OTEL_METRICS_ENABLED
only had internalPropertyName, which is not checked by the rule.

Adding configurationNames: ["traceMetricsEnabled"] ties the two files
together and satisfies the lint check. Regenerated config types to match.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* bug fix

* update post RFC discussion

* bring implementation bug fix

* clean how cinfgs are set

* feat(otlp-trace-metrics): align span-stats export with the trace-metrics contract

Update the OTLP trace-metrics export to match the agreed RFC/system-test contract:

- Rename the enablement env var to OTEL_CLIENT_STATS_COMPUTATION_ENABLED and add
  DD_TRACE_OTEL_SEMANTICS_ENABLED (OTel-semantics mode: emit only OTel attributes, no dd.*).
- Emit a single histogram named traces.span.sdk.metrics.duration.
- Map dimensions to OTel attributes (span.name, span.kind, http.*, rpc.* from grpc tags) and
  convey errors via OTel status.code; default mode also adds dd.operation.name, dd.span.type,
  dd.origin and dd.span.top_level.
- Add telemetry.sdk.{name,language,version} resource attributes and emit process tags as dd.<key>
  (default mode only); gate all dd.* resource attributes behind default mode.
- Drive the flush/export cadence from OTEL_METRIC_EXPORT_INTERVAL and drop the
  _DD_TRACE_STATS_WRITER_INTERVAL override.
- Read grpc.status.code from span.metrics (numeric) with a meta fallback.

Update unit tests accordingly and regenerate config types.

Co-authored-by: Cursor <[email protected]>

* feat(otlp-trace-metrics): report service identity per InstrumentationScope

Partition span-stats data points by service so one OTLP payload can carry
multiple services, each as its own InstrumentationScope with service.name,
service.version and deployment.environment.name. These move off the resource,
which now only carries SDK identity, host.name and dd.* attributes.

Fix the trace-metrics flush cadence at 10s (no longer driven by
OTEL_METRIC_EXPORT_INTERVAL); the internal _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL
overrides it in tests only.

Co-authored-by: Cursor <[email protected]>

* fix(otlp-trace-metrics): apply internal flush interval override

The generic env applier only reads DD_/OTEL_ prefixed vars, so the
internal _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL (which starts with _DD_)
was never wired into config. Read it explicitly so the test-only flush
cadence override takes effect.

Co-authored-by: Cursor <[email protected]>

* feat(otlp-span-stats): emit fixed explicit-bounds histogram from the DDSketch

Derive the OTLP duration histogram from each group's DDSketch into the
spanmetrics-connector default bounds (in seconds), and drop the duplicate
exact-cell accumulator in span_stats. Each group now emits at most two data
points (ok/error) with a per-group dd.span.top_level heuristic, mirroring
libdatadog.

Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): carry service identity as resource attributes

Move service.name/service.version/deployment.environment.name onto the OTLP
resource (the configured default service), emit a single InstrumentationScope,
and add service.name as a data-point attribute only when a span's service
differs from the configured default. Thread DD_SERVICE through the processor so
the transformer can compare against it.

Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): drop redundant dd-trace InstrumentationScope

The exported OTLP metrics no longer carry an InstrumentationScope: a `dd-trace`
scope (name/version) is redundant with the resource's telemetry.sdk.* attributes.
The single scopeMetrics omits the scope field.

Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): datadog.* attribute prefix and OTEL_TRACES_SPAN_METRICS_ENABLED

Rename the OTLP trace-metric attributes from dd.* to datadog.* (operation.name,
span.type, span.top_level, origin, runtime_id, datadog.<process tags>) and rename
the enablement env var OTEL_CLIENT_STATS_COMPUTATION_ENABLED ->
OTEL_TRACES_SPAN_METRICS_ENABLED.

Co-authored-by: Cursor <[email protected]>

* feat(otlp): set _dd.stats_computed resource attribute on OTLP traces when trace metrics enabled

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(span-stats): use timer.unref?.() for Electron compatibility

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(rebase): restore extractRootTags; rename traceMetricsEnabled; add otelSemanticsEnabled to types

span_format.js: rebase conflict resolved to branch's addTag refactor which no longer exists in
master — revert to explicit typeof checks while keeping the FR06.3 BUG comment.

index.d.ts: rename traceMetricsEnabled -> otlpTraceMetricsEnabled to match supported-configurations.json;
add otelSemanticsEnabled (DD_TRACE_OTEL_SEMANTICS_ENABLED). Fixes eslint-config-names-sync errors.

Regenerate generated-config-types.d.ts from updated inputs.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(span-stats): wire OTLP metrics endpoint/protocol and trim dead code

The crash fix: SpanStatsProcessor read config.otelMetricsUrl/otelMetricsProtocol,
which Config never set (dropped in 40014ae), so `new URL(undefined)` threw
ERR_INVALID_URL and crashed tracer init whenever OTLP trace metrics were enabled.
Read the canonical OTEL_EXPORTER_OTLP_METRICS_ENDPOINT/OTEL_EXPORTER_OTLP_METRICS_PROTOCOL
directly instead of introducing redundant alias properties.

Also fix the dead auto-enable check: `this.otelMetricsEnabled` does not exist
(the property is DD_METRICS_OTEL_ENABLED), so `undefined === true` made the
"auto-enable when OTLP traces + OTEL metrics are on" path never trigger.

Minimize the diff vs master without changing behavior:
- drop 4 unused SpanAggStats fields (errorDuration/topLevel*) and their test
- collapse the duplicate JSON/protobuf transformer methods into transform()
- remove two `// BUG` WIP narration comments (reverts the comment-only
  span_format.js hunk; tracked separately)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): privatize internals, trim telemetry, simplify transformer

- Make _drainBuckets / _toLegacyPayload true private (#) — neither
  crosses the class boundary; _ prefix implied false publicness
- Guard SpanStatsExporter construction behind !otlpTraceMetricsEnabled
  so it is never instantiated when the OTLP path is active
- Replace #errorStatus() / #boolAttr() one-shot methods with inline
  literals and a module-level ERROR_STATUS_ATTR constant to avoid
  per-call allocations
- sketchToFixedHistogram now returns number[] directly; #pushPoint
  references EXPLICIT_BOUNDS_SECONDS from the module constant
- Remove this.recordTelemetry calls from OtlpStatsExporter.export —
  not part of the OTLP trace-metrics spec
- Rewrite whitebox _drainBuckets test as blackbox: assert buckets are
  empty after onInterval() instead of calling the private method

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix configs

Co-authored-by: Munir Abdinur <[email protected]>

* chore: regenerate config types after supported-configurations.json update

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): use otelSemanticsEnabled config key instead of DD_TRACE_OTEL_SEMANTICS_ENABLED

Our branch maps DD_TRACE_OTEL_SEMANTICS_ENABLED to the internal property
otelSemanticsEnabled via supported-configurations.json internalPropertyName.
The merged master code was reading config.DD_TRACE_OTEL_SEMANTICS_ENABLED
directly, which was undefined in our config layout.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(test): update span.spec.js to use otelSemanticsEnabled config key

The test was setting config.DD_TRACE_OTEL_SEMANTICS_ENABLED but span.js
now reads config.otelSemanticsEnabled (the internal property name mapped
from DD_TRACE_OTEL_SEMANTICS_ENABLED via supported-configurations.json).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): use string default for _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL

Schema requires default to be a string or null, not a number literal.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): update description for _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL to match registry

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): use short description for _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL

The description field maps to Short Description in the config registry.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): remove description field from _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL

No other int entry with allowed field uses description; may be mutually exclusive in schema.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(otlp-span-stats): address Codex review comments

- Use dd-trace VERSION (not app version) for telemetry.sdk.version resource attribute
- Pass OTEL_EXPORTER_OTLP_METRICS_HEADERS and OTEL_EXPORTER_OTLP_METRICS_TIMEOUT
  to OtlpStatsExporter so authenticated/custom endpoints work correctly
- Fix index.d.ts doc: env var is OTEL_TRACES_SPAN_METRICS_ENABLED and
  auto-enable condition is DD_METRICS_OTEL_ENABLED (not OTEL_METRICS_EXPORTER=otlp)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(lint): fix import order and no-useless-undefined in span_stats and otlp-span-stats

- Move ../../../version import before ./constants to satisfy import/order rule
- Remove explicit = undefined default for headers param (unicorn/no-useless-undefined)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* test(config): cover _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL override branch

Adds test that exercises the setAndTrack call inside the conditional
that reads the internal flush interval override from the environment.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): align DD_TRACE_OTEL_SEMANTICS_ENABLED with registry definition

The config registry has this entry as a plain boolean with default "false"
and no internalPropertyName. Revert our custom mapping so the entry matches
the registry exactly — the validator compares against the registered definition.

All code that previously accessed config.otelSemanticsEnabled now reads
config.DD_TRACE_OTEL_SEMANTICS_ENABLED directly; the destructuring alias
in span_stats.js preserves the otelSemanticsEnabled local variable name.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(lint): add configurationNames for otelSemanticsEnabled to satisfy eslint-config-names-sync

The rule requires that every option name in index.d.ts has a corresponding
entry in supported-configurations.json (as a key, configurationNames value,
or internalPropertyName). Adding configurationNames: ["otelSemanticsEnabled"]
to DD_TRACE_OTEL_SEMANTICS_ENABLED satisfies this while keeping default: "false"
to match the registry. The generator uses configurationNames[0] as the config
key, so code reverts to config.otelSemanticsEnabled.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(config): remove internalPropertyName and unnecessary configurationNames

Per reviewer feedback:
- Remove internalPropertyName from _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL; the flush
  interval is read directly via getEnvironmentVariable() in #applyCalculated
- Remove configurationNames/internalPropertyName from OTEL_TRACES_SPAN_METRICS_ENABLED
  and drop otlpTraceMetricsEnabled as a programmatic option from index.d.ts; use
  this.OTEL_TRACES_SPAN_METRICS_ENABLED directly in #applyCalculated instead
- Remove configurationNames from DD_TRACE_OTEL_SEMANTICS_ENABLED and drop
  otelSemanticsEnabled as a programmatic option from index.d.ts; all callers
  now read config.DD_TRACE_OTEL_SEMANTICS_ENABLED directly

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): resolve Unknown Config properties for otlpTraceMetricsEnabled/ddTraceMetricsOtelFlushInterval

- Map setAndTrack to OTEL_TRACES_SPAN_METRICS_ENABLED (a declared config key)
  instead of the undeclared otlpTraceMetricsEnabled alias; all call sites
  updated to read config.OTEL_TRACES_SPAN_METRICS_ENABLED directly
- Move _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL reading from #applyCalculated
  into span_stats.js; import getEnvironmentVariable there directly — removes
  the undeclared ddTraceMetricsOtelFlushInterval setAndTrack write
- Update tests to use the env-var key names and remove the now-irrelevant
  config override test

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(lint): remove blank line before closing brace in config spec

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* feat(config): generate GeneratedEnvVarConfig interface for env var types

- Extract getBaseType helper from getTypeForEntry to share base type computation
- Add getEnvVarType that only adds undefined when there is no registered default
- Add generateEnvVarConfigTypes to map every env var name (canonical + aliases) to its resolved type
- Append GeneratedEnvVarConfig interface to generated-config-types.d.ts

Rationale: Callers of getValueFromEnvSources need per-env-var typed return values instead of the full config property union, enabling type-safe lookups by literal env var name.

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* fix(span-stats): address PR review comments

- Gate OTLP-only SpanAggKey dimensions (origin, spanKind, rpcMethod,
  rpcStatusCode) on otlpTraceMetricsEnabled to avoid inflating legacy
  span stats aggregation key cardinality
- Thread _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL through the typed config
  system (via setAndTrack/getValueFromEnvSources) instead of reading
  the raw env var directly in SpanStatsProcessor
- Add config tests covering OTEL_TRACES_SPAN_METRICS_ENABLED auto-enable
  logic (both conditions, explicit override)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(span-stats): pass otlpEnabled=true in transformer test bucket helper

makeBucket is used exclusively by OtlpStatsTransformer tests, so spans
must be keyed with otlpEnabled=true to populate the OTLP-gated fields
(origin, spanKind, rpcMethod, rpcStatusCode).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* style(span-stats): minor style and readability cleanups

- Extract flush interval to variable before setAndTrack call
- Remove unnecessary quotes on property key
- Tighten test description wording

Rationale: Small consistency and readability improvements from PR review

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* fix(span-stats): remove rpc.method from otlp span stats aggregation key

- Drop grpc.method.name from SpanAggKey and OtlpStatsTransformer
- rpc.method inflates aggregation key cardinality without sufficient benefit
- Update all affected test assertions

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* refactor(span-stats): remove stale comments and clarify TODO

- Remove redundant inline comments in otlp-span-stats transformer
- Replace misleading comment about OTLP-only dimensions with a TODO
  noting origin and spanKind should eventually be included in legacy
  client stats aggregation

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* Apply suggestion from @mabdinur

* refactor(span-stats): remove redundant inline comments

- Drop comments that restate what the code already shows
- Keep code self-documenting per project style guidelines

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* fix(opentelemetry): fix max-len lint violation in span_processor.js

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): fix max-len lint violation in span_processor.js

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore: add otlp-span-stats exporter to CODEOWNERS

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(opentelemetry): encapsulate OTLP span stats in opentelemetry/metrics

Move OtlpStatsExporter and OtlpStatsTransformer into opentelemetry/metrics/
so the opentelemetry package is self-contained ahead of potential extraction
into its own npm package.

Key changes:
- Move exporters/otlp-span-stats/{index,transformer}.js to
  opentelemetry/metrics/otlp_span_stats_{exporter,transformer}.js
- Move buildResourceAttributes from span_stats.js to opentelemetry/metrics/index.js;
  add createOtlpSpanStatsExporter factory there
- Wire OtlpStatsExporter via DI: opentracing/tracer.js creates it when
  OTEL_TRACES_SPAN_METRICS_ENABLED and passes it through SpanProcessor to
  SpanStatsProcessor — span_stats.js no longer imports from opentelemetry/
- config/index.js mirrors OTEL_TRACES_SPAN_METRICS_ENABLED into
  stats.DD_TRACE_STATS_COMPUTATION_ENABLED so downstream checks are unified
- Remove otlpEnabled flag from SpanAggKey/SpanBuckets — origin, spanKind,
  rpcStatusCode are always populated
- Remove OTEL-specific check from AgentExporter (relies on mirrored flag)
- Remove CODEOWNERS entry for deleted exporters/otlp-span-stats/ path
- Move tests to test/opentelemetry/metrics/

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): align grpc stats with libdatadog and enforce mutual exclusion

- Move GRPC_STATUS_CODE constant to ext/tags.js
- Emit rpc.response.status_code as string (aligns with libdatadog kv_str)
- Use else if in onInterval to make native and OTLP export mutually exclusive

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(opentelemetry): trim JSDoc, privatize transformer, drop empty description

- Make OtlpStatsExporter#transformer a private field (#transformer)
- Remove empty description field from histogram metric
- Trim redundant @param prose in exporter and transformer JSDoc
- Use GRPC_STATUS_CODE import in transformer spec instead of string literal

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): suppress self-instrumentation spans from OTLP exporter requests

Wrap sendPayload's HTTP request in legacyStorage.run({ noop: true }) so the
tracer does not instrument its own outbound connections to the OTLP collector.
Without this, tcp.connect client spans for /v1/metrics requests were fed into
the traces.span.sdk.metrics.duration histogram, displacing real span data points
and inflating counts in the system-tests parametric suite.

Same pattern used by exporters/common/request.js and exporters/common/agents.js.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* test(opentelemetry): improve patch coverage for span stats OTLP export

- Inline URL parsing into OtlpHttpExporterBase constructor; remove setUrl
  (the if(telemetryTags !== undefined) branch was dead — telemetryTags is
  always undefined when setUrl was called from the constructor, and no
  external caller ever invoked it post-construction)
- Add tests for buildResourceAttributes (sdk identity, runtime-id, OTel-
  semantics mode) and createOtlpSpanStatsExporter
- Add tests for HTTP error response and request error paths in sendPayload

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* feat(opentelemetry): emit raw grpc.status.code name for rpc.response.status_code

Prefer the meta status NAME string over the numeric metrics tag and emit it
upper-cased to rpc.response.status_code, aligning with the OTel gRPC semantic
conventions (canonical status name) without any code<->name mapping.

* fix(opentelemetry): restore setUrl method removed by dead-code cleanup

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore(opentelemetry): trim redundant comments in grpc status mapping

Co-authored-by: Cursor <[email protected]>

* refactor(opentelemetry): read grpc.status.code from meta only

Drop the numeric metrics fallback; the gRPC status code is the canonical status
NAME and is read from span meta.

Co-authored-by: Cursor <[email protected]>

* fix(opentelemetry): translate numeric grpc.status.code from metrics to status name

The dd gRPC plugin sets grpc.status.code as a numeric integer via
span.setTag, which span_format.js routes into span.metrics rather than
span.meta. SpanAggKey was reading meta only, so rpcStatusCode was always
empty for real gRPC spans.

Now falls back to span.metrics[GRPC_STATUS_CODE] and translates the
integer to the canonical status name (OK, NOT_FOUND, etc.) using the
gRPC status code table. Meta string takes priority when both are present.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): split agg key by top-level to fix mixed-bucket OTLP metrics

When a bucket contained both top-level and measured non-top-level spans,
the heuristic (topLevelHits === hits) always resolved to false, causing
the OTLP histogram to be emitted as datadog.span.top_level=false and
dropping top-level traffic from APM metrics.

Adding topLevel as a dimension to SpanAggKey causes top-level and
non-top-level spans to bucket separately. Each bucket is now always
purely top-level or purely non-top-level, so the attribute is always
accurate. The native stats path is unaffected because toJSON() omits
topLevel; the Agent merges groups with identical key fields, preserving
the same Hits/TopLevelHits totals.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): track per-top-level distributions to fix native stats regression

The previous fix added topLevel to SpanAggKey to separate top-level and
non-top-level spans into distinct buckets. This created a real regression
in the native /v0.6/stats path: the Agent's mergeDuplicates() correctly
sums Hits/Errors/Duration from duplicate rows but silently drops
TopLevelHits from the merged-away entry. If the non-top-level row is
processed first and becomes the canonical, TopLevelHits from the
top-level row is lost.

Fix: revert topLevel from SpanAggKey (no more duplicate rows). Instead,
split SpanAggStats into four distributions (topLevelOk, topLevelError,
nonTopLevelOk, nonTopLevelError). The native stats path merges them at
export time so toJSON() produces the same combined OkSummary/ErrorSummary
as before. The OTLP path emits separate data points per top-level status
with the correct datadog.span.top_level attribute. OTel-semantics mode
merges the distributions (no top-level attribute to distinguish them).

Also adds SpanKind, Origin, and RpcStatusCode to the native stats
toJSON() payload so the Agent receives these new dimensions.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(opentelemetry): split native stats rows by top-level status; use GRPCStatusCode key

toJSON() now returns an array of up to 2 rows (top-level row first, non-top-level
row second). #toLegacyPayload uses flatMap to flatten them. This eliminates the
merge-time DDSketch allocation and ensures TopLevelHits is always non-zero on
the top-level row, so the Agent's mergeDuplicates retains it as the canonical entry.

Duration and Errors are derived from distribution .sum/.count, removing the
redundant this.duration and this.errors accumulators.

GRPCStatusCode matches the agent's msgpack decoder key (confirmed from
pkg/proto/pbgo/trace/stats_gen.go).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore(opentelemetry): minimize comments, move GRPC_STATUS_NAMES to constants

- Remove descriptive/narrating comments throughout; keep only non-obvious constraints
- Move GRPC_STATUS_NAMES from span_stats.js into constants.js
- Rename #toLegacyPayload -> #v06Payload
- Remove section comment from ext/tags.js

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): restrict ORIGIN_KEY to synthetics boolean; remove origin from aggregation key

ORIGIN_KEY now only populates SpanAggKey.synthetics. The origin string field
is removed from SpanAggKey, toString(), and the v0.6 payload (Origin is not
a field the agent decodes). In the OTLP path, datadog.origin='synthetics' is
emitted when aggKey.synthetics is true.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore: remove pr_description.md

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Munir <[email protected]>
Co-authored-by: Cursor <[email protected]>
rochdev pushed a commit that referenced this pull request Jul 2, 2026
* feat(span-stats): add OTLP metrics export for span stats

Export client-computed span stats as OTLP metrics (dd.trace.span.hits,
dd.trace.span.errors, dd.trace.span.top_level_hits, dd.trace.span.duration)
via a new OtlpStatsExporter alongside the existing Datadog /v0.6/stats
exporter.

Enabled via DD_TRACE_OTEL_METRICS_ENABLED=true, or auto-enabled when both
OTEL_TRACES_EXPORTER=otlp and OTEL_METRICS_EXPORTER=otlp are set. URL and
protocol are derived from the OTLP trace export configuration.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* test(config): register traceMetrics as internal runtime property

traceMetrics is a computed aggregate derived from OTEL_TRACES_EXPORTER,
OTEL_METRICS_EXPORTER, and DD_TRACE_OTEL_METRICS_ENABLED — not a raw
user-facing key — so it belongs in INTERNAL_RUNTIME_PROPERTIES alongside
sampler and stableConfig.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): guard traceMetrics URL derivation against invalid OTLP endpoint

When hostname is an unbracketed IPv6 address (e.g. ::1), the defaultOtlpBase
is http://::1:4318 which is not a valid URL. The new URL() call in the
traceMetrics block was the first code path to actually parse the string,
causing a TypeError that crashed config construction.

Wrap the URL derivation in a try/catch so that a malformed traces endpoint
falls back to the localhost default without throwing.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore(config): regenerate config types for DD_TRACE_OTEL_METRICS_ENABLED

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): add configurationNames to DD_TRACE_OTEL_METRICS_ENABLED entry

The eslint-config-names-sync rule verifies that every leaf property in
TracerOptions (index.d.ts) has a matching configurationNames entry in
supported-configurations.json. The entry for DD_TRACE_OTEL_METRICS_ENABLED
only had internalPropertyName, which is not checked by the rule.

Adding configurationNames: ["traceMetricsEnabled"] ties the two files
together and satisfies the lint check. Regenerated config types to match.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* bug fix

* update post RFC discussion

* bring implementation bug fix

* clean how cinfgs are set

* feat(otlp-trace-metrics): align span-stats export with the trace-metrics contract

Update the OTLP trace-metrics export to match the agreed RFC/system-test contract:

- Rename the enablement env var to OTEL_CLIENT_STATS_COMPUTATION_ENABLED and add
  DD_TRACE_OTEL_SEMANTICS_ENABLED (OTel-semantics mode: emit only OTel attributes, no dd.*).
- Emit a single histogram named traces.span.sdk.metrics.duration.
- Map dimensions to OTel attributes (span.name, span.kind, http.*, rpc.* from grpc tags) and
  convey errors via OTel status.code; default mode also adds dd.operation.name, dd.span.type,
  dd.origin and dd.span.top_level.
- Add telemetry.sdk.{name,language,version} resource attributes and emit process tags as dd.<key>
  (default mode only); gate all dd.* resource attributes behind default mode.
- Drive the flush/export cadence from OTEL_METRIC_EXPORT_INTERVAL and drop the
  _DD_TRACE_STATS_WRITER_INTERVAL override.
- Read grpc.status.code from span.metrics (numeric) with a meta fallback.

Update unit tests accordingly and regenerate config types.

Co-authored-by: Cursor <[email protected]>

* feat(otlp-trace-metrics): report service identity per InstrumentationScope

Partition span-stats data points by service so one OTLP payload can carry
multiple services, each as its own InstrumentationScope with service.name,
service.version and deployment.environment.name. These move off the resource,
which now only carries SDK identity, host.name and dd.* attributes.

Fix the trace-metrics flush cadence at 10s (no longer driven by
OTEL_METRIC_EXPORT_INTERVAL); the internal _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL
overrides it in tests only.

Co-authored-by: Cursor <[email protected]>

* fix(otlp-trace-metrics): apply internal flush interval override

The generic env applier only reads DD_/OTEL_ prefixed vars, so the
internal _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL (which starts with _DD_)
was never wired into config. Read it explicitly so the test-only flush
cadence override takes effect.

Co-authored-by: Cursor <[email protected]>

* feat(otlp-span-stats): emit fixed explicit-bounds histogram from the DDSketch

Derive the OTLP duration histogram from each group's DDSketch into the
spanmetrics-connector default bounds (in seconds), and drop the duplicate
exact-cell accumulator in span_stats. Each group now emits at most two data
points (ok/error) with a per-group dd.span.top_level heuristic, mirroring
libdatadog.

Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): carry service identity as resource attributes

Move service.name/service.version/deployment.environment.name onto the OTLP
resource (the configured default service), emit a single InstrumentationScope,
and add service.name as a data-point attribute only when a span's service
differs from the configured default. Thread DD_SERVICE through the processor so
the transformer can compare against it.

Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): drop redundant dd-trace InstrumentationScope

The exported OTLP metrics no longer carry an InstrumentationScope: a `dd-trace`
scope (name/version) is redundant with the resource's telemetry.sdk.* attributes.
The single scopeMetrics omits the scope field.

Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): datadog.* attribute prefix and OTEL_TRACES_SPAN_METRICS_ENABLED

Rename the OTLP trace-metric attributes from dd.* to datadog.* (operation.name,
span.type, span.top_level, origin, runtime_id, datadog.<process tags>) and rename
the enablement env var OTEL_CLIENT_STATS_COMPUTATION_ENABLED ->
OTEL_TRACES_SPAN_METRICS_ENABLED.

Co-authored-by: Cursor <[email protected]>

* feat(otlp): set _dd.stats_computed resource attribute on OTLP traces when trace metrics enabled

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(span-stats): use timer.unref?.() for Electron compatibility

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(rebase): restore extractRootTags; rename traceMetricsEnabled; add otelSemanticsEnabled to types

span_format.js: rebase conflict resolved to branch's addTag refactor which no longer exists in
master — revert to explicit typeof checks while keeping the FR06.3 BUG comment.

index.d.ts: rename traceMetricsEnabled -> otlpTraceMetricsEnabled to match supported-configurations.json;
add otelSemanticsEnabled (DD_TRACE_OTEL_SEMANTICS_ENABLED). Fixes eslint-config-names-sync errors.

Regenerate generated-config-types.d.ts from updated inputs.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(span-stats): wire OTLP metrics endpoint/protocol and trim dead code

The crash fix: SpanStatsProcessor read config.otelMetricsUrl/otelMetricsProtocol,
which Config never set (dropped in 40014ae), so `new URL(undefined)` threw
ERR_INVALID_URL and crashed tracer init whenever OTLP trace metrics were enabled.
Read the canonical OTEL_EXPORTER_OTLP_METRICS_ENDPOINT/OTEL_EXPORTER_OTLP_METRICS_PROTOCOL
directly instead of introducing redundant alias properties.

Also fix the dead auto-enable check: `this.otelMetricsEnabled` does not exist
(the property is DD_METRICS_OTEL_ENABLED), so `undefined === true` made the
"auto-enable when OTLP traces + OTEL metrics are on" path never trigger.

Minimize the diff vs master without changing behavior:
- drop 4 unused SpanAggStats fields (errorDuration/topLevel*) and their test
- collapse the duplicate JSON/protobuf transformer methods into transform()
- remove two `// BUG` WIP narration comments (reverts the comment-only
  span_format.js hunk; tracked separately)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Co-authored-by: Cursor <[email protected]>

* refactor(span-stats): privatize internals, trim telemetry, simplify transformer

- Make _drainBuckets / _toLegacyPayload true private (#) — neither
  crosses the class boundary; _ prefix implied false publicness
- Guard SpanStatsExporter construction behind !otlpTraceMetricsEnabled
  so it is never instantiated when the OTLP path is active
- Replace #errorStatus() / #boolAttr() one-shot methods with inline
  literals and a module-level ERROR_STATUS_ATTR constant to avoid
  per-call allocations
- sketchToFixedHistogram now returns number[] directly; #pushPoint
  references EXPLICIT_BOUNDS_SECONDS from the module constant
- Remove this.recordTelemetry calls from OtlpStatsExporter.export —
  not part of the OTLP trace-metrics spec
- Rewrite whitebox _drainBuckets test as blackbox: assert buckets are
  empty after onInterval() instead of calling the private method

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix configs

Co-authored-by: Munir Abdinur <[email protected]>

* chore: regenerate config types after supported-configurations.json update

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): use otelSemanticsEnabled config key instead of DD_TRACE_OTEL_SEMANTICS_ENABLED

Our branch maps DD_TRACE_OTEL_SEMANTICS_ENABLED to the internal property
otelSemanticsEnabled via supported-configurations.json internalPropertyName.
The merged master code was reading config.DD_TRACE_OTEL_SEMANTICS_ENABLED
directly, which was undefined in our config layout.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(test): update span.spec.js to use otelSemanticsEnabled config key

The test was setting config.DD_TRACE_OTEL_SEMANTICS_ENABLED but span.js
now reads config.otelSemanticsEnabled (the internal property name mapped
from DD_TRACE_OTEL_SEMANTICS_ENABLED via supported-configurations.json).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): use string default for _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL

Schema requires default to be a string or null, not a number literal.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): update description for _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL to match registry

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): use short description for _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL

The description field maps to Short Description in the config registry.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): remove description field from _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL

No other int entry with allowed field uses description; may be mutually exclusive in schema.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(otlp-span-stats): address Codex review comments

- Use dd-trace VERSION (not app version) for telemetry.sdk.version resource attribute
- Pass OTEL_EXPORTER_OTLP_METRICS_HEADERS and OTEL_EXPORTER_OTLP_METRICS_TIMEOUT
  to OtlpStatsExporter so authenticated/custom endpoints work correctly
- Fix index.d.ts doc: env var is OTEL_TRACES_SPAN_METRICS_ENABLED and
  auto-enable condition is DD_METRICS_OTEL_ENABLED (not OTEL_METRICS_EXPORTER=otlp)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(lint): fix import order and no-useless-undefined in span_stats and otlp-span-stats

- Move ../../../version import before ./constants to satisfy import/order rule
- Remove explicit = undefined default for headers param (unicorn/no-useless-undefined)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* test(config): cover _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL override branch

Adds test that exercises the setAndTrack call inside the conditional
that reads the internal flush interval override from the environment.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): align DD_TRACE_OTEL_SEMANTICS_ENABLED with registry definition

The config registry has this entry as a plain boolean with default "false"
and no internalPropertyName. Revert our custom mapping so the entry matches
the registry exactly — the validator compares against the registered definition.

All code that previously accessed config.otelSemanticsEnabled now reads
config.DD_TRACE_OTEL_SEMANTICS_ENABLED directly; the destructuring alias
in span_stats.js preserves the otelSemanticsEnabled local variable name.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(lint): add configurationNames for otelSemanticsEnabled to satisfy eslint-config-names-sync

The rule requires that every option name in index.d.ts has a corresponding
entry in supported-configurations.json (as a key, configurationNames value,
or internalPropertyName). Adding configurationNames: ["otelSemanticsEnabled"]
to DD_TRACE_OTEL_SEMANTICS_ENABLED satisfies this while keeping default: "false"
to match the registry. The generator uses configurationNames[0] as the config
key, so code reverts to config.otelSemanticsEnabled.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(config): remove internalPropertyName and unnecessary configurationNames

Per reviewer feedback:
- Remove internalPropertyName from _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL; the flush
  interval is read directly via getEnvironmentVariable() in #applyCalculated
- Remove configurationNames/internalPropertyName from OTEL_TRACES_SPAN_METRICS_ENABLED
  and drop otlpTraceMetricsEnabled as a programmatic option from index.d.ts; use
  this.OTEL_TRACES_SPAN_METRICS_ENABLED directly in #applyCalculated instead
- Remove configurationNames from DD_TRACE_OTEL_SEMANTICS_ENABLED and drop
  otelSemanticsEnabled as a programmatic option from index.d.ts; all callers
  now read config.DD_TRACE_OTEL_SEMANTICS_ENABLED directly

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(config): resolve Unknown Config properties for otlpTraceMetricsEnabled/ddTraceMetricsOtelFlushInterval

- Map setAndTrack to OTEL_TRACES_SPAN_METRICS_ENABLED (a declared config key)
  instead of the undeclared otlpTraceMetricsEnabled alias; all call sites
  updated to read config.OTEL_TRACES_SPAN_METRICS_ENABLED directly
- Move _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL reading from #applyCalculated
  into span_stats.js; import getEnvironmentVariable there directly — removes
  the undeclared ddTraceMetricsOtelFlushInterval setAndTrack write
- Update tests to use the env-var key names and remove the now-irrelevant
  config override test

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(lint): remove blank line before closing brace in config spec

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* feat(config): generate GeneratedEnvVarConfig interface for env var types

- Extract getBaseType helper from getTypeForEntry to share base type computation
- Add getEnvVarType that only adds undefined when there is no registered default
- Add generateEnvVarConfigTypes to map every env var name (canonical + aliases) to its resolved type
- Append GeneratedEnvVarConfig interface to generated-config-types.d.ts

Rationale: Callers of getValueFromEnvSources need per-env-var typed return values instead of the full config property union, enabling type-safe lookups by literal env var name.

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* fix(span-stats): address PR review comments

- Gate OTLP-only SpanAggKey dimensions (origin, spanKind, rpcMethod,
  rpcStatusCode) on otlpTraceMetricsEnabled to avoid inflating legacy
  span stats aggregation key cardinality
- Thread _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL through the typed config
  system (via setAndTrack/getValueFromEnvSources) instead of reading
  the raw env var directly in SpanStatsProcessor
- Add config tests covering OTEL_TRACES_SPAN_METRICS_ENABLED auto-enable
  logic (both conditions, explicit override)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(span-stats): pass otlpEnabled=true in transformer test bucket helper

makeBucket is used exclusively by OtlpStatsTransformer tests, so spans
must be keyed with otlpEnabled=true to populate the OTLP-gated fields
(origin, spanKind, rpcMethod, rpcStatusCode).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* style(span-stats): minor style and readability cleanups

- Extract flush interval to variable before setAndTrack call
- Remove unnecessary quotes on property key
- Tighten test description wording

Rationale: Small consistency and readability improvements from PR review

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* fix(span-stats): remove rpc.method from otlp span stats aggregation key

- Drop grpc.method.name from SpanAggKey and OtlpStatsTransformer
- rpc.method inflates aggregation key cardinality without sufficient benefit
- Update all affected test assertions

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* refactor(span-stats): remove stale comments and clarify TODO

- Remove redundant inline comments in otlp-span-stats transformer
- Replace misleading comment about OTLP-only dimensions with a TODO
  noting origin and spanKind should eventually be included in legacy
  client stats aggregation

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* Apply suggestion from @mabdinur

* refactor(span-stats): remove redundant inline comments

- Drop comments that restate what the code already shows
- Keep code self-documenting per project style guidelines

This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md)

* fix(opentelemetry): fix max-len lint violation in span_processor.js

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): fix max-len lint violation in span_processor.js

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore: add otlp-span-stats exporter to CODEOWNERS

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(opentelemetry): encapsulate OTLP span stats in opentelemetry/metrics

Move OtlpStatsExporter and OtlpStatsTransformer into opentelemetry/metrics/
so the opentelemetry package is self-contained ahead of potential extraction
into its own npm package.

Key changes:
- Move exporters/otlp-span-stats/{index,transformer}.js to
  opentelemetry/metrics/otlp_span_stats_{exporter,transformer}.js
- Move buildResourceAttributes from span_stats.js to opentelemetry/metrics/index.js;
  add createOtlpSpanStatsExporter factory there
- Wire OtlpStatsExporter via DI: opentracing/tracer.js creates it when
  OTEL_TRACES_SPAN_METRICS_ENABLED and passes it through SpanProcessor to
  SpanStatsProcessor — span_stats.js no longer imports from opentelemetry/
- config/index.js mirrors OTEL_TRACES_SPAN_METRICS_ENABLED into
  stats.DD_TRACE_STATS_COMPUTATION_ENABLED so downstream checks are unified
- Remove otlpEnabled flag from SpanAggKey/SpanBuckets — origin, spanKind,
  rpcStatusCode are always populated
- Remove OTEL-specific check from AgentExporter (relies on mirrored flag)
- Remove CODEOWNERS entry for deleted exporters/otlp-span-stats/ path
- Move tests to test/opentelemetry/metrics/

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): align grpc stats with libdatadog and enforce mutual exclusion

- Move GRPC_STATUS_CODE constant to ext/tags.js
- Emit rpc.response.status_code as string (aligns with libdatadog kv_str)
- Use else if in onInterval to make native and OTLP export mutually exclusive

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(opentelemetry): trim JSDoc, privatize transformer, drop empty description

- Make OtlpStatsExporter#transformer a private field (#transformer)
- Remove empty description field from histogram metric
- Trim redundant @param prose in exporter and transformer JSDoc
- Use GRPC_STATUS_CODE import in transformer spec instead of string literal

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): suppress self-instrumentation spans from OTLP exporter requests

Wrap sendPayload's HTTP request in legacyStorage.run({ noop: true }) so the
tracer does not instrument its own outbound connections to the OTLP collector.
Without this, tcp.connect client spans for /v1/metrics requests were fed into
the traces.span.sdk.metrics.duration histogram, displacing real span data points
and inflating counts in the system-tests parametric suite.

Same pattern used by exporters/common/request.js and exporters/common/agents.js.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* test(opentelemetry): improve patch coverage for span stats OTLP export

- Inline URL parsing into OtlpHttpExporterBase constructor; remove setUrl
  (the if(telemetryTags !== undefined) branch was dead — telemetryTags is
  always undefined when setUrl was called from the constructor, and no
  external caller ever invoked it post-construction)
- Add tests for buildResourceAttributes (sdk identity, runtime-id, OTel-
  semantics mode) and createOtlpSpanStatsExporter
- Add tests for HTTP error response and request error paths in sendPayload

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* feat(opentelemetry): emit raw grpc.status.code name for rpc.response.status_code

Prefer the meta status NAME string over the numeric metrics tag and emit it
upper-cased to rpc.response.status_code, aligning with the OTel gRPC semantic
conventions (canonical status name) without any code<->name mapping.

* fix(opentelemetry): restore setUrl method removed by dead-code cleanup

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore(opentelemetry): trim redundant comments in grpc status mapping

Co-authored-by: Cursor <[email protected]>

* refactor(opentelemetry): read grpc.status.code from meta only

Drop the numeric metrics fallback; the gRPC status code is the canonical status
NAME and is read from span meta.

Co-authored-by: Cursor <[email protected]>

* fix(opentelemetry): translate numeric grpc.status.code from metrics to status name

The dd gRPC plugin sets grpc.status.code as a numeric integer via
span.setTag, which span_format.js routes into span.metrics rather than
span.meta. SpanAggKey was reading meta only, so rpcStatusCode was always
empty for real gRPC spans.

Now falls back to span.metrics[GRPC_STATUS_CODE] and translates the
integer to the canonical status name (OK, NOT_FOUND, etc.) using the
gRPC status code table. Meta string takes priority when both are present.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): split agg key by top-level to fix mixed-bucket OTLP metrics

When a bucket contained both top-level and measured non-top-level spans,
the heuristic (topLevelHits === hits) always resolved to false, causing
the OTLP histogram to be emitted as datadog.span.top_level=false and
dropping top-level traffic from APM metrics.

Adding topLevel as a dimension to SpanAggKey causes top-level and
non-top-level spans to bucket separately. Each bucket is now always
purely top-level or purely non-top-level, so the attribute is always
accurate. The native stats path is unaffected because toJSON() omits
topLevel; the Agent merges groups with identical key fields, preserving
the same Hits/TopLevelHits totals.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): track per-top-level distributions to fix native stats regression

The previous fix added topLevel to SpanAggKey to separate top-level and
non-top-level spans into distinct buckets. This created a real regression
in the native /v0.6/stats path: the Agent's mergeDuplicates() correctly
sums Hits/Errors/Duration from duplicate rows but silently drops
TopLevelHits from the merged-away entry. If the non-top-level row is
processed first and becomes the canonical, TopLevelHits from the
top-level row is lost.

Fix: revert topLevel from SpanAggKey (no more duplicate rows). Instead,
split SpanAggStats into four distributions (topLevelOk, topLevelError,
nonTopLevelOk, nonTopLevelError). The native stats path merges them at
export time so toJSON() produces the same combined OkSummary/ErrorSummary
as before. The OTLP path emits separate data points per top-level status
with the correct datadog.span.top_level attribute. OTel-semantics mode
merges the distributions (no top-level attribute to distinguish them).

Also adds SpanKind, Origin, and RpcStatusCode to the native stats
toJSON() payload so the Agent receives these new dimensions.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(opentelemetry): split native stats rows by top-level status; use GRPCStatusCode key

toJSON() now returns an array of up to 2 rows (top-level row first, non-top-level
row second). #toLegacyPayload uses flatMap to flatten them. This eliminates the
merge-time DDSketch allocation and ensures TopLevelHits is always non-zero on
the top-level row, so the Agent's mergeDuplicates retains it as the canonical entry.

Duration and Errors are derived from distribution .sum/.count, removing the
redundant this.duration and this.errors accumulators.

GRPCStatusCode matches the agent's msgpack decoder key (confirmed from
pkg/proto/pbgo/trace/stats_gen.go).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore(opentelemetry): minimize comments, move GRPC_STATUS_NAMES to constants

- Remove descriptive/narrating comments throughout; keep only non-obvious constraints
- Move GRPC_STATUS_NAMES from span_stats.js into constants.js
- Rename #toLegacyPayload -> #v06Payload
- Remove section comment from ext/tags.js

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(opentelemetry): restrict ORIGIN_KEY to synthetics boolean; remove origin from aggregation key

ORIGIN_KEY now only populates SpanAggKey.synthetics. The origin string field
is removed from SpanAggKey, toString(), and the v0.6 payload (Origin is not
a field the agent decodes). In the OTLP path, datadog.origin='synthetics' is
emitted when aggKey.synthetics is true.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* chore: remove pr_description.md

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Munir <[email protected]>
Co-authored-by: Cursor <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants