From 068e2f65491ffbb1289c58d9e7444b95c0aa4396 Mon Sep 17 00:00:00 2001 From: rochdev Date: Wed, 12 Feb 2025 17:34:33 -0500 Subject: [PATCH 1/2] lazy load telemetry entrypoint only when needed --- packages/dd-trace/src/telemetry/index.js | 399 +----------------- packages/dd-trace/src/telemetry/telemetry.js | 394 +++++++++++++++++ .../test/appsec/iast/telemetry/logs.spec.js | 2 +- .../dd-trace/test/telemetry/index.spec.js | 75 +++- 4 files changed, 468 insertions(+), 402 deletions(-) create mode 100644 packages/dd-trace/src/telemetry/telemetry.js diff --git a/packages/dd-trace/src/telemetry/index.js b/packages/dd-trace/src/telemetry/index.js index 08a6add25ec..c7b76c663e0 100644 --- a/packages/dd-trace/src/telemetry/index.js +++ b/packages/dd-trace/src/telemetry/index.js @@ -1,394 +1,17 @@ 'use strict' -const tracerVersion = require('../../../../package.json').version -const dc = require('dc-polyfill') -const os = require('os') -const dependencies = require('./dependencies') -const { sendData } = require('./send-data') -const { errors } = require('../startup-log') -const { manager: metricsManager } = require('./metrics') -const telemetryLogger = require('./logs') -const logger = require('../log') -const telemetryStartChannel = dc.channel('datadog:telemetry:start') -const telemetryStopChannel = dc.channel('datadog:telemetry:stop') -const telemetryAppClosingChannel = dc.channel('datadog:telemetry:app-closing') +const inactive = { + start (config, ...args) { + if (!config?.telemetry?.enabled) return -let config -let pluginManager + const active = require('./telemetry') -let application -let host -let heartbeatTimeout -let heartbeatInterval -let extendedInterval -let integrations -let configWithOrigin = [] -let retryData = null -const extendedHeartbeatPayload = {} - -const sentIntegrations = new Set() - -function getRetryData () { - return retryData -} - -function updateRetryData (error, retryObj) { - if (error) { - if (retryObj.reqType === 'message-batch') { - const payload = retryObj.payload[0].payload - const reqType = retryObj.payload[0].request_type - retryData = { payload, reqType } - - // Since this payload failed twice it now gets save in to the extended heartbeat - const failedPayload = retryObj.payload[1].payload - const failedReqType = retryObj.payload[1].request_type - - // save away the dependencies and integration request for extended heartbeat. - if (failedReqType === 'app-integrations-change') { - if (extendedHeartbeatPayload.integrations) { - extendedHeartbeatPayload.integrations.push(failedPayload) - } else { - extendedHeartbeatPayload.integrations = [failedPayload] - } - } - if (failedReqType === 'app-dependencies-loaded') { - if (extendedHeartbeatPayload.dependencies) { - extendedHeartbeatPayload.dependencies.push(failedPayload) - } else { - extendedHeartbeatPayload.dependencies = [failedPayload] - } - } - } else { - retryData = retryObj - } - } else { - retryData = null - } -} - -function getIntegrations () { - const newIntegrations = [] - for (const pluginName in pluginManager._pluginsByName) { - if (sentIntegrations.has(pluginName)) { - continue - } - newIntegrations.push({ - name: pluginName, - enabled: pluginManager._pluginsByName[pluginName]._enabled, - auto_enabled: true - }) - sentIntegrations.add(pluginName) - } - return newIntegrations -} - -function getProducts (config) { - const products = { - appsec: { - enabled: config.appsec.enabled - }, - profiler: { - version: tracerVersion, - enabled: profilingEnabledToBoolean(config.profiling.enabled) - } - } - if (errors.profilingError) { - products.profiler.error = errors.profilingError - errors.profilingError = {} - } - return products -} - -function getInstallSignature (config) { - const { installSignature: sig } = config - if (sig && (sig.id || sig.time || sig.type)) { - return { - install_id: sig.id, - install_time: sig.time, - install_type: sig.type - } - } -} - -function appStarted (config) { - const app = { - products: getProducts(config), - configuration: configWithOrigin - } - const installSignature = getInstallSignature(config) - if (installSignature) { - app.install_signature = installSignature - } - // TODO: add app.error with correct error codes - // if (errors.agentError) { - // app.error = errors.agentError - // errors.agentError = {} - // } - return app -} - -function appClosing () { - if (!config?.telemetry?.enabled) { - return - } - // Give chance to listeners to update metrics before shutting down. - telemetryAppClosingChannel.publish() - const { reqType, payload } = createPayload('app-closing') - sendData(config, application, host, reqType, payload) - // We flush before shutting down. - metricsManager.send(config, application, host) - telemetryLogger.send(config, application, host) -} - -function onBeforeExit () { - process.removeListener('beforeExit', onBeforeExit) - appClosing() -} - -function createAppObject (config) { - return { - service_name: config.service, - env: config.env, - service_version: config.version, - tracer_version: tracerVersion, - language_name: 'nodejs', - language_version: process.versions.node - } -} - -function createHostObject () { - const osName = os.type() - - if (osName === 'Linux' || osName === 'Darwin') { - return { - hostname: os.hostname(), - os: osName, - architecture: os.arch(), - kernel_version: os.version(), - kernel_release: os.release(), - kernel_name: osName - } - } - - if (osName === 'Windows_NT') { - return { - hostname: os.hostname(), - os: osName, - architecture: os.arch(), - os_version: os.version() - } - } - - return { - hostname: os.hostname(), // TODO is this enough? - os: osName - } -} - -function getTelemetryData () { - return { config, application, host, heartbeatInterval } -} - -function createBatchPayload (payload) { - const batchPayload = payload.map(item => { - return { - request_type: item.reqType, - payload: item.payload - } - }) - - return batchPayload -} - -function createPayload (currReqType, currPayload = {}) { - if (getRetryData()) { - const payload = { reqType: currReqType, payload: currPayload } - const batchPayload = createBatchPayload([payload, retryData]) - return { reqType: 'message-batch', payload: batchPayload } - } - - return { reqType: currReqType, payload: currPayload } -} - -function heartbeat (config, application, host) { - heartbeatTimeout = setTimeout(() => { - metricsManager.send(config, application, host) - telemetryLogger.send(config, application, host) - - const { reqType, payload } = createPayload('app-heartbeat') - sendData(config, application, host, reqType, payload, updateRetryData) - heartbeat(config, application, host) - }, heartbeatInterval).unref() - return heartbeatTimeout + Object.setPrototypeOf(module.exports, active).start(config, ...args) + }, + stop () {}, + updateConfig () {}, + updateIntegrations () {}, + appClosing () {} } -function extendedHeartbeat (config) { - extendedInterval = setInterval(() => { - const appPayload = appStarted(config) - const payload = { - ...appPayload, - ...extendedHeartbeatPayload - } - sendData(config, application, host, 'app-extended-heartbeat', payload) - Object.keys(extendedHeartbeatPayload).forEach(key => delete extendedHeartbeatPayload[key]) - }, 1000 * 60 * 60 * 24).unref() - return extendedInterval -} - -function start (aConfig, thePluginManager) { - if (!aConfig.telemetry.enabled) { - if (aConfig.sca?.enabled) { - logger.warn('DD_APPSEC_SCA_ENABLED requires enabling telemetry to work.') - } - - return - } - config = aConfig - pluginManager = thePluginManager - application = createAppObject(config) - host = createHostObject() - heartbeatInterval = config.telemetry.heartbeatInterval - integrations = getIntegrations() - - dependencies.start(config, application, host, getRetryData, updateRetryData) - telemetryLogger.start(config) - - sendData(config, application, host, 'app-started', appStarted(config)) - - if (integrations.length > 0) { - sendData(config, application, host, 'app-integrations-change', - { integrations }, updateRetryData) - } - - heartbeat(config, application, host) - - extendedHeartbeat(config) - - process.on('beforeExit', onBeforeExit) - telemetryStartChannel.publish(getTelemetryData()) -} - -function stop () { - if (!config) { - return - } - clearInterval(extendedInterval) - clearTimeout(heartbeatTimeout) - process.removeListener('beforeExit', onBeforeExit) - - telemetryStopChannel.publish(getTelemetryData()) - - config = undefined -} - -function updateIntegrations () { - if (!config || !config.telemetry.enabled) { - return - } - const integrations = getIntegrations() - if (integrations.length === 0) { - return - } - - const { reqType, payload } = createPayload('app-integrations-change', { integrations }) - - sendData(config, application, host, reqType, payload, updateRetryData) -} - -function formatMapForTelemetry (map) { - // format from an object to a string map in order for - // telemetry intake to accept the configuration - return map - ? Object.entries(map).map(([key, value]) => `${key}:${value}`).join(',') - : '' -} - -function updateConfig (changes, config) { - if (!config.telemetry.enabled) return - if (changes.length === 0) return - - logger.trace(changes) - - const application = createAppObject(config) - const host = createHostObject() - - const nameMapping = { - sampleRate: 'DD_TRACE_SAMPLE_RATE', - logInjection: 'DD_LOG_INJECTION', - headerTags: 'DD_TRACE_HEADER_TAGS', - tags: 'DD_TAGS', - 'sampler.rules': 'DD_TRACE_SAMPLING_RULES', - traceEnabled: 'DD_TRACE_ENABLED', - url: 'DD_TRACE_AGENT_URL', - 'sampler.rateLimit': 'DD_TRACE_RATE_LIMIT', - queryStringObfuscation: 'DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP', - version: 'DD_VERSION', - env: 'DD_ENV', - service: 'DD_SERVICE', - clientIpHeader: 'DD_TRACE_CLIENT_IP_HEADER', - 'grpc.client.error.statuses': 'DD_GRPC_CLIENT_ERROR_STATUSES', - 'grpc.server.error.statuses': 'DD_GRPC_SERVER_ERROR_STATUSES', - traceId128BitLoggingEnabled: 'DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED' - } - - const namesNeedFormatting = new Set(['DD_TAGS', 'peerServiceMapping', 'serviceMapping']) - - const configuration = [] - const names = [] // list of config names whose values have been changed - - for (const change of changes) { - const name = nameMapping[change.name] || change.name - - names.push(name) - const { origin, value } = change - const entry = { name, value, origin } - - if (namesNeedFormatting.has(entry.name)) { - entry.value = formatMapForTelemetry(entry.value) - } else if (entry.name === 'url') { - if (entry.value) { - entry.value = entry.value.toString() - } - } else if (entry.name === 'DD_TRACE_SAMPLING_RULES') { - entry.value = JSON.stringify(entry.value) - } else if (Array.isArray(entry.value)) { - entry.value = value.join(',') - } - configuration.push(entry) - } - - function isNotModified (entry) { - return !names.includes(entry.name) - } - - if (!configWithOrigin.length) { - configWithOrigin = configuration - } else { - // update configWithOrigin to contain up-to-date full list of config values for app-extended-heartbeat - configWithOrigin = configWithOrigin.filter(isNotModified) - configWithOrigin = configWithOrigin.concat(configuration) - const { reqType, payload } = createPayload('app-client-configuration-change', { configuration }) - sendData(config, application, host, reqType, payload, updateRetryData) - } -} - -function profilingEnabledToBoolean (profilingEnabled) { - if (typeof profilingEnabled === 'boolean') { - return profilingEnabled - } - if (['auto', 'true'].includes(profilingEnabled)) { - return true - } - if (profilingEnabled === 'false') { - return false - } - return undefined -} - -module.exports = { - start, - stop, - updateIntegrations, - updateConfig, - appClosing -} +module.exports = Object.setPrototypeOf({}, inactive) diff --git a/packages/dd-trace/src/telemetry/telemetry.js b/packages/dd-trace/src/telemetry/telemetry.js new file mode 100644 index 00000000000..08a6add25ec --- /dev/null +++ b/packages/dd-trace/src/telemetry/telemetry.js @@ -0,0 +1,394 @@ +'use strict' +const tracerVersion = require('../../../../package.json').version +const dc = require('dc-polyfill') +const os = require('os') +const dependencies = require('./dependencies') +const { sendData } = require('./send-data') +const { errors } = require('../startup-log') +const { manager: metricsManager } = require('./metrics') +const telemetryLogger = require('./logs') +const logger = require('../log') + +const telemetryStartChannel = dc.channel('datadog:telemetry:start') +const telemetryStopChannel = dc.channel('datadog:telemetry:stop') +const telemetryAppClosingChannel = dc.channel('datadog:telemetry:app-closing') + +let config +let pluginManager + +let application +let host +let heartbeatTimeout +let heartbeatInterval +let extendedInterval +let integrations +let configWithOrigin = [] +let retryData = null +const extendedHeartbeatPayload = {} + +const sentIntegrations = new Set() + +function getRetryData () { + return retryData +} + +function updateRetryData (error, retryObj) { + if (error) { + if (retryObj.reqType === 'message-batch') { + const payload = retryObj.payload[0].payload + const reqType = retryObj.payload[0].request_type + retryData = { payload, reqType } + + // Since this payload failed twice it now gets save in to the extended heartbeat + const failedPayload = retryObj.payload[1].payload + const failedReqType = retryObj.payload[1].request_type + + // save away the dependencies and integration request for extended heartbeat. + if (failedReqType === 'app-integrations-change') { + if (extendedHeartbeatPayload.integrations) { + extendedHeartbeatPayload.integrations.push(failedPayload) + } else { + extendedHeartbeatPayload.integrations = [failedPayload] + } + } + if (failedReqType === 'app-dependencies-loaded') { + if (extendedHeartbeatPayload.dependencies) { + extendedHeartbeatPayload.dependencies.push(failedPayload) + } else { + extendedHeartbeatPayload.dependencies = [failedPayload] + } + } + } else { + retryData = retryObj + } + } else { + retryData = null + } +} + +function getIntegrations () { + const newIntegrations = [] + for (const pluginName in pluginManager._pluginsByName) { + if (sentIntegrations.has(pluginName)) { + continue + } + newIntegrations.push({ + name: pluginName, + enabled: pluginManager._pluginsByName[pluginName]._enabled, + auto_enabled: true + }) + sentIntegrations.add(pluginName) + } + return newIntegrations +} + +function getProducts (config) { + const products = { + appsec: { + enabled: config.appsec.enabled + }, + profiler: { + version: tracerVersion, + enabled: profilingEnabledToBoolean(config.profiling.enabled) + } + } + if (errors.profilingError) { + products.profiler.error = errors.profilingError + errors.profilingError = {} + } + return products +} + +function getInstallSignature (config) { + const { installSignature: sig } = config + if (sig && (sig.id || sig.time || sig.type)) { + return { + install_id: sig.id, + install_time: sig.time, + install_type: sig.type + } + } +} + +function appStarted (config) { + const app = { + products: getProducts(config), + configuration: configWithOrigin + } + const installSignature = getInstallSignature(config) + if (installSignature) { + app.install_signature = installSignature + } + // TODO: add app.error with correct error codes + // if (errors.agentError) { + // app.error = errors.agentError + // errors.agentError = {} + // } + return app +} + +function appClosing () { + if (!config?.telemetry?.enabled) { + return + } + // Give chance to listeners to update metrics before shutting down. + telemetryAppClosingChannel.publish() + const { reqType, payload } = createPayload('app-closing') + sendData(config, application, host, reqType, payload) + // We flush before shutting down. + metricsManager.send(config, application, host) + telemetryLogger.send(config, application, host) +} + +function onBeforeExit () { + process.removeListener('beforeExit', onBeforeExit) + appClosing() +} + +function createAppObject (config) { + return { + service_name: config.service, + env: config.env, + service_version: config.version, + tracer_version: tracerVersion, + language_name: 'nodejs', + language_version: process.versions.node + } +} + +function createHostObject () { + const osName = os.type() + + if (osName === 'Linux' || osName === 'Darwin') { + return { + hostname: os.hostname(), + os: osName, + architecture: os.arch(), + kernel_version: os.version(), + kernel_release: os.release(), + kernel_name: osName + } + } + + if (osName === 'Windows_NT') { + return { + hostname: os.hostname(), + os: osName, + architecture: os.arch(), + os_version: os.version() + } + } + + return { + hostname: os.hostname(), // TODO is this enough? + os: osName + } +} + +function getTelemetryData () { + return { config, application, host, heartbeatInterval } +} + +function createBatchPayload (payload) { + const batchPayload = payload.map(item => { + return { + request_type: item.reqType, + payload: item.payload + } + }) + + return batchPayload +} + +function createPayload (currReqType, currPayload = {}) { + if (getRetryData()) { + const payload = { reqType: currReqType, payload: currPayload } + const batchPayload = createBatchPayload([payload, retryData]) + return { reqType: 'message-batch', payload: batchPayload } + } + + return { reqType: currReqType, payload: currPayload } +} + +function heartbeat (config, application, host) { + heartbeatTimeout = setTimeout(() => { + metricsManager.send(config, application, host) + telemetryLogger.send(config, application, host) + + const { reqType, payload } = createPayload('app-heartbeat') + sendData(config, application, host, reqType, payload, updateRetryData) + heartbeat(config, application, host) + }, heartbeatInterval).unref() + return heartbeatTimeout +} + +function extendedHeartbeat (config) { + extendedInterval = setInterval(() => { + const appPayload = appStarted(config) + const payload = { + ...appPayload, + ...extendedHeartbeatPayload + } + sendData(config, application, host, 'app-extended-heartbeat', payload) + Object.keys(extendedHeartbeatPayload).forEach(key => delete extendedHeartbeatPayload[key]) + }, 1000 * 60 * 60 * 24).unref() + return extendedInterval +} + +function start (aConfig, thePluginManager) { + if (!aConfig.telemetry.enabled) { + if (aConfig.sca?.enabled) { + logger.warn('DD_APPSEC_SCA_ENABLED requires enabling telemetry to work.') + } + + return + } + config = aConfig + pluginManager = thePluginManager + application = createAppObject(config) + host = createHostObject() + heartbeatInterval = config.telemetry.heartbeatInterval + integrations = getIntegrations() + + dependencies.start(config, application, host, getRetryData, updateRetryData) + telemetryLogger.start(config) + + sendData(config, application, host, 'app-started', appStarted(config)) + + if (integrations.length > 0) { + sendData(config, application, host, 'app-integrations-change', + { integrations }, updateRetryData) + } + + heartbeat(config, application, host) + + extendedHeartbeat(config) + + process.on('beforeExit', onBeforeExit) + telemetryStartChannel.publish(getTelemetryData()) +} + +function stop () { + if (!config) { + return + } + clearInterval(extendedInterval) + clearTimeout(heartbeatTimeout) + process.removeListener('beforeExit', onBeforeExit) + + telemetryStopChannel.publish(getTelemetryData()) + + config = undefined +} + +function updateIntegrations () { + if (!config || !config.telemetry.enabled) { + return + } + const integrations = getIntegrations() + if (integrations.length === 0) { + return + } + + const { reqType, payload } = createPayload('app-integrations-change', { integrations }) + + sendData(config, application, host, reqType, payload, updateRetryData) +} + +function formatMapForTelemetry (map) { + // format from an object to a string map in order for + // telemetry intake to accept the configuration + return map + ? Object.entries(map).map(([key, value]) => `${key}:${value}`).join(',') + : '' +} + +function updateConfig (changes, config) { + if (!config.telemetry.enabled) return + if (changes.length === 0) return + + logger.trace(changes) + + const application = createAppObject(config) + const host = createHostObject() + + const nameMapping = { + sampleRate: 'DD_TRACE_SAMPLE_RATE', + logInjection: 'DD_LOG_INJECTION', + headerTags: 'DD_TRACE_HEADER_TAGS', + tags: 'DD_TAGS', + 'sampler.rules': 'DD_TRACE_SAMPLING_RULES', + traceEnabled: 'DD_TRACE_ENABLED', + url: 'DD_TRACE_AGENT_URL', + 'sampler.rateLimit': 'DD_TRACE_RATE_LIMIT', + queryStringObfuscation: 'DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP', + version: 'DD_VERSION', + env: 'DD_ENV', + service: 'DD_SERVICE', + clientIpHeader: 'DD_TRACE_CLIENT_IP_HEADER', + 'grpc.client.error.statuses': 'DD_GRPC_CLIENT_ERROR_STATUSES', + 'grpc.server.error.statuses': 'DD_GRPC_SERVER_ERROR_STATUSES', + traceId128BitLoggingEnabled: 'DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED' + } + + const namesNeedFormatting = new Set(['DD_TAGS', 'peerServiceMapping', 'serviceMapping']) + + const configuration = [] + const names = [] // list of config names whose values have been changed + + for (const change of changes) { + const name = nameMapping[change.name] || change.name + + names.push(name) + const { origin, value } = change + const entry = { name, value, origin } + + if (namesNeedFormatting.has(entry.name)) { + entry.value = formatMapForTelemetry(entry.value) + } else if (entry.name === 'url') { + if (entry.value) { + entry.value = entry.value.toString() + } + } else if (entry.name === 'DD_TRACE_SAMPLING_RULES') { + entry.value = JSON.stringify(entry.value) + } else if (Array.isArray(entry.value)) { + entry.value = value.join(',') + } + configuration.push(entry) + } + + function isNotModified (entry) { + return !names.includes(entry.name) + } + + if (!configWithOrigin.length) { + configWithOrigin = configuration + } else { + // update configWithOrigin to contain up-to-date full list of config values for app-extended-heartbeat + configWithOrigin = configWithOrigin.filter(isNotModified) + configWithOrigin = configWithOrigin.concat(configuration) + const { reqType, payload } = createPayload('app-client-configuration-change', { configuration }) + sendData(config, application, host, reqType, payload, updateRetryData) + } +} + +function profilingEnabledToBoolean (profilingEnabled) { + if (typeof profilingEnabled === 'boolean') { + return profilingEnabled + } + if (['auto', 'true'].includes(profilingEnabled)) { + return true + } + if (profilingEnabled === 'false') { + return false + } + return undefined +} + +module.exports = { + start, + stop, + updateIntegrations, + updateConfig, + appClosing +} diff --git a/packages/dd-trace/test/appsec/iast/telemetry/logs.spec.js b/packages/dd-trace/test/appsec/iast/telemetry/logs.spec.js index a8b5c358b9f..1cd7394637a 100644 --- a/packages/dd-trace/test/appsec/iast/telemetry/logs.spec.js +++ b/packages/dd-trace/test/appsec/iast/telemetry/logs.spec.js @@ -23,7 +23,7 @@ describe('Telemetry logs', () => { start = sinon.stub() send = sinon.spy() - telemetry = proxyquire('../../../../src/telemetry', { + telemetry = proxyquire('../../../../src/telemetry/telemetry', { '../exporters/common/docker': { id () { return 'test docker id' diff --git a/packages/dd-trace/test/telemetry/index.spec.js b/packages/dd-trace/test/telemetry/index.spec.js index e7c1fe2a77f..f01e98c1a4a 100644 --- a/packages/dd-trace/test/telemetry/index.spec.js +++ b/packages/dd-trace/test/telemetry/index.spec.js @@ -14,6 +14,55 @@ const DEFAULT_HEARTBEAT_INTERVAL = 60000 let traceAgent +describe('telemetry (proxy)', () => { + let telemetry + let proxy + + beforeEach(() => { + telemetry = sinon.spy({ + start () {}, + stop () {}, + updateIntegrations () {}, + updateConfig () {}, + appClosing () {} + }) + + proxy = proxyquire('../../src/telemetry', { + './telemetry': telemetry + }) + }) + + it('should be noop when disabled', () => { + proxy.start() + proxy.updateIntegrations() + proxy.updateConfig([]) + proxy.appClosing() + proxy.stop() + + expect(telemetry.start).to.not.have.been.called + expect(telemetry.updateIntegrations).to.not.have.been.called + expect(telemetry.updateConfig).to.not.have.been.called + expect(telemetry.appClosing).to.not.have.been.called + expect(telemetry.stop).to.not.have.been.called + }) + + it('should proxy when enabled', () => { + const config = { telemetry: { enabled: true } } + + proxy.start(config) + proxy.updateIntegrations() + proxy.updateConfig() + proxy.appClosing() + proxy.stop() + + expect(telemetry.start).to.have.been.calledWith(config) + expect(telemetry.updateIntegrations).to.have.been.called + expect(telemetry.updateConfig).to.have.been.called + expect(telemetry.appClosing).to.have.been.called + expect(telemetry.stop).to.have.been.called + }) +}) + describe('telemetry', () => { let telemetry let pluginsByName @@ -39,7 +88,7 @@ describe('telemetry', () => { traceAgent.reqs = [] - telemetry = proxyquire('../../src/telemetry', { + telemetry = proxyquire('../../src/telemetry/telemetry', { '../exporters/common/docker': { id () { return 'test docker id' @@ -171,7 +220,7 @@ describe('telemetry', () => { it('should not send app-closing if telemetry is not enabled', () => { const sendDataStub = sinon.stub() - const notEnabledTelemetry = proxyquire('../../src/telemetry', { + const notEnabledTelemetry = proxyquire('../../src/telemetry/telemetry', { './send-data': { sendData: sendDataStub } @@ -213,7 +262,7 @@ describe('telemetry app-heartbeat', () => { } } } - telemetry = proxyquire('../../src/telemetry', { + telemetry = proxyquire('../../src/telemetry/telemetry', { '../exporters/common/docker': { id () { return 'test docker id' @@ -278,7 +327,7 @@ describe('Telemetry extended heartbeat', () => { } } - telemetry = proxyquire('../../src/telemetry', { + telemetry = proxyquire('../../src/telemetry/telemetry', { '../exporters/common/docker': { id () { return 'test docker id' @@ -320,7 +369,7 @@ describe('Telemetry extended heartbeat', () => { } } - telemetry = proxyquire('../../src/telemetry', { + telemetry = proxyquire('../../src/telemetry/telemetry', { '../exporters/common/docker': { id () { return 'test docker id' @@ -463,7 +512,7 @@ describe('Telemetry retry', () => { } } - telemetry = proxyquire('../../src/telemetry', { + telemetry = proxyquire('../../src/telemetry/telemetry', { '../exporters/common/docker': { id () { return 'test docker id' @@ -549,7 +598,7 @@ describe('Telemetry retry', () => { } } - telemetry = proxyquire('../../src/telemetry', { + telemetry = proxyquire('../../src/telemetry/telemetry', { '../exporters/common/docker': { id () { return 'test docker id' @@ -618,7 +667,7 @@ describe('Telemetry retry', () => { } } - telemetry = proxyquire('../../src/telemetry', { + telemetry = proxyquire('../../src/telemetry/telemetry', { '../exporters/common/docker': { id () { return 'test docker id' @@ -678,7 +727,7 @@ describe('Telemetry retry', () => { } } - telemetry = proxyquire('../../src/telemetry', { + telemetry = proxyquire('../../src/telemetry/telemetry', { '../exporters/common/docker': { id () { return 'test docker id' @@ -760,7 +809,7 @@ describe('Telemetry retry', () => { } } - telemetry = proxyquire('../../src/telemetry', { + telemetry = proxyquire('../../src/telemetry/telemetry', { '../exporters/common/docker': { id () { return 'test docker id' @@ -848,8 +897,8 @@ describe('AVM OSS', () => { traceAgent.reqs = [] delete require.cache[require.resolve('../../src/telemetry/send-data')] - delete require.cache[require.resolve('../../src/telemetry')] - telemetry = require('../../src/telemetry') + delete require.cache[require.resolve('../../src/telemetry/telemetry')] + telemetry = require('../../src/telemetry/telemetry') telemetryConfig = { telemetry: { enabled: true, heartbeatInterval: HEARTBEAT_INTERVAL }, @@ -909,7 +958,7 @@ describe('AVM OSS', () => { } before(() => { - telemetry = proxyquire('../../src/telemetry', { + telemetry = proxyquire('../../src/telemetry/telemetry', { '../log': logSpy }) }) From c902c9a67b68885ecbef7d170bf7674644eb3a88 Mon Sep 17 00:00:00 2001 From: rochdev Date: Mon, 24 Feb 2025 15:37:50 -0500 Subject: [PATCH 2/2] also lazy load on update config --- packages/dd-trace/src/telemetry/index.js | 18 ++++++++++++------ packages/dd-trace/test/telemetry/index.spec.js | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/dd-trace/src/telemetry/index.js b/packages/dd-trace/src/telemetry/index.js index c7b76c663e0..cd05b23de2c 100644 --- a/packages/dd-trace/src/telemetry/index.js +++ b/packages/dd-trace/src/telemetry/index.js @@ -1,15 +1,21 @@ 'use strict' -const inactive = { - start (config, ...args) { - if (!config?.telemetry?.enabled) return +const activate = () => { + const active = require('./telemetry') - const active = require('./telemetry') + return Object.setPrototypeOf(module.exports, active) +} - Object.setPrototypeOf(module.exports, active).start(config, ...args) +const inactive = { + start (config, ...args) { + return config?.telemetry?.enabled && activate().start(config, ...args) }, stop () {}, - updateConfig () {}, + // This might be called before `start` so we have to trigger loading the + // underlying module here as well. + updateConfig (changes, config, ...args) { + return config?.telemetry?.enabled && activate().updateConfig(changes, config, ...args) + }, updateIntegrations () {}, appClosing () {} } diff --git a/packages/dd-trace/test/telemetry/index.spec.js b/packages/dd-trace/test/telemetry/index.spec.js index f01e98c1a4a..b6ec79f9bda 100644 --- a/packages/dd-trace/test/telemetry/index.spec.js +++ b/packages/dd-trace/test/telemetry/index.spec.js @@ -61,6 +61,20 @@ describe('telemetry (proxy)', () => { expect(telemetry.appClosing).to.have.been.called expect(telemetry.stop).to.have.been.called }) + + it('should proxy when enabled from updateConfig', () => { + const config = { telemetry: { enabled: true } } + + proxy.updateConfig([], config) + proxy.updateIntegrations() + proxy.appClosing() + proxy.stop() + + expect(telemetry.updateIntegrations).to.have.been.called + expect(telemetry.updateConfig).to.have.been.calledWith([], config) + expect(telemetry.appClosing).to.have.been.called + expect(telemetry.stop).to.have.been.called + }) }) describe('telemetry', () => {