Skip to content

Commit 3e7a7d1

Browse files
committed
fix log injection and sampling rule bug
1 parent c12cfd7 commit 3e7a7d1

File tree

5 files changed

+192
-2
lines changed

5 files changed

+192
-2
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
'use strict'
2+
3+
const { FakeAgent, createSandbox, spawnProc, curlAndAssertMessage } = require('./helpers')
4+
const { assert } = require('chai')
5+
const path = require('path')
6+
const { USER_KEEP } = require('../ext/priority')
7+
8+
describe('Log Injection', () => {
9+
let agent
10+
let proc
11+
let sandbox
12+
let cwd
13+
let app
14+
let env
15+
16+
before(async () => {
17+
sandbox = await createSandbox(['express', 'winston'])
18+
cwd = sandbox.folder
19+
app = path.join(cwd, 'log_injection/index.js')
20+
})
21+
22+
after(async () => {
23+
await sandbox.remove()
24+
})
25+
26+
beforeEach(async () => {
27+
agent = await new FakeAgent().start()
28+
})
29+
30+
describe('log injection with rule based sampling', () => {
31+
beforeEach(async () => {
32+
agent = await new FakeAgent().start()
33+
})
34+
35+
afterEach(async () => {
36+
proc.kill()
37+
await agent.stop()
38+
})
39+
40+
it('should correctly apply rule based sampling when log injection is enabled', async () => {
41+
env = {
42+
AGENT_PORT: agent.port,
43+
lOG_INJECTION: 'true'
44+
}
45+
proc = await spawnProc(app, { cwd, env, execArgv: [] })
46+
const url = proc.url + '/sampled'
47+
return curlAndAssertMessage(agent, url, ({ headers, payload }) => {
48+
assert.isArray(payload)
49+
assert.strictEqual(payload.length, 1)
50+
// Bug: previously got USER_REJECT instead of USER_KEEP when log injection is enabled,
51+
// meaning resource rules are not applied & instead global sampling is applied
52+
// Now gets USER_KEEP because resource rules are applied
53+
assert.propertyVal(payload[0][0].metrics, '_sampling_priority_v1', USER_KEEP)
54+
}, 20000, 1)
55+
})
56+
57+
it('should correctly apply rule based sampling when log injection is disabled', async () => {
58+
env = {
59+
AGENT_PORT: agent.port,
60+
lOG_INJECTION: 'false'
61+
}
62+
proc = await spawnProc(app, { cwd, env, execArgv: [] })
63+
const url = proc.url + '/sampled'
64+
return curlAndAssertMessage(agent, url, ({ headers, payload }) => {
65+
assert.isArray(payload)
66+
assert.strictEqual(payload.length, 1)
67+
assert.propertyVal(payload[0][0].metrics, '_sampling_priority_v1', USER_KEEP)
68+
}, 20000, 1)
69+
})
70+
})
71+
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict'
2+
3+
const options = {
4+
service: 'test-service',
5+
sampleRate: 0.0,
6+
samplingRules: [
7+
{
8+
resource: 'GET /sampled',
9+
sampleRate: 1.0
10+
}
11+
]
12+
}
13+
14+
if (process.env.AGENT_PORT) {
15+
options.port = process.env.AGENT_PORT
16+
}
17+
18+
if (process.env.lOG_INJECTION) {
19+
options.logInjection = process.env.lOG_INJECTION
20+
}
21+
22+
const tracer = require('dd-trace')
23+
tracer.init(options)
24+
25+
const express = require('express')
26+
const winston = require('winston')
27+
28+
const app = express()
29+
30+
// Create winston logger
31+
const logger = winston.createLogger({
32+
level: 'info',
33+
format: winston.format.json(),
34+
transports: [
35+
new winston.transports.Console({ silent: true })
36+
]
37+
})
38+
39+
// Route WITH logging (demonstrates the bug)
40+
app.get('/sampled', (req, res) => {
41+
// BUG: This winston.info() triggers log injection BEFORE resource.name is set
42+
// which causes sampling decision to happen too early, bypassing the resource rule
43+
logger.info('Processing GET /sampled request')
44+
res.json({ message: 'logged request' })
45+
})
46+
47+
const server = app.listen(0, () => {
48+
const port = server.address().port
49+
process.send({ port })
50+
})

packages/dd-trace/src/priority_sampler.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const RateLimiter = require('./rate_limiter')
55
const Sampler = require('./sampler')
66
const { setSamplingRules } = require('./startup-log')
77
const SamplingRule = require('./sampling_rule')
8+
const { resourceLocator } = SamplingRule
89

910
const {
1011
SAMPLING_MECHANISM_DEFAULT,
@@ -116,7 +117,11 @@ class PrioritySampler {
116117
context._sampling.priority = tag
117118
context._sampling.mechanism = SAMPLING_MECHANISM_MANUAL
118119
} else if (auto) {
119-
context._sampling.priority = this._getPriorityFromAuto(root)
120+
const priority = this._getPriorityFromAuto(root)
121+
if (priority === undefined) {
122+
return // Defer sampling until resource information is available
123+
}
124+
context._sampling.priority = priority
120125
} else {
121126
return
122127
}
@@ -200,10 +205,18 @@ class PrioritySampler {
200205
/**
201206
*
202207
* @param span {DatadogSpan}
203-
* @returns {SamplingPriority}
208+
* @returns {SamplingPriority|undefined}
204209
*/
205210
_getPriorityFromAuto (span) {
206211
const context = this._getContext(span)
212+
213+
// Check if resource name is available and if we have resource-based rules
214+
const { _tags: tags } = context
215+
const hasResource = tags.resource || tags['resource.name']
216+
if (!hasResource && this.#hasResourceBasedRules()) {
217+
return // Defer sampling until resource is available
218+
}
219+
207220
const rule = this.#findRule(span)
208221

209222
return rule
@@ -338,6 +351,16 @@ class PrioritySampler {
338351
}
339352
}
340353

354+
/**
355+
* Check if any sampling rules depend on resource names
356+
* @returns {boolean}
357+
*/
358+
#hasResourceBasedRules () {
359+
return this._rules.some(rule =>
360+
rule.matchers.some(matcher => matcher.locator === resourceLocator)
361+
)
362+
}
363+
341364
/**
342365
*
343366
* @param span {DatadogSpan}

packages/dd-trace/src/sampling_rule.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,4 @@ class SamplingRule {
143143
}
144144

145145
module.exports = SamplingRule
146+
module.exports.resourceLocator = resourceLocator

packages/dd-trace/test/priority_sampler.spec.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,4 +527,49 @@ describe('PrioritySampler', () => {
527527
expect(setPriority).to.be.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC)
528528
})
529529
})
530+
531+
describe('resource-based rule deferral', () => {
532+
beforeEach(() => {
533+
span.tracer = sinon.stub().returns({ _service: 'test-service' })
534+
})
535+
536+
it('should defer sampling when resource rules exist but resource name is not set', () => {
537+
prioritySampler.configure('test', {
538+
sampleRate: 0,
539+
rules: [{ resource: 'GET /api/*', sampleRate: 1 }]
540+
})
541+
542+
span.context()._tags = {}
543+
544+
prioritySampler.sample(span)
545+
546+
expect(span.context()._sampling.priority).to.be.undefined
547+
})
548+
549+
it('should sample normally when resource rules exist and resource name is set', () => {
550+
prioritySampler.configure('test', {
551+
sampleRate: 0,
552+
rules: [{ resource: 'GET /api/*', sampleRate: 1 }]
553+
})
554+
555+
span.context()._tags = { resource: 'GET /api/users' }
556+
557+
prioritySampler.sample(span)
558+
559+
expect(span.context()._sampling.priority).to.equal(USER_KEEP)
560+
})
561+
562+
it('should sample normally when no resource rules exist', () => {
563+
prioritySampler.configure('test', {
564+
sampleRate: 1,
565+
rules: [{ service: 'my-service', sampleRate: 1 }]
566+
})
567+
568+
span.context()._tags = {}
569+
570+
prioritySampler.sample(span)
571+
572+
expect(span.context()._sampling.priority).to.equal(USER_KEEP)
573+
})
574+
})
530575
})

0 commit comments

Comments
 (0)