Skip to content

Commit 40f4a78

Browse files
committed
fix(otel): capture application API copies through instrumentation
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
1 parent 59c00dd commit 40f4a78

25 files changed

Lines changed: 533 additions & 73 deletions

File tree

.github/workflows/instrumentation.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,16 @@ jobs:
405405
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
406406
- uses: ./.github/actions/instrumentations/test
407407

408+
instrumentation-otel-api:
409+
runs-on: ubuntu-latest
410+
permissions:
411+
id-token: write
412+
env:
413+
PLUGINS: otel-api
414+
steps:
415+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
416+
- uses: ./.github/actions/instrumentations/test
417+
408418
instrumentation-otel-sdk-trace:
409419
runs-on: ubuntu-latest
410420
permissions:

docs/API.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -400,10 +400,10 @@ const tracerProvider = new tracer.TracerProvider()
400400
tracerProvider.register()
401401
```
402402

403-
dd-trace registers providers through its bundled, compatibility-max OpenTelemetry API copies.
404-
Application copies of `@opentelemetry/api` from 1.4.1 through 1.9.x and
405-
`@opentelemetry/api-logs` from 0.33.0 through 0.x consume those providers through OpenTelemetry's
406-
global API without becoming provider owners themselves.
403+
dd-trace captures application copies of `@opentelemetry/api` from 1.4.1 through 1.9.x and
404+
`@opentelemetry/api-logs` from 0.33.0 through 0.x for bridge operations. Provider registration stays
405+
on dd-trace's optional compatibility-max copies so every supported application copy can consume the
406+
providers through OpenTelemetry's global API.
407407

408408
The `dd-trace/esbuild` and `dd-trace/webpack` plugins keep dd-trace's API copies inside relocated
409409
bundles even when application imports are configured as external. Application imports otherwise

integration-tests/helpers/otel-api-bundle.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ const apiLogs = holder.getApiLogs()`
116116
? `if (api.trace !== globalThis.__ddRuntimeApi.trace || apiLogs.logs !== globalThis.__ddRuntimeApiLogs.logs) {
117117
throw new Error('Application OpenTelemetry APIs were bundled instead of loaded at runtime')
118118
}
119-
if (api.trace === holder.getApi().trace || apiLogs.logs === holder.getApiLogs().logs) {
120-
throw new Error('Application and bridge APIs unexpectedly share a bundled module')
119+
if (api.trace !== holder.getApi().trace || apiLogs.logs !== holder.getApiLogs().logs) {
120+
throw new Error('The bridge did not capture the application OpenTelemetry APIs')
121121
}
122122
`
123123
: ''

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,6 @@
167167
"version.js"
168168
],
169169
"dependencies": {
170-
"@opentelemetry/api": "1.9.1",
171-
"@opentelemetry/api-logs": "0.212.0",
172170
"dc-polyfill": "^0.1.11",
173171
"import-in-the-middle": "^3.3.1",
174172
"opentracing": ">=0.14.7"
@@ -181,6 +179,8 @@
181179
"@datadog/openfeature-node-server": "2.0.0",
182180
"@datadog/pprof": "5.15.1",
183181
"@datadog/wasm-js-rewriter": "5.0.1",
182+
"@opentelemetry/api": "1.9.1",
183+
"@opentelemetry/api-logs": "0.212.0",
184184
"oxc-parser": "^0.132.0"
185185
},
186186
"devDependencies": {

packages/datadog-esbuild/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ function addModuleOfInterest (name, file) {
5151
const modulesOfInterest = new Set()
5252

5353
for (const [name, instrumentation] of Object.entries(instrumentations)) {
54+
if (OTEL_API_PACKAGE_PATTERN.test(name)) continue
55+
5456
for (const entry of instrumentation) {
5557
addModuleOfInterest(name, entry.file)
5658
}

packages/datadog-instrumentations/src/helpers/hooks.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ module.exports = {
4848
'@langchain/core': { esmFirst: true, fn: () => require('../langchain') },
4949
'@node-redis/client': () => require('../redis'),
5050
'@opensearch-project/opensearch': () => require('../opensearch'),
51+
'@opentelemetry/api': { esmFirst: true, fn: () => require('../otel-api') },
52+
'@opentelemetry/api-logs': { esmFirst: true, fn: () => require('../otel-api') },
5153
'@opentelemetry/sdk-trace': () => require('../otel-sdk-trace'),
5254
'@opentelemetry/sdk-trace-node': () => require('../otel-sdk-trace'),
5355
'@prisma/client': { esmFirst: true, fn: () => require('../prisma') },
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict'
2+
3+
const {
4+
API_LOGS_VERSION_RANGE,
5+
API_VERSION_RANGE,
6+
setApi,
7+
setApiLogs,
8+
} = require('../../dd-trace/src/opentelemetry/api')
9+
const { addHook } = require('./helpers/instrument')
10+
11+
addHook({
12+
name: '@opentelemetry/api',
13+
versions: [API_VERSION_RANGE],
14+
// The default export omits constants used by the bridge.
15+
patchDefault: false,
16+
}, setApi)
17+
18+
addHook({
19+
name: '@opentelemetry/api-logs',
20+
versions: [API_LOGS_VERSION_RANGE],
21+
patchDefault: false,
22+
}, setApiLogs)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict'
2+
3+
const assert = require('node:assert/strict')
4+
5+
const { describe, it, beforeEach } = require('mocha')
6+
const proxyquire = require('proxyquire')
7+
const sinon = require('sinon')
8+
9+
describe('otel-api instrumentation', () => {
10+
let addHook
11+
let hooks
12+
let setApi
13+
let setApiLogs
14+
15+
function load () {
16+
addHook = sinon.spy()
17+
setApi = sinon.spy(api => api)
18+
setApiLogs = sinon.spy(apiLogs => apiLogs)
19+
proxyquire.noPreserveCache()
20+
try {
21+
proxyquire('../src/otel-api', {
22+
'./helpers/instrument': { addHook },
23+
'../../dd-trace/src/opentelemetry/api': {
24+
API_LOGS_VERSION_RANGE: '>=0.33.0 <1.0.0',
25+
API_VERSION_RANGE: '>=1.4.1 <1.10.0',
26+
setApi,
27+
setApiLogs,
28+
},
29+
})
30+
} finally {
31+
proxyquire.preserveCache()
32+
}
33+
const hooks = new Map()
34+
for (const call of addHook.getCalls()) {
35+
hooks.set(call.args[0].name, call.args[1])
36+
}
37+
return hooks
38+
}
39+
40+
beforeEach(() => {
41+
hooks = load()
42+
})
43+
44+
it('hooks @opentelemetry/api across the supported range', () => {
45+
sinon.assert.calledWith(addHook, {
46+
name: '@opentelemetry/api',
47+
versions: ['>=1.4.1 <1.10.0'],
48+
patchDefault: false,
49+
}, setApi)
50+
})
51+
52+
it('hooks @opentelemetry/api-logs across its published pre-1.0 range', () => {
53+
sinon.assert.calledWith(addHook, {
54+
name: '@opentelemetry/api-logs',
55+
versions: ['>=0.33.0 <1.0.0'],
56+
patchDefault: false,
57+
}, setApiLogs)
58+
})
59+
60+
it('passes the loaded core API to the holder unchanged', () => {
61+
const api = { trace: {}, context: {} }
62+
const hookMetadata = { moduleBaseDir: '/app/node_modules/@opentelemetry/api' }
63+
64+
assert.strictEqual(hooks.get('@opentelemetry/api')(api, '1.9.0', false, hookMetadata), api)
65+
sinon.assert.calledOnceWithExactly(setApi, api, '1.9.0', false, hookMetadata)
66+
})
67+
68+
it('passes the loaded Logs API to the holder unchanged', () => {
69+
const apiLogs = { logs: {}, SeverityNumber: {} }
70+
const hookMetadata = { moduleBaseDir: '/app/node_modules/@opentelemetry/api-logs' }
71+
72+
assert.strictEqual(hooks.get('@opentelemetry/api-logs')(apiLogs, '0.212.0', false, hookMetadata), apiLogs)
73+
sinon.assert.calledOnceWithExactly(setApiLogs, apiLogs, '0.212.0', false, hookMetadata)
74+
})
75+
})

packages/datadog-webpack/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ for (const hook of Object.values(hooks)) {
2626
const modulesOfInterest = new Set()
2727

2828
for (const [name, instrumentation] of Object.entries(instrumentations)) {
29+
if (OTEL_API_PACKAGE_PATTERN.test(name)) continue
30+
2931
for (const entry of instrumentation) {
3032
if (entry.file) {
3133
modulesOfInterest.add(`${name}/${entry.file}`) // e.g. "redis/my/file.js"

packages/dd-trace/src/openfeature/eval-metrics-hook.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict'
22

33
const log = require('../log')
4-
const { getApi } = require('../opentelemetry/api')
4+
const { getApiOwner } = require('../opentelemetry/api')
55

66
const METER_NAME = 'dd-trace-js/openfeature'
77
const COUNTER_NAME = 'feature_flag.evaluations'
@@ -54,7 +54,7 @@ class EvalMetricsHook {
5454
if (this.#counter) return this.#counter
5555

5656
try {
57-
const { metrics } = getApi()
57+
const { metrics } = getApiOwner()
5858
const meter = metrics.getMeter(METER_NAME)
5959
this.#counter = meter.createCounter(COUNTER_NAME, {
6060
description: COUNTER_DESCRIPTION,

0 commit comments

Comments
 (0)