UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

204 lines (170 loc) 6.9 kB
'use strict' const { builtinModules } = require('module') const path = require('path') const { channel } = require('dc-polyfill') const satisfies = require('../../../../vendor/dist/semifies') const log = require('../../../dd-trace/src/log') const telemetry = require('../../../dd-trace/src/guardrails/telemetry') const { IS_SERVERLESS } = require('../../../dd-trace/src/serverless') const { getValueFromEnvSources } = require('../../../dd-trace/src/config/helper') const checkRequireCache = require('./check-require-cache') const Hook = require('./hook') const { isRelativeRequire } = require('./shared-utils') const rewriter = require('./rewriter') const DD_TRACE_DISABLED_INSTRUMENTATIONS = getValueFromEnvSources('DD_TRACE_DISABLED_INSTRUMENTATIONS') || '' const DD_TRACE_DEBUG = getValueFromEnvSources('DD_TRACE_DEBUG') || '' const hooks = require('./hooks') const instrumentations = require('./instrumentations') const names = Object.keys(hooks) const pathSepExpr = new RegExp(`\\${path.sep}`, 'g') const disabledInstrumentations = new Set( DD_TRACE_DISABLED_INSTRUMENTATIONS?.split(',') ) const loadChannel = channel('dd-trace:instrumentation:load') // Globals if (!disabledInstrumentations.has('fetch')) { require('../fetch') } if (!disabledInstrumentations.has('process')) { require('../process') } if (DD_TRACE_DEBUG && DD_TRACE_DEBUG.toLowerCase() !== 'false') { checkRequireCache.checkForRequiredModules() setImmediate(checkRequireCache.checkForPotentialConflicts) } for (const inst of disabledInstrumentations) { rewriter.disable(inst) } /** @type {Map<string, object>} */ const instrumentedNodeModules = new Map() /** @type {Map<string, boolean>} */ const instrumentedIntegrationsSuccess = new Map() /** @type {Set<string>} */ const alreadyLoggedIncompatibleIntegrations = new Set() // Always disable prefixed and unprefixed node modules if one is disabled. if (disabledInstrumentations.size) { const builtinsSet = new Set(builtinModules) for (const name of disabledInstrumentations) { const hasPrefix = name.startsWith('node:') if (hasPrefix || builtinsSet.has(name)) { if (hasPrefix) { const unprefixedName = name.slice(5) if (!disabledInstrumentations.has(unprefixedName)) { disabledInstrumentations.add(unprefixedName) } } else if (!disabledInstrumentations.has(`node:${name}`)) { disabledInstrumentations.add(`node:${name}`) } } } builtinsSet.clear() } for (const name of names) { if (disabledInstrumentations.has(name)) continue const hookOptions = {} let hook = hooks[name] if (hook !== null && typeof hook === 'object') { if (hook.serverless === false && IS_SERVERLESS) continue hookOptions.internals = hook.esmFirst hook = hook.fn } Hook([name], hookOptions, (moduleExports, moduleName, moduleBaseDir, moduleVersion, isIitm) => { // All loaded versions are first expected to fail instrumentation. if (!instrumentedIntegrationsSuccess.has(`${name}@${moduleVersion}`)) { instrumentedIntegrationsSuccess.set(`${name}@${moduleVersion}`, false) } // This executes the integration file thus adding its entries to `instrumentations` hook() if (!instrumentations[name] || moduleExports === instrumentedNodeModules.get(name)) { return moduleExports } // Used for node: prefixed modules to prevent double instrumentation. if (moduleBaseDir) { moduleName = moduleName.replace(pathSepExpr, '/') } else { instrumentedNodeModules.set(name, moduleExports) } for (const { file, versions, hook, filePattern, patchDefault } of instrumentations[name]) { if (isIitm && patchDefault === !!moduleExports.default) { if (patchDefault) { moduleExports = moduleExports.default } else { return moduleExports } } const fullFilename = filename(name, file) let matchesFile = moduleName === fullFilename if (!matchesFile && isRelativeRequire(name)) matchesFile = true const fullFilePattern = filePattern && filename(name, filePattern) if (fullFilePattern) { // Some libraries include a hash in their filenames when installed, // so our instrumentation has to include a '.*' to match them for more than a single version. matchesFile ||= new RegExp(fullFilePattern).test(moduleName) } if (matchesFile && matchVersion(moduleVersion, versions)) { // Do not log in case of an error to prevent duplicate telemetry for the same integration version. instrumentedIntegrationsSuccess.set(`${name}@${moduleVersion}`, true) try { loadChannel.publish({ name }) moduleExports = hook(moduleExports, moduleVersion, isIitm) ?? moduleExports } catch (error) { log.info('Error during ddtrace instrumentation of application, aborting.', error) telemetry('error', [ `error_type:${error.constructor.name}`, `integration:${name}`, `integration_version:${moduleVersion}`, ], { result: 'error', result_class: 'internal_error', result_reason: `Error during instrumentation of ${name}@${moduleVersion}: ${error.message}`, }) } } } return moduleExports }) } globalThis[Symbol.for('dd-trace')]?.beforeExitHandlers.add(logAbortedIntegrations) // TODO: check if we want to stop using channels for single subscriber tasks channel('dd-trace:exporter:first-flush').subscribe(logAbortedIntegrations) function logAbortedIntegrations () { for (const [nameVersion, success] of instrumentedIntegrationsSuccess) { // Only ever log a single version of an integration, even if it is loaded later. if (!success && !alreadyLoggedIncompatibleIntegrations.has(nameVersion)) { const [name, version] = nameVersion.split('@') telemetry('abort.integration', [ `integration:${name}`, `integration_version:${version}`, ], { result: 'abort', result_class: 'incompatible_library', result_reason: `Incompatible integration version: ${name}@${version}`, }) log.info('Found incompatible integration version: %s', nameVersion) alreadyLoggedIncompatibleIntegrations.add(nameVersion) } } // Clear the map to avoid reporting the same integration version again. instrumentedIntegrationsSuccess.clear() } /** * @param {string|undefined} version * @param {string[]|undefined} ranges */ function matchVersion (version, ranges) { return !version || !ranges || ranges.some(range => satisfies(version, range)) } /** * @param {string} name * @param {string} [file] * @returns {string} */ function filename (name, file) { return file ? `${name}/${file}` : name } module.exports = { filename, pathSepExpr, loadChannel, matchVersion, }