dd-trace 5.70.0 → 5.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-3rdparty.csv +5 -0
- package/index.d.ts +110 -1
- package/initialize.mjs +7 -1
- package/package.json +21 -2
- package/packages/datadog-instrumentations/src/anthropic.js +115 -0
- package/packages/datadog-instrumentations/src/azure-event-hubs.js +37 -0
- package/packages/datadog-instrumentations/src/azure-functions.js +3 -0
- package/packages/datadog-instrumentations/src/cucumber.js +7 -7
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/jest.js +29 -36
- package/packages/datadog-instrumentations/src/mocha/main.js +8 -9
- package/packages/datadog-instrumentations/src/mocha/utils.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/worker.js +2 -2
- package/packages/datadog-instrumentations/src/pg.js +1 -1
- package/packages/datadog-instrumentations/src/playwright.js +5 -5
- package/packages/datadog-instrumentations/src/vitest.js +8 -8
- package/packages/datadog-plugin-anthropic/src/index.js +17 -0
- package/packages/datadog-plugin-anthropic/src/tracing.js +30 -0
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +73 -27
- package/packages/datadog-plugin-azure-event-hubs/src/index.js +15 -0
- package/packages/datadog-plugin-azure-event-hubs/src/producer.js +82 -0
- package/packages/datadog-plugin-azure-functions/src/index.js +37 -0
- package/packages/datadog-plugin-cucumber/src/index.js +3 -3
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +9 -9
- package/packages/datadog-plugin-jest/src/util.js +10 -2
- package/packages/datadog-plugin-mocha/src/index.js +2 -2
- package/packages/datadog-plugin-playwright/src/index.js +2 -2
- package/packages/datadog-plugin-vitest/src/index.js +2 -2
- package/packages/datadog-plugin-ws/src/server.js +5 -3
- package/packages/dd-trace/src/appsec/reporter.js +70 -21
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +1 -1
- package/packages/dd-trace/src/config.js +110 -26
- package/packages/dd-trace/src/config_defaults.js +14 -0
- package/packages/dd-trace/src/git_properties.js +90 -5
- package/packages/dd-trace/src/llmobs/plugins/anthropic.js +282 -0
- package/packages/dd-trace/src/llmobs/tagger.js +35 -0
- package/packages/dd-trace/src/noop/proxy.js +3 -0
- package/packages/dd-trace/src/openfeature/constants/constants.js +51 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +45 -0
- package/packages/dd-trace/src/openfeature/index.js +77 -0
- package/packages/dd-trace/src/openfeature/noop.js +101 -0
- package/packages/dd-trace/src/openfeature/writers/base.js +181 -0
- package/packages/dd-trace/src/openfeature/writers/exposures.js +173 -0
- package/packages/dd-trace/src/openfeature/writers/util.js +43 -0
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +100 -0
- package/packages/dd-trace/src/opentelemetry/logs/index.js +87 -0
- package/packages/dd-trace/src/opentelemetry/logs/logger.js +77 -0
- package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +126 -0
- package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +173 -0
- package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +367 -0
- package/packages/dd-trace/src/opentelemetry/protos/common.proto +116 -0
- package/packages/dd-trace/src/opentelemetry/protos/logs.proto +226 -0
- package/packages/dd-trace/src/opentelemetry/protos/logs_service.proto +78 -0
- package/packages/dd-trace/src/opentelemetry/protos/protobuf_loader.js +48 -0
- package/packages/dd-trace/src/opentelemetry/protos/resource.proto +45 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +7 -6
- package/packages/dd-trace/src/plugins/index.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +6 -5
- package/packages/dd-trace/src/profiling/config.js +21 -1
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -2
- package/packages/dd-trace/src/profiling/profiler.js +44 -22
- package/packages/dd-trace/src/profiling/profilers/events.js +12 -3
- package/packages/dd-trace/src/profiling/profilers/space.js +35 -24
- package/packages/dd-trace/src/profiling/profilers/wall.js +14 -6
- package/packages/dd-trace/src/proxy.js +22 -1
- package/packages/dd-trace/src/remote_config/capabilities.js +2 -0
- package/packages/dd-trace/src/remote_config/index.js +3 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +4 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +8 -0
- package/packages/dd-trace/src/supported-configurations.json +18 -0
- package/packages/dd-trace/src/telemetry/telemetry.js +13 -1
- package/register.js +9 -1
|
@@ -268,6 +268,32 @@ class LLMObsTagger {
|
|
|
268
268
|
return filteredToolCalls
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
#filterToolResults (toolResults) {
|
|
272
|
+
if (!Array.isArray(toolResults)) {
|
|
273
|
+
toolResults = [toolResults]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const filteredToolResults = []
|
|
277
|
+
for (const toolResult of toolResults) {
|
|
278
|
+
if (typeof toolResult !== 'object') {
|
|
279
|
+
this.#handleFailure('Tool result must be an object.', 'invalid_io_messages')
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const { result, toolId, type } = toolResult
|
|
284
|
+
const toolResultObj = {}
|
|
285
|
+
|
|
286
|
+
const condition1 = this.#tagConditionalString(result, 'Tool result', toolResultObj, 'result')
|
|
287
|
+
const condition2 = this.#tagConditionalString(toolId, 'Tool ID', toolResultObj, 'tool_id')
|
|
288
|
+
const condition3 = this.#tagConditionalString(type, 'Tool type', toolResultObj, 'type')
|
|
289
|
+
|
|
290
|
+
if (condition1 && condition2 && condition3) {
|
|
291
|
+
filteredToolResults.push(toolResultObj)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return filteredToolResults
|
|
295
|
+
}
|
|
296
|
+
|
|
271
297
|
#tagMessages (span, data, key) {
|
|
272
298
|
if (!data) {
|
|
273
299
|
return
|
|
@@ -290,6 +316,7 @@ class LLMObsTagger {
|
|
|
290
316
|
|
|
291
317
|
const { content = '', role } = message
|
|
292
318
|
const toolCalls = message.toolCalls
|
|
319
|
+
const toolResults = message.toolResults
|
|
293
320
|
const toolId = message.toolId
|
|
294
321
|
const messageObj = { content }
|
|
295
322
|
|
|
@@ -308,6 +335,14 @@ class LLMObsTagger {
|
|
|
308
335
|
}
|
|
309
336
|
}
|
|
310
337
|
|
|
338
|
+
if (toolResults) {
|
|
339
|
+
const filteredToolResults = this.#filterToolResults(toolResults)
|
|
340
|
+
|
|
341
|
+
if (filteredToolResults.length) {
|
|
342
|
+
messageObj.tool_results = filteredToolResults
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
311
346
|
if (toolId) {
|
|
312
347
|
if (role === 'tool') {
|
|
313
348
|
condition = this.#tagConditionalString(toolId, 'Tool ID', messageObj, 'tool_id')
|
|
@@ -4,12 +4,14 @@ const NoopTracer = require('./tracer')
|
|
|
4
4
|
const NoopAppsecSdk = require('../appsec/sdk/noop')
|
|
5
5
|
const NoopDogStatsDClient = require('./dogstatsd')
|
|
6
6
|
const NoopLLMObsSDK = require('../llmobs/noop')
|
|
7
|
+
const NoopFlaggingProvider = require('../openfeature/noop')
|
|
7
8
|
const NoopAIGuardSDK = require('../aiguard/noop')
|
|
8
9
|
|
|
9
10
|
const noop = new NoopTracer()
|
|
10
11
|
const noopAppsec = new NoopAppsecSdk()
|
|
11
12
|
const noopDogStatsDClient = new NoopDogStatsDClient()
|
|
12
13
|
const noopLLMObs = new NoopLLMObsSDK(noop)
|
|
14
|
+
const noopOpenFeatureProvider = new NoopFlaggingProvider()
|
|
13
15
|
const noopAIGuard = new NoopAIGuardSDK()
|
|
14
16
|
|
|
15
17
|
/** @type {import('../../src/index')} Proxy */
|
|
@@ -19,6 +21,7 @@ class NoopProxy {
|
|
|
19
21
|
this.appsec = noopAppsec
|
|
20
22
|
this.dogstatsd = noopDogStatsDClient
|
|
21
23
|
this.llmobs = noopLLMObs
|
|
24
|
+
this.openfeature = noopOpenFeatureProvider
|
|
22
25
|
this.aiguard = noopAIGuard
|
|
23
26
|
this.setBaggageItem = () => {}
|
|
24
27
|
this.getBaggageItem = () => {}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
/**
|
|
5
|
+
* @constant
|
|
6
|
+
* @type {string} Base path for EVP proxy agent endpoint
|
|
7
|
+
*/
|
|
8
|
+
EVP_PROXY_AGENT_BASE_PATH: '/evp_proxy/v2/',
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @constant
|
|
12
|
+
* @type {string} HTTP header name for EVP subdomain routing
|
|
13
|
+
*/
|
|
14
|
+
EVP_SUBDOMAIN_HEADER_NAME: 'X-Datadog-EVP-Subdomain',
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @constant
|
|
18
|
+
* @type {string} EVP subdomain value for event platform intake
|
|
19
|
+
*/
|
|
20
|
+
EVP_SUBDOMAIN_VALUE: 'event-platform-intake',
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @constant
|
|
24
|
+
* @type {string} API endpoint for exposure events EVP track
|
|
25
|
+
*/
|
|
26
|
+
EXPOSURES_ENDPOINT: '/api/v2/exposures',
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @constant
|
|
30
|
+
* @type {number} Maximum payload size for EVP intake (5MB, actual limit is 5.1MB)
|
|
31
|
+
*/
|
|
32
|
+
EVP_PAYLOAD_SIZE_LIMIT: 5 << 20,
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @constant
|
|
36
|
+
* @type {number} Maximum individual event size (999KB, actual limit is 1MB)
|
|
37
|
+
*/
|
|
38
|
+
EVP_EVENT_SIZE_LIMIT: (1 << 20) - 1024,
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @constant
|
|
42
|
+
* @type {string} Channel name for exposure event submission
|
|
43
|
+
*/
|
|
44
|
+
EXPOSURE_CHANNEL: 'ffe:exposure:submit',
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @constant
|
|
48
|
+
* @type {string} Reason code for noop provider evaluations
|
|
49
|
+
*/
|
|
50
|
+
NOOP_REASON: 'STATIC'
|
|
51
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { DatadogNodeServerProvider } = require('@datadog/openfeature-node-server')
|
|
4
|
+
const { channel } = require('dc-polyfill')
|
|
5
|
+
const log = require('../log')
|
|
6
|
+
const { EXPOSURE_CHANNEL } = require('./constants/constants')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OpenFeature provider that integrates with Datadog's feature flagging system.
|
|
10
|
+
* Extends DatadogNodeServerProvider to add tracer integration and configuration management.
|
|
11
|
+
*/
|
|
12
|
+
class FlaggingProvider extends DatadogNodeServerProvider {
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('../tracer')} tracer - Datadog tracer instance
|
|
15
|
+
* @param {import('../config')} config - Tracer configuration object
|
|
16
|
+
*/
|
|
17
|
+
constructor (tracer, config) {
|
|
18
|
+
// Call parent constructor with required options
|
|
19
|
+
super({
|
|
20
|
+
exposureChannel: channel(EXPOSURE_CHANNEL)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
this._tracer = tracer
|
|
24
|
+
this._config = config
|
|
25
|
+
|
|
26
|
+
log.debug(this.constructor.name + ' created')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Internal method to update flag configuration from Remote Config.
|
|
31
|
+
* This method is called automatically when Remote Config delivers UFC updates.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
* @param {import('@datadog/openfeature-node-server').UniversalFlagConfigurationV1} ufc
|
|
35
|
+
* - Universal Flag Configuration object
|
|
36
|
+
*/
|
|
37
|
+
_setConfiguration (ufc) {
|
|
38
|
+
if (typeof this.setConfiguration === 'function') {
|
|
39
|
+
this.setConfiguration(ufc)
|
|
40
|
+
}
|
|
41
|
+
log.debug(this.constructor.name + ' provider configuration updated')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = FlaggingProvider
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../log')
|
|
4
|
+
const ExposuresWriter = require('./writers/exposures')
|
|
5
|
+
const { setAgentStrategy } = require('./writers/util')
|
|
6
|
+
const { channel } = require('dc-polyfill')
|
|
7
|
+
|
|
8
|
+
const exposureSubmitCh = channel('ffe:exposure:submit')
|
|
9
|
+
const flushCh = channel('ffe:writers:flush')
|
|
10
|
+
|
|
11
|
+
let exposuresWriter = null
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @private
|
|
15
|
+
* @param {Object|Array<Object>} exposureEvents - Exposure events channel subscriber
|
|
16
|
+
* @returns {void}
|
|
17
|
+
*/
|
|
18
|
+
function _handleExposureSubmit (exposureEvents) {
|
|
19
|
+
if (!exposuresWriter) return
|
|
20
|
+
exposuresWriter.append(exposureEvents)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Channel subscriber for manually flushing the exposures writer
|
|
25
|
+
* @private
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
function _handleFlush () {
|
|
29
|
+
exposuresWriter?.flush()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Enables the OpenFeature module and sets up FF&E writer and channel subscribers
|
|
34
|
+
* @param {import('../config')} config - Tracer configuration object
|
|
35
|
+
* @returns {void}
|
|
36
|
+
*/
|
|
37
|
+
function enable (config) {
|
|
38
|
+
if (exposuresWriter) {
|
|
39
|
+
log.warn(exposuresWriter.constructor.name + ' already enabled')
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
exposuresWriter = new ExposuresWriter(config)
|
|
44
|
+
exposureSubmitCh.subscribe(_handleExposureSubmit)
|
|
45
|
+
flushCh.subscribe(_handleFlush)
|
|
46
|
+
|
|
47
|
+
setAgentStrategy(config, hasAgent => {
|
|
48
|
+
exposuresWriter?.setEnabled(hasAgent)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
log.debug('OpenFeature module enabled')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Disables the OpenFeature module and cleans up resources
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
58
|
+
function disable () {
|
|
59
|
+
if (!exposuresWriter) return
|
|
60
|
+
|
|
61
|
+
if (exposureSubmitCh.hasSubscribers) {
|
|
62
|
+
exposureSubmitCh.unsubscribe(_handleExposureSubmit)
|
|
63
|
+
}
|
|
64
|
+
if (flushCh.hasSubscribers) {
|
|
65
|
+
flushCh.unsubscribe(_handleFlush)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
exposuresWriter.destroy?.()
|
|
69
|
+
exposuresWriter = null
|
|
70
|
+
|
|
71
|
+
log.debug('OpenFeature module disabled')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
enable,
|
|
76
|
+
disable
|
|
77
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { NOOP_REASON } = require('./constants/constants')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* No-op implementation of OpenFeature provider that always returns default values.
|
|
7
|
+
* Used when the OpenFeature provider is not initialized or disabled.
|
|
8
|
+
* https://openfeature.dev/docs/reference/concepts/provider/
|
|
9
|
+
*/
|
|
10
|
+
class NoopFlaggingProvider {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} [noopTracer] - Optional noop tracer instance
|
|
13
|
+
*/
|
|
14
|
+
constructor (noopTracer) {
|
|
15
|
+
this._tracer = noopTracer
|
|
16
|
+
this._config = {}
|
|
17
|
+
this.metadata = { name: 'NoopFlaggingProvider' }
|
|
18
|
+
this.status = 'NOT_READY'
|
|
19
|
+
this.runsOn = 'server'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} flagKey - Flag key
|
|
24
|
+
* @param {boolean} defaultValue - Default value to return
|
|
25
|
+
* @param {Object} context - Evaluation context
|
|
26
|
+
* @param {Object} logger - Logger instance
|
|
27
|
+
* @returns {Promise<{value: boolean, reason: string}>} Resolution details
|
|
28
|
+
*/
|
|
29
|
+
resolveBooleanEvaluation (flagKey, defaultValue, context, logger) {
|
|
30
|
+
return Promise.resolve({
|
|
31
|
+
value: defaultValue,
|
|
32
|
+
reason: NOOP_REASON
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} flagKey - Flag key
|
|
38
|
+
* @param {string} defaultValue - Default value to return
|
|
39
|
+
* @param {Object} context - Evaluation context
|
|
40
|
+
* @param {Object} logger - Logger instance
|
|
41
|
+
* @returns {Promise<{value: string, reason: string}>} Resolution details
|
|
42
|
+
*/
|
|
43
|
+
resolveStringEvaluation (flagKey, defaultValue, context, logger) {
|
|
44
|
+
return Promise.resolve({
|
|
45
|
+
value: defaultValue,
|
|
46
|
+
reason: NOOP_REASON
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} flagKey - Flag key
|
|
52
|
+
* @param {number} defaultValue - Default value to return
|
|
53
|
+
* @param {Object} context - Evaluation context
|
|
54
|
+
* @param {Object} logger - Logger instance
|
|
55
|
+
* @returns {Promise<{value: number, reason: string}>} Resolution details
|
|
56
|
+
*/
|
|
57
|
+
resolveNumberEvaluation (flagKey, defaultValue, context, logger) {
|
|
58
|
+
return Promise.resolve({
|
|
59
|
+
value: defaultValue,
|
|
60
|
+
reason: NOOP_REASON,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} flagKey - Flag key
|
|
66
|
+
* @param {Object} defaultValue - Default value to return
|
|
67
|
+
* @param {Object} context - Evaluation context
|
|
68
|
+
* @param {Object} logger - Logger instance
|
|
69
|
+
* @returns {Promise<{value: Object, reason: string}>} Resolution details
|
|
70
|
+
*/
|
|
71
|
+
resolveObjectEvaluation (flagKey, defaultValue, context, logger) {
|
|
72
|
+
return Promise.resolve({
|
|
73
|
+
value: defaultValue,
|
|
74
|
+
reason: NOOP_REASON
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @returns {Object} Current configuration
|
|
80
|
+
*/
|
|
81
|
+
getConfiguration () {
|
|
82
|
+
return this._config
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {Object} config - Configuration to set
|
|
87
|
+
*/
|
|
88
|
+
setConfiguration (config) {
|
|
89
|
+
this._config = config
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @internal
|
|
94
|
+
* @param {Object} ufc - Universal Flag Configuration object
|
|
95
|
+
*/
|
|
96
|
+
_setConfiguration (ufc) {
|
|
97
|
+
this.setConfiguration(ufc)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = NoopFlaggingProvider
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const request = require('../../exporters/common/request')
|
|
4
|
+
const { safeJSONStringify } = require('../../exporters/common/util')
|
|
5
|
+
const { URL, format } = require('node:url')
|
|
6
|
+
|
|
7
|
+
const log = require('../../log')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} BaseFFEWriterOptions
|
|
11
|
+
* @property {number} [interval] - Flush interval in milliseconds
|
|
12
|
+
* @property {number} [timeout] - Request timeout in milliseconds
|
|
13
|
+
* @property {Object} config - Tracer configuration object
|
|
14
|
+
* @property {string} endpoint - API endpoint path
|
|
15
|
+
* @property {URL} [agentUrl] - Base URL for the agent
|
|
16
|
+
* @property {number} [payloadSizeLimit] - Maximum payload size in bytes
|
|
17
|
+
* @property {number} [eventSizeLimit] - Maximum individual event size in bytes
|
|
18
|
+
* @property {Object} [headers] - Additional HTTP headers
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* BaseFFEWriter is the base class for sending Feature Flagging & Exposure Events payloads to the Datadog Agent.
|
|
23
|
+
* @class BaseFFEWriter
|
|
24
|
+
*/
|
|
25
|
+
class BaseFFEWriter {
|
|
26
|
+
/**
|
|
27
|
+
* @param {BaseFFEWriterOptions} options - Writer configuration options
|
|
28
|
+
*/
|
|
29
|
+
constructor ({ interval, timeout, config, endpoint, agentUrl, payloadSizeLimit, eventSizeLimit, headers }) {
|
|
30
|
+
this._interval = interval ?? 1000
|
|
31
|
+
this._timeout = timeout ?? 5000
|
|
32
|
+
|
|
33
|
+
this._buffer = []
|
|
34
|
+
this._bufferLimit = 1000
|
|
35
|
+
this._bufferSize = 0
|
|
36
|
+
|
|
37
|
+
this._config = config
|
|
38
|
+
this._endpoint = endpoint
|
|
39
|
+
this._baseUrl = agentUrl ?? this._getAgentUrl()
|
|
40
|
+
this._payloadSizeLimit = payloadSizeLimit
|
|
41
|
+
this._eventSizeLimit = eventSizeLimit
|
|
42
|
+
this._headers = headers || {}
|
|
43
|
+
|
|
44
|
+
this._requestOptions = {
|
|
45
|
+
headers: {
|
|
46
|
+
...this._headers,
|
|
47
|
+
'Content-Type': 'application/json'
|
|
48
|
+
},
|
|
49
|
+
method: 'POST',
|
|
50
|
+
timeout: this._timeout,
|
|
51
|
+
url: this._baseUrl,
|
|
52
|
+
path: this._endpoint
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this._periodic = setInterval(() => {
|
|
56
|
+
this.flush()
|
|
57
|
+
}, this._interval).unref()
|
|
58
|
+
|
|
59
|
+
this._beforeExitHandler = () => {
|
|
60
|
+
this.destroy()
|
|
61
|
+
}
|
|
62
|
+
process.once('beforeExit', this._beforeExitHandler)
|
|
63
|
+
|
|
64
|
+
this._destroyed = false
|
|
65
|
+
this._droppedEvents = 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Appends an event array to the buffer
|
|
70
|
+
* @param {Array|Object} events - Event object(s) to append to buffer
|
|
71
|
+
*/
|
|
72
|
+
append (events) {
|
|
73
|
+
const eventArray = Array.isArray(events) ? events : [events]
|
|
74
|
+
|
|
75
|
+
for (const event of eventArray) {
|
|
76
|
+
if (this._buffer.length >= this._bufferLimit) {
|
|
77
|
+
log.warn(`${this.constructor.name} event buffer full (limit is ${this._bufferLimit}), dropping event`)
|
|
78
|
+
this._droppedEvents++
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const eventSizeBytes = Buffer.byteLength(JSON.stringify(event))
|
|
83
|
+
|
|
84
|
+
// Check individual event size limit if configured
|
|
85
|
+
if (this._eventSizeLimit && eventSizeBytes > this._eventSizeLimit) {
|
|
86
|
+
log.warn(`${this.constructor.name} event size
|
|
87
|
+
${eventSizeBytes} bytes exceeds limit ${this._eventSizeLimit}, dropping event`)
|
|
88
|
+
this._droppedEvents++
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if adding this event would exceed payload size limit if configured
|
|
93
|
+
if (this._payloadSizeLimit && this._bufferSize + eventSizeBytes > this._payloadSizeLimit) {
|
|
94
|
+
log.debug(() => `${this.constructor.name}
|
|
95
|
+
buffer size would exceed ${this._payloadSizeLimit} bytes, flushing first`)
|
|
96
|
+
this.flush()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this._bufferSize += eventSizeBytes
|
|
100
|
+
this._buffer.push(event)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Flushes all buffered events to the agent
|
|
106
|
+
*/
|
|
107
|
+
flush () {
|
|
108
|
+
if (this._buffer.length === 0) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
const events = this._buffer
|
|
112
|
+
this._buffer = []
|
|
113
|
+
this._bufferSize = 0
|
|
114
|
+
|
|
115
|
+
const payload = this._encode(this.makePayload(events))
|
|
116
|
+
|
|
117
|
+
log.debug(() => `${this.constructor.name} flushing payload: ${safeJSONStringify(payload)}`)
|
|
118
|
+
|
|
119
|
+
request(payload, this._requestOptions, (err, resp, code) => {
|
|
120
|
+
if (err) {
|
|
121
|
+
log.error(`Failed to send events to ${this._baseUrl.href}${this._endpoint}: ${err.message}`)
|
|
122
|
+
} else if (code >= 200 && code < 300) {
|
|
123
|
+
log.debug(() => `Successfully sent ${events.length} events`)
|
|
124
|
+
} else {
|
|
125
|
+
log.warn(`Events request returned status ${code}`)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Override in subclass to customize payload structure
|
|
132
|
+
* @param {Array} events - Array of events to be sent
|
|
133
|
+
* @returns {object} Formatted payload
|
|
134
|
+
*/
|
|
135
|
+
makePayload (events) {
|
|
136
|
+
// Override in subclass
|
|
137
|
+
return events
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Cleans up resources and flushes remaining events
|
|
142
|
+
*/
|
|
143
|
+
destroy () {
|
|
144
|
+
if (!this._destroyed) {
|
|
145
|
+
log.debug(() => `Stopping ${this.constructor.name}`)
|
|
146
|
+
clearInterval(this._periodic)
|
|
147
|
+
process.removeListener('beforeExit', this._beforeExitHandler)
|
|
148
|
+
this.flush()
|
|
149
|
+
this._destroyed = true
|
|
150
|
+
|
|
151
|
+
if (this._droppedEvents > 0) {
|
|
152
|
+
log.warn(`${this.constructor.name} dropped ${this._droppedEvents} events due to buffer overflow`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @private
|
|
159
|
+
* @returns {URL} Constructs agent URL from config
|
|
160
|
+
*/
|
|
161
|
+
_getAgentUrl () {
|
|
162
|
+
const { hostname, port } = this._config
|
|
163
|
+
|
|
164
|
+
return this._config.url ?? new URL(format({
|
|
165
|
+
protocol: 'http:',
|
|
166
|
+
hostname: hostname || 'localhost',
|
|
167
|
+
port: port || 8126
|
|
168
|
+
}))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @private
|
|
173
|
+
* @param {Array<object>} payload - Payload to encode
|
|
174
|
+
* @returns {string} JSON-stringified payload
|
|
175
|
+
*/
|
|
176
|
+
_encode (payload) {
|
|
177
|
+
return JSON.stringify(payload)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = BaseFFEWriter
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const BaseFFEWriter = require('./base')
|
|
4
|
+
const {
|
|
5
|
+
EXPOSURES_ENDPOINT,
|
|
6
|
+
EVP_PROXY_AGENT_BASE_PATH,
|
|
7
|
+
EVP_SUBDOMAIN_HEADER_NAME,
|
|
8
|
+
EVP_SUBDOMAIN_VALUE,
|
|
9
|
+
EVP_PAYLOAD_SIZE_LIMIT,
|
|
10
|
+
EVP_EVENT_SIZE_LIMIT
|
|
11
|
+
} = require('../constants/constants')
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} ExposureEvent
|
|
15
|
+
* @property {number} timestamp - Unix timestamp in milliseconds
|
|
16
|
+
* @property {Object} allocation - Allocation information
|
|
17
|
+
* @property {string} allocation.key - Allocation key
|
|
18
|
+
* @property {Object} flag - Flag information
|
|
19
|
+
* @property {string} flag.key - Flag key
|
|
20
|
+
* @property {Object} variant - Variant information
|
|
21
|
+
* @property {string} variant.key - Variant key
|
|
22
|
+
* @property {Object} subject - Subject (user/entity) information
|
|
23
|
+
* @property {string} subject.id - Subject identifier
|
|
24
|
+
* @property {string} [subject.type] - Subject type
|
|
25
|
+
* @property {Object} [subject.attributes] - Additional subject attributes
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} ExposureContext
|
|
30
|
+
* @property {string} service_name - Service name
|
|
31
|
+
* @property {string} [version] - Service version
|
|
32
|
+
* @property {string} [env] - Service environment
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} ExposureEventPayload
|
|
37
|
+
* @property {ExposureContext} context - Service context metadata
|
|
38
|
+
* @property {ExposureEvent[]} exposures - Formatted exposure events
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* ExposuresWriter is responsible for sending exposure events to the Datadog Agent.
|
|
43
|
+
*/
|
|
44
|
+
class ExposuresWriter extends BaseFFEWriter {
|
|
45
|
+
/**
|
|
46
|
+
* @param {import('../../config')} config - Tracer configuration object
|
|
47
|
+
*/
|
|
48
|
+
constructor (config) {
|
|
49
|
+
// Build full EVP endpoint path
|
|
50
|
+
const basePath = EVP_PROXY_AGENT_BASE_PATH.replace(/\/+$/, '')
|
|
51
|
+
const endpoint = EXPOSURES_ENDPOINT.replace(/^\/+/, '')
|
|
52
|
+
const fullEndpoint = `${basePath}/${endpoint}`
|
|
53
|
+
|
|
54
|
+
super({
|
|
55
|
+
config,
|
|
56
|
+
endpoint: fullEndpoint,
|
|
57
|
+
payloadSizeLimit: EVP_PAYLOAD_SIZE_LIMIT,
|
|
58
|
+
eventSizeLimit: EVP_EVENT_SIZE_LIMIT,
|
|
59
|
+
headers: {
|
|
60
|
+
[EVP_SUBDOMAIN_HEADER_NAME]: EVP_SUBDOMAIN_VALUE
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
this._enabled = false // Start disabled until agent strategy is set
|
|
64
|
+
this._pendingEvents = [] // Buffer events until enabled
|
|
65
|
+
this._context = this._buildContext()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {boolean} enabled - Whether to enable the writer
|
|
70
|
+
*/
|
|
71
|
+
setEnabled (enabled) {
|
|
72
|
+
this._enabled = enabled
|
|
73
|
+
|
|
74
|
+
if (enabled && this._pendingEvents.length > 0) {
|
|
75
|
+
// Flush all pending events as a batch
|
|
76
|
+
super.append(this._pendingEvents)
|
|
77
|
+
this._pendingEvents = []
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Appends exposure event(s) to the buffer
|
|
83
|
+
* @param {ExposureEvent|ExposureEvent[]} events - Exposure event(s) to append
|
|
84
|
+
*/
|
|
85
|
+
append (events) {
|
|
86
|
+
if (!this._enabled) {
|
|
87
|
+
// Buffer events until writer is ready
|
|
88
|
+
if (Array.isArray(events)) {
|
|
89
|
+
this._pendingEvents.push(...events)
|
|
90
|
+
} else {
|
|
91
|
+
this._pendingEvents.push(events)
|
|
92
|
+
}
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
super.append(events)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Flushes buffered exposure events to the agent
|
|
100
|
+
*/
|
|
101
|
+
flush () {
|
|
102
|
+
if (!this._enabled) {
|
|
103
|
+
// Don't flush when disabled
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
super.flush()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Formats exposure events with service context metadata
|
|
111
|
+
* @param {Array<ExposureEvent>} events - Array of exposure events
|
|
112
|
+
* @returns {ExposureEventPayload} Formatted payload with service context
|
|
113
|
+
*/
|
|
114
|
+
makePayload (events) {
|
|
115
|
+
const formattedEvents = events.map(event => this._formatExposureEvent(event))
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
context: this._context,
|
|
119
|
+
exposures: formattedEvents
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Builds service context metadata
|
|
125
|
+
* @private
|
|
126
|
+
* @returns {ExposureContext} Service context
|
|
127
|
+
*/
|
|
128
|
+
_buildContext () {
|
|
129
|
+
const context = {
|
|
130
|
+
service_name: this._config.service || 'unknown'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Only include version and env if they are defined
|
|
134
|
+
if (this._config.version !== undefined) {
|
|
135
|
+
context.version = this._config.version
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (this._config.env !== undefined) {
|
|
139
|
+
context.env = this._config.env
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return context
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @private
|
|
147
|
+
* @param {ExposureEvent} event - Raw exposure event
|
|
148
|
+
* @returns {ExposureEvent} Formatted exposure event
|
|
149
|
+
*/
|
|
150
|
+
_formatExposureEvent (event) {
|
|
151
|
+
// Ensure the event matches the expected schema
|
|
152
|
+
const formattedEvent = {
|
|
153
|
+
timestamp: event.timestamp || Date.now(),
|
|
154
|
+
allocation: {
|
|
155
|
+
key: event.allocation?.key || event['allocation.key']
|
|
156
|
+
},
|
|
157
|
+
flag: {
|
|
158
|
+
key: event.flag?.key || event['flag.key']
|
|
159
|
+
},
|
|
160
|
+
variant: {
|
|
161
|
+
key: event.variant?.key || event['variant.key']
|
|
162
|
+
},
|
|
163
|
+
subject: {
|
|
164
|
+
id: event.subject?.id || event['subject.id'],
|
|
165
|
+
type: event.subject?.type,
|
|
166
|
+
attributes: event.subject?.attributes
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return formattedEvent
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = ExposuresWriter
|