Skip to content

Commit 3dc4c7b

Browse files
committed
fix(tracing): make peer service finalization idempotent
Repeated finish notifications can reach an outbound span before an ancestor lets the trace flush. The second pass treats the computed peer service as preconfigured and rewrites its source to `peer.service`; an existing source now marks finalization. The peer-service assertion keeps its correlation parent open through completion and matches the integration component, so delayed setup traces and spans from another integration cannot satisfy it.
1 parent 5661b73 commit 3dc4c7b

5 files changed

Lines changed: 80 additions & 15 deletions

File tree

packages/datadog-plugin-mongodb-core/test/mongodb.spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ describe('Plugin', () => {
107107
'mongodb-core',
108108
(done) => collection.insertOne({ a: 1 }, {}, done),
109109
'test',
110-
'peer.service'
110+
'peer.service',
111+
{ component: 'mongodb' }
111112
)
112113

113114
// The bulkWrite span is opened with `db.name` set, so it flows through the same
@@ -118,7 +119,7 @@ describe('Plugin', () => {
118119
() => collection.bulkWrite([{ insertOne: { document: { a: 1 } } }]),
119120
'test',
120121
'peer.service',
121-
{ desc: 'with bulkWrite' }
122+
{ component: 'mongodb', desc: 'with bulkWrite' }
122123
)
123124

124125
it('should do automatic instrumentation', done => {

packages/datadog-plugin-mongoose/test/index.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ describe('Plugin', () => {
7373
return new PeerCat({ name: 'PeerCat' }).save()
7474
},
7575
() => dbName,
76-
'peer.service')
76+
'peer.service',
77+
{ component: 'mongodb' })
7778

7879
it('should propagate context with write operations', () => {
7980
const Cat = mongoose.model('Cat1', { name: String })

packages/dd-trace/src/plugins/outbound.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ class OutboundPlugin extends TracingPlugin {
125125
*/
126126
tagPeerService (span) {
127127
if (this._tracerConfig.spanComputePeerService) {
128-
const peerData = this.getPeerService(span.context().getTags())
128+
const tags = span.context().getTags()
129+
if (tags[PEER_SERVICE_SOURCE_KEY] !== undefined) return
130+
131+
const peerData = this.getPeerService(tags)
129132
if (peerData !== undefined) {
130133
span.addTags(this.getPeerServiceRemap(peerData))
131134
}

packages/dd-trace/test/plugins/outbound.spec.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,44 @@ describe('OuboundPlugin', () => {
5050
sinon.assert.notCalled(getRemapStub)
5151
})
5252

53+
it('should not recompute peer service after its source was set', () => {
54+
computePeerServiceStub.value({ spanComputePeerService: true })
55+
const tags = {
56+
'peer.service': 'mypeerservice',
57+
'_dd.peer.service.source': 'out.host',
58+
}
59+
instance.tagPeerService({ context: () => ({ getTags: () => tags }) })
60+
61+
sinon.assert.notCalled(getPeerServiceStub)
62+
sinon.assert.notCalled(getRemapStub)
63+
})
64+
65+
it('should not recompute peer service when only its source was set', () => {
66+
computePeerServiceStub.value({ spanComputePeerService: true })
67+
const tags = {
68+
'_dd.peer.service.source': 'out.host',
69+
}
70+
instance.tagPeerService({ context: () => ({ getTags: () => tags }) })
71+
72+
sinon.assert.notCalled(getPeerServiceStub)
73+
sinon.assert.notCalled(getRemapStub)
74+
})
75+
76+
it('should compute peer service when only its value was set', () => {
77+
computePeerServiceStub.value({ spanComputePeerService: true })
78+
getPeerServiceStub.returns({
79+
'peer.service': 'mypeerservice',
80+
'_dd.peer.service.source': 'peer.service',
81+
})
82+
const tags = {
83+
'peer.service': 'mypeerservice',
84+
}
85+
instance.tagPeerService({ context: () => ({ getTags: () => tags }), addTags: () => {} })
86+
87+
sinon.assert.called(getPeerServiceStub)
88+
sinon.assert.called(getRemapStub)
89+
})
90+
5391
it('should do nothing when disabled', () => {
5492
computePeerServiceStub.value({ spanComputePeerService: false })
5593
instance.tagPeerService({ context: () => ({ _tags: {}, getTags () { return this._tags } }), addTags: () => {} })

packages/dd-trace/test/setup/mocha.js

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ function withNamingSchema (
188188
* @param {((callback: (error?: Error) => void) => unknown) | (() => Promise<unknown>)} spanGenerationFn
189189
* @param {string | (() => string)} service
190190
* @param {string} serviceSource
191-
* @param {{ desc?: string }} [opts]
191+
* @param {{ component?: string, desc?: string }} [opts]
192192
*/
193193
function withPeerService (tracer, pluginName, spanGenerationFn, service, serviceSource, opts = {}) {
194194
describe('peer service computation' + (opts.desc ? ` ${opts.desc}` : ''), function () {
@@ -207,19 +207,39 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service
207207
})
208208

209209
it('should compute peer service', async () => {
210-
const { expectSomeSpan } = require('../plugins/helpers')
211210
const currentTracer = global._ddtrace
212211
const parentSpan = currentTracer.startSpan('peer-service.test')
212+
const traceId = BigInt(parentSpan.context().toTraceId())
213+
const component = opts.component ?? pluginName
213214
let traceAssertion
214215

216+
/**
217+
* @param {Array<Array<{ trace_id: bigint, meta: Record<string, string> }>>} traces
218+
*/
219+
function assertPeerServiceSpan (traces) {
220+
const expectedService = typeof service === 'function' ? service() : service
221+
222+
for (const trace of traces) {
223+
for (const span of trace) {
224+
if (
225+
span.trace_id === traceId &&
226+
span.meta.component === component &&
227+
span.meta['peer.service'] === expectedService &&
228+
span.meta['_dd.peer.service.source'] === serviceSource
229+
) {
230+
return
231+
}
232+
}
233+
}
234+
235+
assert.fail(
236+
`No ${component} span in trace ${traceId} had peer.service=${expectedService} and ` +
237+
`_dd.peer.service.source=${serviceSource}.\n\nCandidate Traces:\n${util.inspect(traces, { depth: null })}`
238+
)
239+
}
240+
215241
try {
216-
traceAssertion = expectSomeSpan(getAgent(), {
217-
trace_id: BigInt(parentSpan.context().toTraceId()),
218-
meta: {
219-
'peer.service': typeof service === 'function' ? service() : service,
220-
'_dd.peer.service.source': serviceSource,
221-
},
222-
})
242+
traceAssertion = getAgent().assertSomeTraces(assertPeerServiceSpan, { timeoutMs: 9000 })
223243
const useCallback = spanGenerationFn.length === 1
224244
const spanGenerationPromise = currentTracer.scope().activate(parentSpan, () => {
225245
return useCallback
@@ -233,7 +253,6 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service
233253
})
234254
: spanGenerationFn()
235255
})
236-
parentSpan.finish()
237256

238257
assert.strictEqual(
239258
typeof spanGenerationPromise?.then, 'function',
@@ -243,7 +262,10 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service
243262

244263
await Promise.all([
245264
traceAssertion,
246-
spanGenerationPromise,
265+
(async () => {
266+
await spanGenerationPromise
267+
parentSpan.finish()
268+
})(),
247269
])
248270
} finally {
249271
parentSpan.finish()

0 commit comments

Comments
 (0)