Skip to content

Commit 3915c1e

Browse files
committed
feat(openfeature): discover local EVP proxy routes
1 parent 82d8c08 commit 3915c1e

7 files changed

Lines changed: 169 additions & 14 deletions

File tree

packages/dd-trace/src/openfeature/constants/constants.js

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

33
module.exports = {
4+
/**
5+
* @constant
6+
* @type {string[]}
7+
* Supported local EVP proxy paths in preference order.
8+
*/
9+
EVP_PROXY_AGENT_BASE_PATHS: ['/evp_proxy/v4', '/evp_proxy/v2'],
10+
411
/**
512
* @constant
613
* @type {string} Base path for EVP proxy agent endpoint

packages/dd-trace/src/openfeature/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ function enable (config) {
4444
exposureSubmitCh.subscribe(_handleExposureSubmit)
4545
flushCh.subscribe(_handleFlush)
4646

47-
setAgentStrategy(config, hasAgent => {
48-
exposuresWriter?.setEnabled(hasAgent)
47+
setAgentStrategy(config, (hasAgent, basePath) => {
48+
exposuresWriter?.setEnabled(hasAgent, basePath)
4949
})
5050
}
5151

packages/dd-trace/src/openfeature/writers/exposures.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,18 @@ class ExposuresWriter extends BaseFFEWriter {
9595

9696
/**
9797
* @param {boolean} enabled - Whether to enable the writer
98+
* @param {string} [basePath] - Selected local EVP proxy base path
99+
* @returns {void}
98100
*/
99-
setEnabled (enabled) {
101+
setEnabled (enabled, basePath) {
100102
this.#enabled = enabled
101103

104+
if (basePath) {
105+
const normalizedBasePath = basePath.replace(/\/+$/, '')
106+
const endpoint = EXPOSURES_ENDPOINT.replace(/^\/+/, '')
107+
this._requestOptions.path = `${normalizedBasePath}/${endpoint}`
108+
}
109+
102110
if (enabled && this.#pendingEvents.length > 0) {
103111
// Flush all pending events as a batch
104112
super.append(this.#pendingEvents)

packages/dd-trace/src/openfeature/writers/util.js

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

33
const logger = require('../../log')
4-
const { EVP_PROXY_AGENT_BASE_PATH } = require('../constants/constants')
54
const { fetchAgentInfo } = require('../../agent/info')
5+
const { EVP_PROXY_AGENT_BASE_PATHS } = require('../constants/constants')
6+
7+
const TRAILING_SLASHES = /\/+$/
8+
9+
let missingRouteWarningLogged = false
10+
11+
/**
12+
* Selects the newest supported local EVP proxy path.
13+
*
14+
* @param {unknown} endpoints
15+
* @returns {string | undefined}
16+
*/
17+
function selectAgentEVPBasePath (endpoints) {
18+
if (!Array.isArray(endpoints)) return
19+
20+
for (const supportedPath of EVP_PROXY_AGENT_BASE_PATHS) {
21+
for (const endpoint of endpoints) {
22+
if (typeof endpoint === 'string' && endpoint.replace(TRAILING_SLASHES, '') === supportedPath) {
23+
return supportedPath
24+
}
25+
}
26+
}
27+
}
28+
29+
/**
30+
* Logs the unavailable direct-delivery warning once.
31+
*
32+
* @returns {void}
33+
*/
34+
function warnDirectDeliveryUnavailable () {
35+
if (missingRouteWarningLogged) return
36+
missingRouteWarningLogged = true
37+
logger.warn(
38+
'Feature Flags exposure delivery is disabled because no compatible local EVP route is available. ' +
39+
'Direct delivery is forthcoming.'
40+
)
41+
}
642

743
/**
844
* Determines if the agent supports EVP proxy and sets the writer enabled state accordingly
945
* @param {import('../../config')} config - Tracer configuration object
10-
* @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
46+
* @param {(enabled: boolean, basePath?: string) => void} setWriterEnabledValue
47+
* Callback to set the writer route and enabled state
48+
* @returns {void}
1149
*/
1250
function setAgentStrategy (config, setWriterEnabledValue) {
1351
fetchAgentInfo(config.url, (err, agentInfo) => {
1452
if (err) {
15-
logger.debug('FFE Writer disabled - error getting agent info:', err.message)
53+
logger.debug('FFE Writer disabled - error getting agent info: %s', err.message)
54+
warnDirectDeliveryUnavailable()
1655
setWriterEnabledValue(false)
1756
return
1857
}
1958

20-
const endpoints = agentInfo.endpoints
21-
const normalizedPath = EVP_PROXY_AGENT_BASE_PATH.replace(/\/+$/, '')
22-
const hasEndpoint = Array.isArray(endpoints) &&
23-
endpoints.includes(normalizedPath) || endpoints.includes(normalizedPath + '/')
59+
const basePath = selectAgentEVPBasePath(agentInfo?.endpoints)
2460

25-
if (hasEndpoint) {
26-
logger.debug('FFE Writer enabled - agent has EVP proxy support')
27-
setWriterEnabledValue(true)
61+
if (basePath) {
62+
logger.debug('FFE Writer enabled - agent supports EVP proxy path %s', basePath)
63+
setWriterEnabledValue(true, basePath)
2864
} else {
29-
logger.debug('FFE Writer disabled - agent does not have EVP proxy support')
65+
warnDirectDeliveryUnavailable()
3066
setWriterEnabledValue(false)
3167
}
3268
})

packages/dd-trace/test/openfeature/index.spec.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ describe('OpenFeature Module', () => {
5555
sinon.assert.calledOnce(setAgentStrategyStub)
5656
})
5757

58+
it('configures the writer with the discovered Agent route', () => {
59+
openfeatureModule.enable(config)
60+
const setWriterEnabled = setAgentStrategyStub.firstCall.args[1]
61+
setWriterEnabled(true, '/evp_proxy/v4')
62+
63+
sinon.assert.calledOnceWithExactly(mockWriter.setEnabled, true, '/evp_proxy/v4')
64+
})
65+
5866
it('should handle multiple enable calls gracefully', () => {
5967
openfeatureModule.enable(config)
6068
openfeatureModule.enable(config)

packages/dd-trace/test/openfeature/writers/exposures.spec.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,16 @@ describe('OpenFeature Exposures Writer', () => {
364364
assert.strictEqual(parsedPayload.context.service, 'test-service')
365365
})
366366

367+
it('should flush events through the selected EVP v4 proxy path', () => {
368+
writer.setEnabled(true, '/evp_proxy/v4/')
369+
writer.append(exposureEvent)
370+
371+
writer.flush()
372+
373+
const [, options] = request.getCall(0).args
374+
assert.strictEqual(options.path, '/evp_proxy/v4/api/v2/exposures')
375+
})
376+
367377
it('should empty buffer after flushing', () => {
368378
writer.append(exposureEvent)
369379
assert.strictEqual(writer._buffer?.length, 1)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use strict'
2+
3+
const assert = require('node:assert/strict')
4+
const { format } = require('node:util')
5+
6+
const { describe, it, beforeEach } = require('mocha')
7+
const proxyquire = require('proxyquire').noPreserveCache()
8+
const sinon = require('sinon')
9+
10+
require('../../setup/core')
11+
12+
describe('OpenFeature writer Agent strategy', () => {
13+
let fetchAgentInfo
14+
let log
15+
let setAgentStrategy
16+
let setWriterEnabledValue
17+
18+
beforeEach(() => {
19+
fetchAgentInfo = sinon.stub()
20+
log = {
21+
debug: sinon.spy(),
22+
warn: sinon.spy(),
23+
}
24+
setWriterEnabledValue = sinon.spy()
25+
26+
setAgentStrategy = proxyquire('../../../src/openfeature/writers/util', {
27+
'../../agent/info': { fetchAgentInfo },
28+
'../../log': log,
29+
}).setAgentStrategy
30+
})
31+
32+
it('prefers EVP v4 when the Agent advertises v4 and v2', () => {
33+
fetchAgentInfo.yields(null, {
34+
endpoints: ['/evp_proxy/v2/', '/evp_proxy/v4/'],
35+
})
36+
37+
setAgentStrategy({ url: new URL('http://localhost:8126') }, setWriterEnabledValue)
38+
39+
sinon.assert.calledOnceWithExactly(setWriterEnabledValue, true, '/evp_proxy/v4')
40+
sinon.assert.notCalled(log.warn)
41+
})
42+
43+
it('uses EVP v2 when the Agent does not advertise v4', () => {
44+
fetchAgentInfo.yields(null, {
45+
endpoints: ['/evp_proxy/v2/'],
46+
})
47+
48+
setAgentStrategy({ url: new URL('http://localhost:8126') }, setWriterEnabledValue)
49+
50+
sinon.assert.calledOnceWithExactly(setWriterEnabledValue, true, '/evp_proxy/v2')
51+
sinon.assert.notCalled(log.warn)
52+
})
53+
54+
it('disables delivery and warns when Agent discovery fails', () => {
55+
fetchAgentInfo.yields(new Error('connect ECONNREFUSED'))
56+
57+
setAgentStrategy({ url: new URL('http://localhost:8126') }, setWriterEnabledValue)
58+
59+
sinon.assert.calledOnceWithExactly(setWriterEnabledValue, false)
60+
sinon.assert.calledOnce(log.warn)
61+
assert.match(format(...log.warn.firstCall.args), /Direct delivery is forthcoming/)
62+
})
63+
64+
it('disables delivery and warns when the Agent has no compatible EVP route', () => {
65+
fetchAgentInfo.yields(null, {
66+
endpoints: ['/v0.4/traces'],
67+
})
68+
69+
setAgentStrategy({ url: new URL('http://localhost:8126') }, setWriterEnabledValue)
70+
71+
sinon.assert.calledOnceWithExactly(setWriterEnabledValue, false)
72+
sinon.assert.calledOnce(log.warn)
73+
assert.match(format(...log.warn.firstCall.args), /no compatible local EVP route/)
74+
})
75+
76+
it('logs the unavailable direct-delivery warning once', () => {
77+
fetchAgentInfo.yields(new Error('connect ECONNREFUSED'))
78+
const config = { url: new URL('http://localhost:8126') }
79+
80+
setAgentStrategy(config, setWriterEnabledValue)
81+
setAgentStrategy(config, setWriterEnabledValue)
82+
83+
sinon.assert.calledTwice(setWriterEnabledValue)
84+
sinon.assert.calledOnce(log.warn)
85+
})
86+
})

0 commit comments

Comments
 (0)