fix(otel)!: share a single @opentelemetry/api copy with the application - #9077
fix(otel)!: share a single @opentelemetry/api copy with the application#9077BridgeAR wants to merge 15 commits into
Conversation
Overall package sizeSelf size: 7.53 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.3.2 | 124.41 kB | 440.65 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 |
|
BenchmarksBenchmark execution time: 2026-07-22 12:14:19 Comparing candidate commit 9a14ac1 in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 2326 metrics, 32 unstable metrics.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #9077 +/- ##
==========================================
+ Coverage 98.41% 98.43% +0.02%
==========================================
Files 932 934 +2
Lines 124878 125349 +471
Branches 10529 10640 +111
==========================================
+ Hits 122893 123391 +498
+ Misses 1985 1958 -27
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The guard that disables DD_LOGS_OTEL_ENABLED and DD_METRICS_OTEL_ENABLED when @opentelemetry/api is absent warned unconditionally. If a later release defaults one of these flags on, every app without the optional peer would get a warning for a feature it never opted into. Warn only when the flag's origin is the customer (tracked in trackedConfigOrigins); disable a default-on flag silently. The config test harness gains a defaultsOverride hook so the default-on path runs through the real #applyDefaults (which sets defaults untracked) instead of poking internal state. Refs: #9077 (comment)
7ae51db to
b375026
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b3750262a7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
zacharycmontoya
left a comment
There was a problem hiding this comment.
While I am not an expert on the Node.js require mechanism, at a high-level the loading logic and the unit tests look good to me. I'd appreciate a review from someone more knowledgeable about Node.js on this
zacharycmontoya
left a comment
There was a problem hiding this comment.
LGTM again after the changes to also handle the @opentelemetry/api-logs package
9f94958 to
f4810d7
Compare
40f4a78 to
5f8c7b4
Compare
…ridge dd-trace bundles its own @opentelemetry/api as an optional dependency. The OTel global API keys providers per major and rejects one registered by a copy older than the reader's, so once the application loaded a newer copy the bridge's provider lost the version check and every span silently downgraded to a no-op (reported with @opentelemetry/api >1.4.1). An instrumentation now captures the application's own copy of @opentelemetry/api and @opentelemetry/api-logs the moment the application requires it, and every trace, metrics, and logs module reads that copy through a holder. When the application ships its own copy the bridge registers on it and the version mismatch disappears; when it does not, the holder falls back to the bundled copy, so nothing outside dd-trace reads OTel and the single bundled copy has nothing to clash with. The trace bridge and the OTLP metrics and logs pipelines therefore keep working without the application adding the packages itself, and they initialize eagerly again rather than waiting on the capture. The @opentelemetry/api-logs hook is scoped to >=0.33.0 <1.0.0: the package has no 1.0.0 release, and an open-below <1.0.0 range makes the plugin version matrix coerce a non-existent 1.0.0 floor and fail to install. Fixes: #6882
…bundling The bridge binds to the application's @opentelemetry/api copy through an instrumentation that fires on a runtime require. A bundler resolves a string-literal require at build time, so bundling the API packages inlines a second copy the instrumentation never sees; the bridge then registers its provider on the wrong copy and downgrades every span to a no-op. Marking @opentelemetry/api and @opentelemetry/api-logs external keeps the require intact for interception. The webpack `commonjs` external type forces a CommonJS require regardless of the user's externalsType. Fixes: #6882
Bridge modules read the captured @opentelemetry/api copy at module load. The application's copy is captured when it requires the package, which can happen after a bridge module has already loaded, so the module kept dd-trace's fallback copy. Registering the provider on that copy while the application reads its own lets the OpenTelemetry global API reject the older registration and downgrade every span to a no-op (issue #6882). Every copy-sensitive path now reads the API through the holder at use time: provider registration, context conversion, span creation, log emission, and runtime metrics. A copy captured after the module loaded is therefore the one the bridge registers on and uses. SpanKind and SeverityNumber stay resolved at load because they are spec-fixed constants, identical across copies. Fixes: #6882
…app has no copy The plugins marked @opentelemetry/api and @opentelemetry/api-logs external unconditionally, so a bundle that used the OpenTelemetry bridge but was deployed without the packages in node_modules threw at runtime on the fallback require. Externalizing is only needed when the application owns a copy the instrumentation must capture and share; a package the application does not declare has no competing copy to protect. Externalize a package only when the application's package.json declares it, and otherwise leave it in the bundle so dd-trace's own copy is inlined and the bundle stays self-contained. When the manifest cannot be read, keep externalizing so the shared-copy correctness is never traded for the self-contained-bundle optimization. Fixes: #6882
An API subpath or dd-trace's fallback can load before the application's package entrypoint. Treating that first hook as authoritative leaves provider registration on the wrong OpenTelemetry global registry, and ESM namespaces cannot be patched in place. Prefer the API resolved from the application entrypoint, keep fallback loads out of capture, and resolve the holder at each use so late CommonJS and ESM loads share the application's registry. Bundlers only externalize application-owned API packages, preserving self-contained fallback bundles. Fixes: #6882
An application-owned OpenTelemetry API can load after dd-trace has already registered providers against its fallback. Keeping those registrations on the fallback registry leaves the application's trace, metrics, and logs calls as silent no-ops. Track capture ownership and priority, move registrations atomically when a better application copy appears, and bind hot paths directly to the selected API. Resolve only supported installed application dependencies, preserve required bundler external precedence, and cover relocated CJS and ESM bundles with real API copies. Fixes: #6882
Patch coverage exposed untested branches around generated ownership metadata, missing request values, empty trace state, and repeated provider registration. Pin those boundaries so regressions cannot silently change which OpenTelemetry registry receives providers.
OpenTelemetry keeps its original global version after disable(), so transferring providers to a later API copy can reject the next registration and leave supported application copies on no-op providers. Keep provider registration on pinned compatibility-max owners; late captures only replace immutable copy-local snapshots. Resolve API ownership from each workspace importer so nested applications externalize their declared runtime copy without externalizing dd-trace's fallback. Fixes: #6882
An older API can claim OpenTelemetry's core global by configuring diagnostics, causing later provider registration through the pinned copy to fail its exact-version check. Replacing only that diagnostic-only owner preserves the logger and lets the pinned compatibility-max copy own every signal without moving live providers. Relocated bundles keep dd-trace's fallback graph internal, while application API imports still honor user externals.
Regular dependencies let dd-trace's resolver win over an application-owned copy, which can leave bridge operations on a different global registry. Keep the fallback copies optional. Provider registration remains pinned to compatibility-max owners. Fixes: #6882
Bun 1.3.1 strips scopes from npm aliases while importing Yarn v1 locks, making CI request nonexistent unscoped packages. Refs: oven-sh/bun#27781
Bun's Yarn v1 lockfile migration drops scopes from npm alias targets, making every CI install request nonexistent unscoped packages. Refs: oven-sh/bun#27781
The package-size job resolves dev dependencies from the packed manifest even under --prod, where local test fixture paths do not exist.
Windows treats drive-letter loader paths as URL schemes and resolves rooted paths against the current drive. Use URL and absolute-path forms that preserve the fixture intent on every platform.
5cc58e0 to
e1c972a
Compare
Summary
dd-trace bundled its own
@opentelemetry/apias an optional dependency. The OpenTelemetry global API keys providers per major and rejects one registered by a copy older than the reader's, so once the application loaded a newer copy the bridge's provider lost the version check and every span silently downgraded to a no-op (reported with@opentelemetry/api>1.4.1).Binding to the application's own copy removes the mismatch: an instrumentation captures
@opentelemetry/apiand@opentelemetry/api-logsas the application requires them, and every bridge module reads that copy through a holder. There is then exactly one copy, and ESM callers work identically to CommonJS because require-in-the-middle drives the capture.onReadycontinuation until the copy is captured; the meter provider registers before OTLP runtime metrics so their instruments bind to the real meter, not the noop one.requireintact for the instrumentation.Test plan
test/opentelemetry/api.spec.js: the holder captures the first copy, ignores later captures, and runsonReadyimmediately when captured or defers it in registration order otherwise.datadog-instrumentations/test/otel-api.spec.js: the hooks fire across the supported version ranges and hand the module to the holder unchanged.test/proxy.spec.js: metrics/logs registration defers until capture, and runtime metrics start after the meter provider.opentelemetry,runtime_metrics, andopenfeaturesuites pass with every consumer reading through the holder.Fixes: #6882