UNPKG

newrelic

Version:
556 lines (488 loc) 15.4 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' const path = require('path') const { fsPromises } = require('./util/unwrapped-core') const os = require('os') const logger = require('./logger').child({ component: 'environment' }) const stringify = require('json-stringify-safe') const asyncEachLimit = require('./util/async-each-limit') const DISPATCHER_VERSION = 'Dispatcher Version' // As of 1.7.0 you can no longer dynamically link v8 // https://github.com/nodejs/io.js/commit/d726a177ed const remapping = { node_install_npm: 'npm installed?', node_install_waf: 'WAF build system installed?', node_use_openssl: 'OpenSSL support?', node_shared_openssl: 'Dynamically linked to OpenSSL?', node_shared_v8: 'Dynamically linked to V8?', node_shared_zlib: 'Dynamically linked to Zlib?', node_use_dtrace: 'DTrace support?', node_use_etw: 'Event Tracing for Windows (ETW) support?' } let settings = Object.create(null) /** * Fetches the setting of the given name, defaulting to an empty array. * * @param {string} name - The name of the setting to look for. * @returns {Array.<string>} An array of values matching that name. */ function getSetting(name) { return settings[name] || [] } /** * Add a setting to the module's shared settings object. * * @param {string} name - The name of the setting value being added. * @param {string} value - The value to add or the setting. */ function addSetting(name, value) { if (!settings[name]) { settings[name] = [value] } else if (settings[name].indexOf(value) === -1) { settings[name].push(value) } } /** * Remove settings with the given name. * * @param {string} name - The name of the setting to remove. */ function clearSetting(name) { delete settings[name] } /** * Build up a list of top-level packages available to an application relative to * the provided root. * * @param {string} root - Path to start listing packages from. * @param {Array} [packages] - Array to append found packages to. */ async function listPackages(root, packages = []) { _log('Listing packages in %s', root) try { const dirs = await fsPromises.readdir(root) await asyncEachLimit(dirs, forEachDir, 2) _log('Done listing packages in %s', root) } catch (err) { logger.trace(err, 'Could not list packages in %s (probably not an error)', root) } async function forEachDir(dir) { _log('Checking package %s in %s', dir, root) // Skip npm's binary directory where it stores executables. if (dir === '.bin') { _log('Skipping .bin directory') return } // Recurse into module scopes. if (dir[0] === '@') { logger.trace('Recursing into scoped module directory %s', dir) return listPackages(path.resolve(root, dir), packages) } // Read the package and pull out the name and version of it. const pkg = path.resolve(root, dir, 'package.json') let name = null let version = null try { const pkgFile = await fsPromises.readFile(pkg) _log('Read package at %s', pkg) ;({ name, version } = JSON.parse(pkgFile)) } catch (err) { logger.debug(err, 'Could not read %s.', pkg) } packages.push([name || dir, version || '<unknown>']) _log('Package from %s added (%s@%s)', pkg, name, version) } } /** * Build up a list of dependencies from a given node_module root. * * @param {string} root - Path to start listing dependencies from. * @param {Array} [children] - Array to append found dependencies to. * @param {object} [visited] - Map of visited directories. */ async function listDependencies(root, children = [], visited = Object.create(null)) { _log('Listing dependencies in %s', root) try { const dirs = await fsPromises.readdir(root) await asyncEachLimit(dirs, forEachEntry, 2) _log('Done listing dependencies in %s', root) } catch (err) { logger.trace(err, 'Could not read directories in %s (probably not an error)', root) } async function forEachEntry(entry) { _log('Checking dependencies in %s (%s)', entry, root) const candidate = path.resolve(root, entry, 'node_modules') try { const realCandidate = await fsPromises.realpath(candidate) _log('Resolved %s to real path %s', candidate, realCandidate) // Make sure we haven't been to this directory before. if (visited[realCandidate]) { logger.trace('Not revisiting %s (from %s)', realCandidate, candidate) return } visited[realCandidate] = true // Load the packages and dependencies for this directory. await listPackages(realCandidate, children) await listDependencies(realCandidate, children, visited) _log('Done with dependencies in %s', realCandidate) } catch (err) { // Don't care to log about files that don't exist. if (err.code !== 'ENOENT') { logger.debug(err, 'Failed to resolve candidate real path %s', candidate) } _log(err, 'Real path for %s failed', candidate) } } } /** * Build up a list of packages, starting from the current directory. * * @returns {object} Two lists, of packages and dependencies, with the * appropriate names. */ async function getLocalPackages() { const packages = [] const dependencies = [] let candidate = process.cwd() const visited = Object.create(null) _log('Getting local packages') while (candidate) { _log('Checking for local packages in %s', candidate) const root = path.resolve(candidate, 'node_modules') await listPackages(root, packages) await listDependencies(root, dependencies, visited) _log('Done checking for local packages in %s', candidate) const last = candidate candidate = path.dirname(candidate) if (last === candidate) { candidate = null } } _log('Done getting local packages') return { packages, dependencies } } /** * Generic method for getting packages and dependencies relative to a * provided root directory. * * @param {string} root - Where to start looking -- doesn't add node_modules. * @returns {object} Two lists, of packages and dependencies, with the * appropriate names. */ async function getPackages(root) { const packages = [] const dependencies = [] _log('Getting packages from %s', root) await listPackages(root, packages) await listDependencies(root, dependencies) _log('Done getting packages from %s', root) return { packages, dependencies } } /** * Generate a list of globally-installed packages, if available / accessible * via the environment. * * @returns {object} Two lists, of packages and dependencies, with the * appropriate names. */ function getGlobalPackages() { _log('Getting global packages') if (process.config && process.config.variables) { const prefix = process.config.variables.node_prefix if (prefix) { const root = path.resolve(prefix, 'lib', 'node_modules') _log('Getting global packages from %s', root) return getPackages(root) } } _log('No global packages to get') return { packages: [], dependencies: [] } } /** * Take a list of packages and reduce it to a list of pairs serialized * to JSON (to simplify things on the collector end) where each * package appears at most once, with all the versions joined into a * comma-delimited list. * * @param {Array} packages list of packages to process * @returns {Array.<string[]>} Sorted list of [name, version] pairs. */ function flattenVersions(packages) { const info = Object.create(null) packages.forEach((pair) => { const p = pair[0] const v = pair[1] if (info[p]) { if (info[p].indexOf(v) < 0) { info[p].push(v) } } else { info[p] = [v] } }) return Object.keys(info) .map((key) => [key, info[key].join(', ')]) .sort() .map((pair) => { try { return stringify(pair) } catch (err) { logger.debug(err, 'Unable to stringify package version') return '<unknown>' } }) } /** * There are a bunch of settings generated at build time that are useful to * know for troubleshooting purposes. These settings are only available in 0.7 * and up. * * This function works entirely via side effects using the * addSetting function. */ function remapConfigSettings() { if (process.config && process.config.variables) { const variables = process.config.variables Object.keys(variables).forEach((key) => { if (remapping[key]) { let value = variables[key] if (value === true || value === 1) { value = 'yes' } if (value === false || value === 0) { value = 'no' } addSetting(remapping[key], value) } }) addMissingProcessVars() } } /** * As of Node 19 DTrace and ETW are no longer bundled * see: https://nodejs.org/en/blog/announcements/v19-release-announce#dtrace/systemtap/etw-support */ function addMissingProcessVars() { addSetting(remapping.node_use_dtrace, 'no') addSetting(remapping.node_use_etw, 'no') } async function getOtherPackages() { _log('Getting other packages') const other = { packages: [], dependencies: [] } if (!process.env.NODE_PATH) { return other } let paths if (process.platform === 'win32') { // why. WHY. paths = process.env.NODE_PATH.split(';') } else { paths = process.env.NODE_PATH.split(':') } _log('Looking for other packages in %j', paths) const otherPackages = await asyncEachLimit( paths, (nodePath) => { if (nodePath[0] !== '/') { nodePath = path.resolve(process.cwd(), nodePath) } _log('Getting other packages from %s', nodePath) return getPackages(nodePath) }, 2 ) otherPackages.forEach((pkg) => { other.packages.push.apply(other.packages, pkg.packages) other.dependencies.push.apply(other.dependencies, pkg.dependencies) }) _log('Done getting other packages') return other } async function getHomePackages() { let homeDir = null if (process.platform === 'win32') { if (process.env.USERDIR) { homeDir = process.env.USERDIR } } else if (process.env.HOME) { homeDir = process.env.HOME } _log('Getting home packages from %s', homeDir) if (!homeDir) { return } const homePath = path.resolve(homeDir, '.node_modules') const homeOldPath = path.resolve(homeDir, '.node_libraries') const home = await getPackages(homePath) const homeOld = await getPackages(homeOldPath) return { home, homeOld } } /** * Scrape the list of packages, following the algorithm as described in the * node module page: * * http://nodejs.org/docs/latest/api/modules.html * * This function works entirely via side effects using the addSetting * function. */ async function findPackages() { _log('Finding all packages') const pkgPromises = [ time(getLocalPackages), time(getGlobalPackages), time(getOtherPackages), time(getHomePackages) ] const [local, global, other, home] = await Promise.all(pkgPromises) _log('Done finding all packages') const packages = local.packages packages.push.apply(packages, global.packages) packages.push.apply(packages, other.packages) const dependencies = local.dependencies dependencies.push.apply(dependencies, global.dependencies) dependencies.push.apply(dependencies, other.dependencies) if (home) { if (home.home) { packages.unshift.apply(packages, home.home.packages) dependencies.unshift.apply(dependencies, home.home.dependencies) } if (home.homeOld) { packages.unshift.apply(packages, home.homeOld.packages) dependencies.unshift.apply(dependencies, home.homeOld.dependencies) } } addSetting('Packages', flattenVersions(packages)) addSetting('Dependencies', flattenVersions(dependencies)) } async function time(fn) { const name = fn.name const start = Date.now() logger.trace('Starting %s', name) const data = await fn() const end = Date.now() logger.trace('Finished %s in %dms', name, end - start) return data } /** * Settings actually get scraped below. */ function gatherEnv() { addSetting('Processors', os.cpus().length) addSetting('OS', os.type()) addSetting('OS version', os.release()) addSetting('Node.js version', process.version) addSetting('Architecture', process.arch) if ('NODE_ENV' in process.env) { addSetting('NODE_ENV', process.env.NODE_ENV) } } function refreshSyncOnly() { // gather persisted settings const framework = getSetting('Framework') const dispatcher = getSetting('Dispatcher') const dispatcherVersion = getSetting(DISPATCHER_VERSION) // clearing and rebuilding a global variable settings = Object.create(null) // add persisted settings if (framework.length) { framework.forEach(function addFrameworks(fw) { addSetting('Framework', fw) }) } if (dispatcher.length) { dispatcher.forEach(function addDispatchers(d) { addSetting('Dispatcher', d) }) } if (dispatcherVersion.length) { dispatcher.forEach(function addDispatchers(d) { addSetting(DISPATCHER_VERSION, d) }) } gatherEnv() remapConfigSettings() } /** * Reset settings and gather them, built to minimally refactor this file. */ async function refresh() { _log('Refreshing environment settings') refreshSyncOnly() const packages = getSetting('Packages') const dependencies = getSetting('Dependencies') if (packages.length && dependencies.length) { settings.Packages = packages settings.Dependencies = dependencies _log('Using cached values') return } _log('Fetching new package information') await findPackages() } /** * Refreshes settings and returns the settings object. * * @private * @returns {Promise} the updated/refreshed settings */ async function getJSON() { _log('Getting environment JSON') try { await refresh() } catch { // swallow error } const items = [] Object.keys(settings).forEach(function settingKeysForEach(key) { settings[key].forEach(function settingsValuesForEach(setting) { items.push([key, setting]) }) }) _log('JSON got') return items } // At startup, do the synchronous environment scanning stuff. refreshSyncOnly() let userSetDispatcher = false module.exports = { setFramework: function setFramework(framework) { addSetting('Framework', framework) }, setDispatcher: function setDispatcher(dispatcher, version, userSet) { if (userSetDispatcher) { return } userSetDispatcher = !!userSet clearSetting(DISPATCHER_VERSION) clearSetting('Dispatcher') // TODO: Decide if this should only happen once for internals as well. if (version) { addSetting(DISPATCHER_VERSION, version) } addSetting('Dispatcher', dispatcher) }, clearFramework: function clearFramework() { clearSetting('Framework') }, clearDispatcher: function clearDispatcher() { // This method is only used for tests. userSetDispatcher = false clearSetting('Dispatcher') clearSetting(DISPATCHER_VERSION) }, listPackages, getJSON, get: getSetting, refresh } /** * For super verbose logging that we can disable completely, separate from the * rest of logging. */ function _log() { // logger.trace.apply(logger, arguments) }