diff --git a/.github/workflows/release-3.yml b/.github/workflows/release-3.yml index ef602708327..e3367eccebd 100644 --- a/.github/workflows/release-3.yml +++ b/.github/workflows/release-3.yml @@ -28,3 +28,26 @@ jobs: - run: | git tag v${{ fromJson(steps.pkg.outputs.json).version }} git push origin v${{ fromJson(steps.pkg.outputs.json).version }} + + injection-image-publish: + runs-on: ubuntu-latest + needs: ['publish'] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - name: Log in to the Container registry + uses: docker/login-action@49ed152c8eca782a232dede0303416e8f356c37b + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: pkg + run: | + content=`cat ./package.json | tr '\n' ' '` + echo "::set-output name=json::$content" + - name: npm pack for injection image + run: | + npm pack dd-trace@${{ fromJson(steps.pkg.outputs.json).version }} + - uses: ./.github/actions/injection + with: + init-image-version: v${{ fromJson(steps.pkg.outputs.json).version }} diff --git a/integration-tests/helpers.js b/integration-tests/helpers.js index 6bf0f66968d..4e32391e39e 100644 --- a/integration-tests/helpers.js +++ b/integration-tests/helpers.js @@ -205,6 +205,7 @@ async function createSandbox (dependencies = [], isGitRepo = false, integrationT integrationTestsPaths.forEach(async (path) => { await exec(`cp -R ${path} ${folder}`) + await exec(`sync ${folder}`) }) if (isGitRepo) { diff --git a/integration-tests/telemetry.spec.js b/integration-tests/telemetry.spec.js new file mode 100644 index 00000000000..452eb78284c --- /dev/null +++ b/integration-tests/telemetry.spec.js @@ -0,0 +1,64 @@ +'use strict' + +const { createSandbox, FakeAgent, spawnProc } = require('./helpers') +const path = require('path') + +describe('telemetry', () => { + describe('dependencies', () => { + let sandbox + let cwd + let startupTestFile + let agent + let proc + + before(async () => { + sandbox = await createSandbox() + cwd = sandbox.folder + startupTestFile = path.join(cwd, 'startup/index.js') + }) + + after(async () => { + await sandbox.remove() + }) + + beforeEach(async () => { + agent = await new FakeAgent().start() + proc = await spawnProc(startupTestFile, { + cwd, + env: { + AGENT_PORT: agent.port + } + }) + }) + + afterEach(async () => { + proc.kill() + await agent.stop() + }) + + it('Test that tracer and iitm are sent as dependencies', (done) => { + let ddTraceFound = false + let importInTheMiddleFound = false + + agent.assertTelemetryReceived(msg => { + const { payload } = msg + + if (payload.request_type === 'app-dependencies-loaded') { + if (payload.payload.dependencies) { + payload.payload.dependencies.forEach(dependency => { + if (dependency.name === 'dd-trace') { + ddTraceFound = true + } + if (dependency.name === 'import-in-the-middle') { + importInTheMiddleFound = true + } + }) + if (ddTraceFound && importInTheMiddleFound) { + done() + } + } + } + }, null, 'app-dependencies-loaded', 1) + }) + }) +}) diff --git a/package.json b/package.json index bdf6de9402a..998300b972d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dd-trace", - "version": "4.13.0", + "version": "4.13.1", "description": "Datadog APM tracing client for JavaScript", "main": "index.js", "typings": "index.d.ts", diff --git a/packages/datadog-instrumentations/src/kafkajs.js b/packages/datadog-instrumentations/src/kafkajs.js index 519b9873784..beaba513097 100644 --- a/packages/datadog-instrumentations/src/kafkajs.js +++ b/packages/datadog-instrumentations/src/kafkajs.js @@ -15,15 +15,14 @@ const consumerStartCh = channel('apm:kafkajs:consume:start') const consumerFinishCh = channel('apm:kafkajs:consume:finish') const consumerErrorCh = channel('apm:kafkajs:consume:error') -addHook({ name: 'kafkajs', versions: ['>=1.4'] }, (obj) => { - class Kafka extends obj.Kafka { +addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKafka) => { + class Kafka extends BaseKafka { constructor (options) { super(options) this._brokers = (options.brokers && typeof options.brokers !== 'function') ? options.brokers.join(',') : undefined } } - obj.Kafka = Kafka shimmer.wrap(Kafka.prototype, 'producer', createProducer => function () { const producer = createProducer.apply(this, arguments) @@ -117,5 +116,5 @@ addHook({ name: 'kafkajs', versions: ['>=1.4'] }, (obj) => { } return consumer }) - return obj + return Kafka }) diff --git a/packages/datadog-plugin-amqp10/test/integration-test/server.mjs b/packages/datadog-plugin-amqp10/test/integration-test/server.mjs index 9418cf24a13..c2e1b92e643 100644 --- a/packages/datadog-plugin-amqp10/test/integration-test/server.mjs +++ b/packages/datadog-plugin-amqp10/test/integration-test/server.mjs @@ -2,10 +2,9 @@ import 'dd-trace/init.js' import amqp from 'amqp10' const client = new amqp.Client() -await client.connect('amqp://admin:admin@localhost:5673') +await client.connect('amqp://admin:admin@127.0.0.1:5673') const handlers = await Promise.all([client.createSender('amq.topic')]) const sender = handlers[0] - sender.send({ key: 'value' }) if (sender) { @@ -13,4 +12,4 @@ if (sender) { } if (client) { await client.disconnect() -} \ No newline at end of file +} diff --git a/packages/datadog-plugin-kafkajs/test/integration-test/client.spec.js b/packages/datadog-plugin-kafkajs/test/integration-test/client.spec.js new file mode 100644 index 00000000000..488628a66fa --- /dev/null +++ b/packages/datadog-plugin-kafkajs/test/integration-test/client.spec.js @@ -0,0 +1,48 @@ +'use strict' + +const { + FakeAgent, + createSandbox, + checkSpansForServiceName, + spawnPluginIntegrationTestProc +} = require('../../../../integration-tests/helpers') +const { assert } = require('chai') + +describe('esm', () => { + let agent + let proc + let sandbox + + before(async function () { + this.timeout(20000) + sandbox = await createSandbox(['kafkajs@>=1.4.0'], false, [ + `./packages/datadog-plugin-kafkajs/test/integration-test/*`]) + }) + + after(async () => { + await sandbox.remove() + }) + + beforeEach(async () => { + agent = await new FakeAgent().start() + }) + + afterEach(async () => { + proc && proc.kill() + await agent.stop() + }) + + context('kafkajs', () => { + it('is instrumented', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`) + assert.isArray(payload) + assert.strictEqual(checkSpansForServiceName(payload, 'kafka.produce'), true) + }) + + proc = await spawnPluginIntegrationTestProc(sandbox.folder, 'server.mjs', agent.port) + + await res + }).timeout(20000) + }) +}) diff --git a/packages/datadog-plugin-kafkajs/test/integration-test/server.mjs b/packages/datadog-plugin-kafkajs/test/integration-test/server.mjs new file mode 100644 index 00000000000..342d98b909b --- /dev/null +++ b/packages/datadog-plugin-kafkajs/test/integration-test/server.mjs @@ -0,0 +1,19 @@ +import 'dd-trace/init.js' +import { Kafka } from 'kafkajs' + +const kafka = new Kafka({ + clientId: 'my-app', + brokers: ['127.0.0.1:9092'] +}) + +const sendMessage = async (topic, messages) => { + const producer = kafka.producer() + await producer.connect() + await producer.send({ + topic, + messages + }) + await producer.disconnect() +} + +await sendMessage('test-topic', [{ key: 'key1', value: 'test2' }]) diff --git a/packages/datadog-plugin-mongodb-core/src/index.js b/packages/datadog-plugin-mongodb-core/src/index.js index e1ae187e8d5..80d7a58bb56 100644 --- a/packages/datadog-plugin-mongodb-core/src/index.js +++ b/packages/datadog-plugin-mongodb-core/src/index.js @@ -36,10 +36,14 @@ class MongodbCorePlugin extends DatabasePlugin { } } +function sanitizeBigInt (data) { + return JSON.stringify(data, (_key, value) => typeof value === 'bigint' ? value.toString() : value) +} + function getQuery (cmd) { if (!cmd || typeof cmd !== 'object' || Array.isArray(cmd)) return - if (cmd.query) return JSON.stringify(limitDepth(cmd.query)) - if (cmd.filter) return JSON.stringify(limitDepth(cmd.filter)) + if (cmd.query) return sanitizeBigInt(limitDepth(cmd.query)) + if (cmd.filter) return sanitizeBigInt(limitDepth(cmd.filter)) } function getResource (plugin, ns, query, operationName) { diff --git a/packages/datadog-plugin-mongodb-core/test/core.spec.js b/packages/datadog-plugin-mongodb-core/test/core.spec.js index 8cdabf4f275..31f1c0e6d7e 100644 --- a/packages/datadog-plugin-mongodb-core/test/core.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/core.spec.js @@ -133,6 +133,39 @@ describe('Plugin', () => { }, () => {}) }) + it('should serialize BigInt without erroring', done => { + agent + .use(traces => { + const span = traces[0][0] + const resource = `find test.${collection}` + const query = `{"_id":"9999999999999999999999"}` + + expect(span).to.have.property('resource', resource) + expect(span.meta).to.have.property('mongodb.query', query) + }) + .then(done) + .catch(done) + + try { + server.command(`test.${collection}`, { + find: `test.${collection}`, + query: { + _id: 9999999999999999999999n + } + }, () => {}) + } catch (err) { + // It appears that most versions of MongodDB are happy to use a BigInt instance. + // For example, 2.0.0, 3.2.0, 3.1.10, etc. + // However, version 3.1.9 throws a synchronous error that it wants a Decimal128 instead. + if (err.message.includes('Decimal128')) { + // eslint-disable-next-line no-console + console.log('This version of mongodb-core does not accept BigInt instances') + return done() + } + done(err) + } + }) + it('should stringify BSON objects', done => { const BSON = require(`../../../versions/bson@4.0.0`).get() const id = '123456781234567812345678' diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js index 8545b3e5696..86f9863aeb0 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js @@ -76,7 +76,18 @@ class SensitiveHandler { while (nextSensitive != null && contains(nextTainted, nextSensitive)) { const redactionStart = nextSensitive.start - nextTainted.start const redactionEnd = nextSensitive.end - nextTainted.start - this.redactSource(sources, redactedSources, redactedSourcesContext, sourceIndex, redactionStart, redactionEnd) + if (redactionStart === redactionEnd) { + this.writeRedactedValuePart(valueParts, 0) + } else { + this.redactSource( + sources, + redactedSources, + redactedSourcesContext, + sourceIndex, + redactionStart, + redactionEnd + ) + } nextSensitive = sensitive.shift() } diff --git a/packages/dd-trace/src/telemetry/dependencies.js b/packages/dd-trace/src/telemetry/dependencies.js index 7dbdde5a71f..11c443fee98 100644 --- a/packages/dd-trace/src/telemetry/dependencies.js +++ b/packages/dd-trace/src/telemetry/dependencies.js @@ -15,6 +15,7 @@ const FILE_URI_START = `file://` const moduleLoadStartChannel = dc.channel('dd-trace:moduleLoadStart') let immediate, config, application, host +let isFirstModule = true function waitAndSend (config, application, host) { if (!immediate) { @@ -36,7 +37,21 @@ function waitAndSend (config, application, host) { } } +function loadAllTheLoadedModules () { + if (require.cache) { + const filenames = Object.keys(require.cache) + filenames.forEach(filename => { + onModuleLoad({ filename }) + }) + } +} + function onModuleLoad (data) { + if (isFirstModule) { + isFirstModule = false + loadAllTheLoadedModules() + } + if (data) { let filename = data.filename if (filename && filename.startsWith(FILE_URI_START)) { diff --git a/packages/dd-trace/src/telemetry/index.js b/packages/dd-trace/src/telemetry/index.js index eaf6c6910b7..e2568f8cfc0 100644 --- a/packages/dd-trace/src/telemetry/index.js +++ b/packages/dd-trace/src/telemetry/index.js @@ -17,6 +17,7 @@ let pluginManager let application let host let interval +let heartbeatTimeout let heartbeatInterval const sentIntegrations = new Set() @@ -110,6 +111,14 @@ function getTelemetryData () { return { config, application, host, heartbeatInterval } } +function heartbeat (config, application, host) { + heartbeatTimeout = setTimeout(() => { + sendData(config, application, host, 'app-heartbeat') + heartbeat(config, application, host) + }, heartbeatInterval).unref() + return heartbeatTimeout +} + function start (aConfig, thePluginManager) { if (!aConfig.telemetry.enabled) { return @@ -122,9 +131,9 @@ function start (aConfig, thePluginManager) { dependencies.start(config, application, host) sendData(config, application, host, 'app-started', appStarted()) + heartbeat(config, application, host) interval = setInterval(() => { metricsManager.send(config, application, host) - sendData(config, application, host, 'app-heartbeat') }, heartbeatInterval) interval.unref() process.on('beforeExit', onBeforeExit) @@ -137,6 +146,7 @@ function stop () { return } clearInterval(interval) + clearTimeout(heartbeatTimeout) process.removeListener('beforeExit', onBeforeExit) telemetryStopChannel.publish(getTelemetryData()) diff --git a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json index 6d9748cdd99..89fc975a262 100644 --- a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json +++ b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json @@ -2679,6 +2679,60 @@ } ] } + }, + { + "type": "VULNERABILITIES", + "description": "SQLi exploited", + "input": [ + { + "type": "SQL_INJECTION", + "evidence": { + "value": "SELECT * FROM Users WHERE email = '' OR TRUE --' AND password = '81dc9bdb52d04dc20036dbd8313ed055' AND deletedAt IS NULL", + "ranges": [ + { + "start": 35, + "end": 47, + "iinfo": { + "type": "http.request.parameter", + "parameterName": "email", + "parameterValue": "' OR TRUE --" + } + } + ] + } + } + ], + "expected": { + "sources": [ + { + "origin": "http.request.parameter", + "name": "email", + "value": "' OR TRUE --" + } + ], + "vulnerabilities": [ + { + "type": "SQL_INJECTION", + "evidence": { + "valueParts": [ + { + "value": "SELECT * FROM Users WHERE email = '" + }, + { + "redacted": true + }, + { + "source": 0, + "value": "' OR TRUE --" + }, + { + "redacted": true + } + ] + } + } + ] + } } ] } diff --git a/packages/dd-trace/test/telemetry/dependencies.spec.js b/packages/dd-trace/test/telemetry/dependencies.spec.js index e3084c9ef47..65c6b4e8962 100644 --- a/packages/dd-trace/test/telemetry/dependencies.spec.js +++ b/packages/dd-trace/test/telemetry/dependencies.spec.js @@ -19,6 +19,7 @@ describe('dependencies', () => { expect(subscribe).to.have.been.calledOnce }) }) + describe('on event', () => { const config = {} const application = 'test' @@ -37,6 +38,11 @@ describe('dependencies', () => { '../require-package-json': requirePackageJson }) global.setImmediate = function (callback) { callback() } + + dependencies.start(config, application, host) + + // force first publish to load cached requires + moduleLoadStartChannel.publish({}) }) afterEach(() => { @@ -46,7 +52,6 @@ describe('dependencies', () => { }) it('should not fail with invalid data', () => { - dependencies.start(config, application, host) moduleLoadStartChannel.publish(null) moduleLoadStartChannel.publish({}) moduleLoadStartChannel.publish({ filename: 'filename' }) @@ -56,35 +61,30 @@ describe('dependencies', () => { }) it('should not call to sendData with core library', () => { - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ request: 'crypto', filename: 'crypto' }) expect(sendData).not.to.have.been.called }) it('should not call to sendData without node_modules in path', () => { const filename = path.join(basepathWithoutNodeModules, 'custom.js') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ request: 'custom-module', filename }) expect(sendData).not.to.have.been.called }) it('should not call to sendData without node_modules in file URI', () => { const filename = [fileURIWithoutNodeModules, 'custom.js'].join('/') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ request: 'custom-module', filename }) expect(sendData).not.to.have.been.called }) it('should not call to sendData without node_modules in path when request does not come in message', () => { const filename = path.join(basepathWithoutNodeModules, 'custom.js') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ filename }) expect(sendData).not.to.have.been.called }) it('should not call to sendData without node_modules in path when request does not come in message', () => { const filename = [fileURIWithoutNodeModules, 'custom.js'].join('/') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ filename }) expect(sendData).not.to.have.been.called }) @@ -93,7 +93,6 @@ describe('dependencies', () => { const request = 'custom-module' requirePackageJson.callsFake(function () { throw new Error() }) const filename = path.join(basepathWithoutNodeModules, 'node_modules', request, 'index.js') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ request, filename }) expect(sendData).not.to.have.been.called }) @@ -107,7 +106,6 @@ describe('dependencies', () => { it(`should not call to sendData with file paths request: ${request}`, () => { requirePackageJson.returns({ version: '1.0.0' }) const filename = path.join(basepathWithoutNodeModules, 'node_modules', 'custom-module', 'index.js') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ request, filename }) expect(sendData).not.to.have.been.called }) @@ -117,7 +115,6 @@ describe('dependencies', () => { const request = 'custom-module' requirePackageJson.returns({ version: '1.0.0' }) const filename = path.join(basepathWithoutNodeModules, 'node_modules', request, 'index.js') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ request, filename }) expect(sendData).to.have.been.calledOnce }) @@ -126,7 +123,6 @@ describe('dependencies', () => { const request = 'custom-module' requirePackageJson.returns({ version: '1.0.0' }) const filename = [fileURIWithoutNodeModules, 'node_modules', request, 'index.js'].join('/') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ request, filename }) expect(sendData).to.have.been.calledOnce }) @@ -136,7 +132,6 @@ describe('dependencies', () => { const packageVersion = '1.0.0' requirePackageJson.returns({ version: packageVersion }) const filename = [fileURIWithoutNodeModules, 'node_modules', request, 'index.js'].join('/') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ filename }) const expectedDependencies = { dependencies: [ @@ -152,7 +147,6 @@ describe('dependencies', () => { const packageVersion = '1.0.0' requirePackageJson.returns({ version: packageVersion }) const filename = [fileURIWithoutNodeModules, 'node_modules', request, 'index.js'].join('/') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ filename }) const expectedDependencies = { dependencies: [ @@ -169,7 +163,6 @@ describe('dependencies', () => { requirePackageJson.returns({ version: packageVersion }) const filename = 'file:' + path.sep + path.sep + path.join(basepathWithoutNodeModules, 'node_modules', request, 'index.js') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ filename }) const expectedDependencies = { dependencies: [ @@ -186,7 +179,6 @@ describe('dependencies', () => { requirePackageJson.returns({ version: packageVersion }) const filename1 = [fileURIWithoutNodeModules, 'node_modules', moduleName, 'index1.js'].join('/') const filename2 = [fileURIWithoutNodeModules, 'node_modules', moduleName, 'index2.js'].join('/') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ request: moduleName, filename: filename1 }) moduleLoadStartChannel.publish({ request: moduleName, filename: filename2 }) const expectedDependencies = { @@ -214,7 +206,6 @@ describe('dependencies', () => { } }) - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ request: moduleName, filename: firstLevelDependency }) moduleLoadStartChannel.publish({ request: moduleName, filename: nestedDependency }) @@ -252,7 +243,6 @@ describe('dependencies', () => { } }) - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ request: moduleName, filename: firstLevelDependency }) moduleLoadStartChannel.publish({ request: moduleName, filename: nestedDependency }) @@ -269,7 +259,6 @@ describe('dependencies', () => { const request = 'custom-module' requirePackageJson.returns({ version: '1.0.0' }) const filename = path.join(basepathWithoutNodeModules, 'node_modules', request, 'index.js') - dependencies.start(config, application, host) moduleLoadStartChannel.publish({ request, filename }) moduleLoadStartChannel.publish({ request, filename }) moduleLoadStartChannel.publish({ request, filename }) @@ -277,7 +266,6 @@ describe('dependencies', () => { }) it('should call sendData twice with more than 1000 dependencies', (done) => { - dependencies.start(config, application, host) const requestPrefix = 'custom-module' requirePackageJson.returns({ version: '1.0.0' }) const timeouts = [] diff --git a/packages/dd-trace/test/telemetry/index.spec.js b/packages/dd-trace/test/telemetry/index.spec.js index 9750632ba4c..2bf61fc3e53 100644 --- a/packages/dd-trace/test/telemetry/index.spec.js +++ b/packages/dd-trace/test/telemetry/index.spec.js @@ -12,6 +12,7 @@ const os = require('os') let traceAgent describe('telemetry', () => { + const HEARTBEAT_INTERVAL = 60000 let origSetInterval let telemetry let pluginsByName @@ -20,7 +21,7 @@ describe('telemetry', () => { origSetInterval = setInterval global.setInterval = (fn, interval) => { - expect(interval).to.equal(60000) + expect(interval).to.equal(HEARTBEAT_INTERVAL) // we only want one of these return setTimeout(fn, 100) } @@ -64,7 +65,7 @@ describe('telemetry', () => { circularObject.child.parent = circularObject telemetry.start({ - telemetry: { enabled: true, heartbeatInterval: 60000 }, + telemetry: { enabled: true, heartbeatInterval: HEARTBEAT_INTERVAL }, hostname: 'localhost', port: traceAgent.address().port, service: 'test service', @@ -107,17 +108,11 @@ describe('telemetry', () => { }) }) - it('should send app-heartbeat', () => { - return testSeq(2, 'app-heartbeat', payload => { - expect(payload).to.deep.equal({}) - }) - }) - it('should send app-integrations-change', () => { pluginsByName.baz2 = { _enabled: true } telemetry.updateIntegrations() - return testSeq(3, 'app-integrations-change', payload => { + return testSeq(2, 'app-integrations-change', payload => { expect(payload).to.deep.equal({ integrations: [ { name: 'baz2', enabled: true, auto_enabled: true } @@ -130,7 +125,7 @@ describe('telemetry', () => { pluginsByName.boo2 = { _enabled: true } telemetry.updateIntegrations() - return testSeq(4, 'app-integrations-change', payload => { + return testSeq(3, 'app-integrations-change', payload => { expect(payload).to.deep.equal({ integrations: [ { name: 'boo2', enabled: true, auto_enabled: true } @@ -167,6 +162,98 @@ describe('telemetry', () => { }) }) +describe('telemetry app-heartbeat', () => { + const HEARTBEAT_INTERVAL = 60 + let origSetInterval + let telemetry + let pluginsByName + + before(done => { + origSetInterval = setInterval + + global.setInterval = (fn, interval) => { + expect(interval).to.equal(HEARTBEAT_INTERVAL) + // we only want one of these + return setTimeout(fn, 100) + } + + storage.run({ noop: true }, () => { + traceAgent = http.createServer(async (req, res) => { + const chunks = [] + for await (const chunk of req) { + chunks.push(chunk) + } + req.body = JSON.parse(Buffer.concat(chunks).toString('utf8')) + traceAgent.reqs.push(req) + traceAgent.emit('handled-req') + res.end() + }).listen(0, done) + }) + + traceAgent.reqs = [] + + telemetry = proxyquire('../../src/telemetry', { + '../exporters/common/docker': { + id () { + return 'test docker id' + } + } + }) + + pluginsByName = { + foo2: { _enabled: true }, + bar2: { _enabled: false } + } + + const circularObject = { + child: { parent: null, field: 'child_value' }, + field: 'parent_value' + } + circularObject.child.parent = circularObject + + telemetry.start({ + telemetry: { enabled: true, heartbeatInterval: HEARTBEAT_INTERVAL }, + hostname: 'localhost', + port: traceAgent.address().port, + service: 'test service', + version: '1.2.3-beta4', + env: 'preprod', + tags: { + 'runtime-id': '1a2b3c' + }, + circularObject + }, { + _pluginsByName: pluginsByName + }) + }) + + after(() => { + setTimeout(() => { + telemetry.stop() + traceAgent.close() + global.setInterval = origSetInterval + }, HEARTBEAT_INTERVAL * 3) + }) + + it('should send app-heartbeat at uniform intervals', () => { + // TODO: switch to clock.tick + setTimeout(() => { + const heartbeats = [] + const reqCount = traceAgent.reqs.length + for (let i = 0; i < reqCount; i++) { + const req = traceAgent.reqs[i] + if (req.headers && req.headers['dd-telemetry-request-type'] === 'app-heartbeat') { + heartbeats.push(req.body.tracer_time) + } + } + expect(heartbeats.length).to.be.greaterThanOrEqual(2) + for (let k = 0; k++; k < heartbeats.length - 1) { + expect(heartbeats[k + 1] - heartbeats[k]).to.be.equal(1) + } + }, HEARTBEAT_INTERVAL * 3) + }) +}) + describe('telemetry with interval change', () => { it('should set the interval correctly', (done) => { const telemetry = proxyquire('../../src/telemetry', { @@ -203,6 +290,7 @@ describe('telemetry with interval change', () => { process.nextTick(() => { expect(intervalSetCorrectly).to.be.true + telemetry.stop() done() }) })