Skip to content

Commit

Permalink
update guardrails to report telemetry in old node versions (#4949)
Browse files Browse the repository at this point in the history
  • Loading branch information
rochdev authored Nov 27, 2024
1 parent 82c489b commit ac19207
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 171 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down
72 changes: 4 additions & 68 deletions init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
26 changes: 1 addition & 25 deletions integration-tests/init.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 })

Expand Down
2 changes: 1 addition & 1 deletion packages/datadog-instrumentations/src/helpers/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '',
Expand Down
67 changes: 67 additions & 0 deletions packages/dd-trace/src/guardrails/index.js
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions packages/dd-trace/src/guardrails/log.js
Original file line number Diff line number Diff line change
@@ -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
78 changes: 78 additions & 0 deletions packages/dd-trace/src/guardrails/telemetry.js
Original file line number Diff line number Diff line change
@@ -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 }))
}
10 changes: 10 additions & 0 deletions packages/dd-trace/src/guardrails/util.js
Original file line number Diff line number Diff line change
@@ -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 }
Loading

0 comments on commit ac19207

Please sign in to comment.