From ac1920755519f880867cd1799535f3fd540a6a0d Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Wed, 27 Nov 2024 16:01:55 -0500 Subject: [PATCH] update guardrails to report telemetry in old node versions (#4949) --- .github/workflows/project.yml | 4 +- init.js | 72 +---------------- integration-tests/init.spec.js | 26 +------ .../src/helpers/register.js | 2 +- packages/dd-trace/src/guardrails/index.js | 67 ++++++++++++++++ packages/dd-trace/src/guardrails/log.js | 32 ++++++++ packages/dd-trace/src/guardrails/telemetry.js | 78 +++++++++++++++++++ packages/dd-trace/src/guardrails/util.js | 10 +++ .../dd-trace/src/telemetry/init-telemetry.js | 75 ------------------ 9 files changed, 195 insertions(+), 171 deletions(-) create mode 100644 packages/dd-trace/src/guardrails/index.js create mode 100644 packages/dd-trace/src/guardrails/log.js create mode 100644 packages/dd-trace/src/guardrails/telemetry.js create mode 100644 packages/dd-trace/src/guardrails/util.js delete mode 100644 packages/dd-trace/src/telemetry/init-telemetry.js diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 92a97c56457..c58392833d2 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -34,7 +34,7 @@ jobs: integration-guardrails: strategy: matrix: - version: [12.0.0, 12, 14.0.0, 14, 16.0.0, 16, 18.0.0, 18.1.0, 20.0.0, 22.0.0] + version: [12, 14.0.0, 14, 16.0.0, 16, 18.0.0, 18.1.0, 20.0.0, 22.0.0] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -47,7 +47,7 @@ jobs: integration-guardrails-unsupported: strategy: matrix: - version: ['0.8', '0.10', '0.12', '4', '6', '8', '10'] + version: ['0.8', '0.10', '0.12', '4', '6', '8', '10', '12.0.0'] runs-on: ubuntu-latest env: DD_INJECTION_ENABLED: 'true' diff --git a/init.js b/init.js index d9286b0307f..625d493b3b1 100644 --- a/init.js +++ b/init.js @@ -2,72 +2,8 @@ /* eslint-disable no-var */ -var nodeVersion = require('./version') -var NODE_MAJOR = nodeVersion.NODE_MAJOR -var NODE_MINOR = nodeVersion.NODE_MINOR +var guard = require('./packages/dd-trace/src/guardrails') -// We use several things that are not supported by older versions of Node: -// - AsyncLocalStorage -// - The `semver` module -// - dc-polyfill -// - Mocha (for testing) -// and probably others. -// TODO: Remove all these dependencies so that we can report telemetry. -if ((NODE_MAJOR === 12 && NODE_MINOR >= 17) || NODE_MAJOR > 12) { - var path = require('path') - var Module = require('module') - var semver = require('semver') - var log = require('./packages/dd-trace/src/log') - var isTrue = require('./packages/dd-trace/src/util').isTrue - var telemetry = require('./packages/dd-trace/src/telemetry/init-telemetry') - - var initBailout = false - var clobberBailout = false - var forced = isTrue(process.env.DD_INJECT_FORCE) - - if (process.env.DD_INJECTION_ENABLED) { - // If we're running via single-step install, and we're not in the app's - // node_modules, then we should not initialize the tracer. This prevents - // single-step-installed tracer from clobbering the manually-installed tracer. - var resolvedInApp - var entrypoint = process.argv[1] - try { - resolvedInApp = Module.createRequire(entrypoint).resolve('dd-trace') - } catch (e) { - // Ignore. If we can't resolve the module, we assume it's not in the app. - } - if (resolvedInApp) { - var ourselves = path.join(__dirname, 'index.js') - if (ourselves !== resolvedInApp) { - clobberBailout = true - } - } - - // If we're running via single-step install, and the runtime doesn't match - // the engines field in package.json, then we should not initialize the tracer. - if (!clobberBailout) { - var engines = require('./package.json').engines - var version = process.versions.node - if (!semver.satisfies(version, engines.node)) { - initBailout = true - telemetry([ - { name: 'abort', tags: ['reason:incompatible_runtime'] }, - { name: 'abort.runtime', tags: [] } - ]) - log.info('Aborting application instrumentation due to incompatible_runtime.') - log.info('Found incompatible runtime nodejs ' + version + ', Supported runtimes: nodejs ' + engines.node + '.') - if (forced) { - log.info('DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing.') - } - } - } - } - - if (!clobberBailout && (!initBailout || forced)) { - var tracer = require('.') - tracer.init() - module.exports = tracer - telemetry('complete', ['injection_forced:' + (forced && initBailout ? 'true' : 'false')]) - log.info('Application instrumentation bootstrapping complete') - } -} +module.exports = guard(function () { + return require('.').init() +}) diff --git a/integration-tests/init.spec.js b/integration-tests/init.spec.js index 3c37004f607..03a17d5f4c7 100644 --- a/integration-tests/init.spec.js +++ b/integration-tests/init.spec.js @@ -20,7 +20,6 @@ const telemetryGood = ['complete', 'injection_forced:false'] const { engines } = require('../package.json') const supportedRange = engines.node const currentVersionIsSupported = semver.satisfies(process.versions.node, supportedRange) -const currentVersionCanLog = semver.satisfies(process.versions.node, '>=12.17.0') // These are on by default in release tests, so we'll turn them off for // more fine-grained control of these variables in these tests. @@ -84,30 +83,7 @@ function testRuntimeVersionChecks (arg, filename) { } } - if (!currentVersionCanLog) { - context('when node version is too low for AsyncLocalStorage', () => { - useEnv({ NODE_OPTIONS }) - - it('should initialize the tracer, if no DD_INJECTION_ENABLED', () => - doTest('false\n')) - context('with DD_INJECTION_ENABLED', () => { - useEnv({ DD_INJECTION_ENABLED }) - - context('without debug', () => { - it('should not initialize the tracer', () => doTest('false\n')) - it('should not, if DD_INJECT_FORCE', () => doTestForced('false\n')) - }) - context('with debug', () => { - useEnv({ DD_TRACE_DEBUG }) - - it('should not initialize the tracer', () => - doTest('false\n')) - it('should initialize the tracer, if DD_INJECT_FORCE', () => - doTestForced('false\n')) - }) - }) - }) - } else if (!currentVersionIsSupported) { + if (!currentVersionIsSupported) { context('when node version is less than engines field', () => { useEnv({ NODE_OPTIONS }) diff --git a/packages/datadog-instrumentations/src/helpers/register.js b/packages/datadog-instrumentations/src/helpers/register.js index 4b4185423c0..171db91e224 100644 --- a/packages/datadog-instrumentations/src/helpers/register.js +++ b/packages/datadog-instrumentations/src/helpers/register.js @@ -7,7 +7,7 @@ const Hook = require('./hook') const requirePackageJson = require('../../../dd-trace/src/require-package-json') const log = require('../../../dd-trace/src/log') const checkRequireCache = require('../check_require_cache') -const telemetry = require('../../../dd-trace/src/telemetry/init-telemetry') +const telemetry = require('../../../dd-trace/src/guardrails/telemetry') const { DD_TRACE_DISABLED_INSTRUMENTATIONS = '', diff --git a/packages/dd-trace/src/guardrails/index.js b/packages/dd-trace/src/guardrails/index.js new file mode 100644 index 00000000000..249b9343a39 --- /dev/null +++ b/packages/dd-trace/src/guardrails/index.js @@ -0,0 +1,67 @@ +'use strict' + +/* eslint-disable no-var */ + +var path = require('path') +var Module = require('module') +var isTrue = require('./util').isTrue +var log = require('./log') +var telemetry = require('./telemetry') +var nodeVersion = require('../../../../version') + +var NODE_MAJOR = nodeVersion.NODE_MAJOR + +// TODO: Test telemetry for Node <12. For now only bailout is tested for those. +function guard (fn) { + var initBailout = false + var clobberBailout = false + var forced = isTrue(process.env.DD_INJECT_FORCE) + + if (process.env.DD_INJECTION_ENABLED) { + // If we're running via single-step install, and we're not in the app's + // node_modules, then we should not initialize the tracer. This prevents + // single-step-installed tracer from clobbering the manually-installed tracer. + var resolvedInApp + var entrypoint = process.argv[1] + try { + resolvedInApp = Module.createRequire(entrypoint).resolve('dd-trace') + } catch (e) { + // Ignore. If we can't resolve the module, we assume it's not in the app. + } + if (resolvedInApp) { + var ourselves = path.normalize(path.join(__dirname, '..', '..', '..', '..', 'index.js')) + if (ourselves !== resolvedInApp) { + clobberBailout = true + } + } + + // If we're running via single-step install, and the runtime doesn't match + // the engines field in package.json, then we should not initialize the tracer. + if (!clobberBailout) { + var engines = require('../../../../package.json').engines + var minMajor = parseInt(engines.node.replace(/[^0-9]/g, '')) + var version = process.versions.node + if (NODE_MAJOR < minMajor) { + initBailout = true + telemetry([ + { name: 'abort', tags: ['reason:incompatible_runtime'] }, + { name: 'abort.runtime', tags: [] } + ]) + log.info('Aborting application instrumentation due to incompatible_runtime.') + log.info('Found incompatible runtime nodejs ' + version + ', Supported runtimes: nodejs ' + engines.node + '.') + if (forced) { + log.info('DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing.') + } + } + } + } + + if (!clobberBailout && (!initBailout || forced)) { + var result = fn() + telemetry('complete', ['injection_forced:' + (forced && initBailout ? 'true' : 'false')]) + log.info('Application instrumentation bootstrapping complete') + return result + } +} + +module.exports = guard diff --git a/packages/dd-trace/src/guardrails/log.js b/packages/dd-trace/src/guardrails/log.js new file mode 100644 index 00000000000..dd74e5bdbf0 --- /dev/null +++ b/packages/dd-trace/src/guardrails/log.js @@ -0,0 +1,32 @@ +'use strict' + +/* eslint-disable no-var */ +/* eslint-disable no-console */ + +var isTrue = require('./util').isTrue + +var DD_TRACE_DEBUG = process.env.DD_TRACE_DEBUG +var DD_TRACE_LOG_LEVEL = process.env.DD_TRACE_LOG_LEVEL + +var logLevels = { + trace: 20, + debug: 20, + info: 30, + warn: 40, + error: 50, + critical: 50, + off: 100 +} + +var logLevel = isTrue(DD_TRACE_DEBUG) + ? Number(DD_TRACE_LOG_LEVEL) || logLevels.debug + : logLevels.off + +var log = { + debug: logLevel <= 20 ? console.debug.bind(console) : function () {}, + info: logLevel <= 30 ? console.info.bind(console) : function () {}, + warn: logLevel <= 40 ? console.warn.bind(console) : function () {}, + error: logLevel <= 50 ? console.error.bind(console) : function () {} +} + +module.exports = log diff --git a/packages/dd-trace/src/guardrails/telemetry.js b/packages/dd-trace/src/guardrails/telemetry.js new file mode 100644 index 00000000000..0c73e1f0bce --- /dev/null +++ b/packages/dd-trace/src/guardrails/telemetry.js @@ -0,0 +1,78 @@ +'use strict' + +/* eslint-disable no-var */ +/* eslint-disable object-shorthand */ + +var fs = require('fs') +var spawn = require('child_process').spawn +var tracerVersion = require('../../../../package.json').version +var log = require('./log') + +module.exports = sendTelemetry + +if (!process.env.DD_INJECTION_ENABLED) { + module.exports = function () {} +} + +if (!process.env.DD_TELEMETRY_FORWARDER_PATH) { + module.exports = function () {} +} + +if (!fs.existsSync(process.env.DD_TELEMETRY_FORWARDER_PATH)) { + module.exports = function () {} +} + +var metadata = { + language_name: 'nodejs', + language_version: process.versions.node, + runtime_name: 'nodejs', + runtime_version: process.versions.node, + tracer_version: tracerVersion, + pid: process.pid +} + +var seen = [] +function hasSeen (point) { + if (point.name === 'abort') { + // This one can only be sent once, regardless of tags + return seen.includes('abort') + } + if (point.name === 'abort.integration') { + // For now, this is the only other one we want to dedupe + var compiledPoint = point.name + point.tags.join('') + return seen.includes(compiledPoint) + } + return false +} + +function sendTelemetry (name, tags) { + var points = name + if (typeof name === 'string') { + points = [{ name: name, tags: tags || [] }] + } + if (['1', 'true', 'True'].indexOf(process.env.DD_INJECT_FORCE) !== -1) { + points = points.filter(function (p) { return ['error', 'complete'].includes(p.name) }) + } + points = points.filter(function (p) { return !hasSeen(p) }) + for (var i = 0; i < points.length; i++) { + points[i].name = 'library_entrypoint.' + points[i].name + } + if (points.length === 0) { + return + } + var proc = spawn(process.env.DD_TELEMETRY_FORWARDER_PATH, ['library_entrypoint'], { + stdio: 'pipe' + }) + proc.on('error', function () { + log.error('Failed to spawn telemetry forwarder') + }) + proc.on('exit', function (code) { + if (code !== 0) { + log.error('Telemetry forwarder exited with code ' + code) + } + }) + proc.stdin.on('error', function () { + log.error('Failed to write telemetry data to telemetry forwarder') + }) + proc.stdin.end(JSON.stringify({ metadata: metadata, points: points })) +} diff --git a/packages/dd-trace/src/guardrails/util.js b/packages/dd-trace/src/guardrails/util.js new file mode 100644 index 00000000000..9aa60713573 --- /dev/null +++ b/packages/dd-trace/src/guardrails/util.js @@ -0,0 +1,10 @@ +'use strict' + +/* eslint-disable object-shorthand */ + +function isTrue (str) { + str = String(str).toLowerCase() + return str === 'true' || str === '1' +} + +module.exports = { isTrue: isTrue } diff --git a/packages/dd-trace/src/telemetry/init-telemetry.js b/packages/dd-trace/src/telemetry/init-telemetry.js deleted file mode 100644 index a126ecc6238..00000000000 --- a/packages/dd-trace/src/telemetry/init-telemetry.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict' - -const fs = require('fs') -const { spawn } = require('child_process') -const tracerVersion = require('../../../../package.json').version -const log = require('../log') - -module.exports = sendTelemetry - -if (!process.env.DD_INJECTION_ENABLED) { - module.exports = () => {} -} - -if (!process.env.DD_TELEMETRY_FORWARDER_PATH) { - module.exports = () => {} -} - -if (!fs.existsSync(process.env.DD_TELEMETRY_FORWARDER_PATH)) { - module.exports = () => {} -} - -const metadata = { - language_name: 'nodejs', - language_version: process.versions.node, - runtime_name: 'nodejs', - runtime_version: process.versions.node, - tracer_version: tracerVersion, - pid: process.pid -} - -const seen = [] -function hasSeen (point) { - if (point.name === 'abort') { - // This one can only be sent once, regardless of tags - return seen.includes('abort') - } - if (point.name === 'abort.integration') { - // For now, this is the only other one we want to dedupe - const compiledPoint = point.name + point.tags.join('') - return seen.includes(compiledPoint) - } - return false -} - -function sendTelemetry (name, tags = []) { - let points = name - if (typeof name === 'string') { - points = [{ name, tags }] - } - if (['1', 'true', 'True'].includes(process.env.DD_INJECT_FORCE)) { - points = points.filter(p => ['error', 'complete'].includes(p.name)) - } - points = points.filter(p => !hasSeen(p)) - points.forEach(p => { - p.name = `library_entrypoint.${p.name}` - }) - if (points.length === 0) { - return - } - const proc = spawn(process.env.DD_TELEMETRY_FORWARDER_PATH, ['library_entrypoint'], { - stdio: 'pipe' - }) - proc.on('error', () => { - log.error('Failed to spawn telemetry forwarder') - }) - proc.on('exit', (code) => { - if (code !== 0) { - log.error(`Telemetry forwarder exited with code ${code}`) - } - }) - proc.stdin.on('error', () => { - log.error('Failed to write telemetry data to telemetry forwarder') - }) - proc.stdin.end(JSON.stringify({ metadata, points })) -}