newrelic
Version:
New Relic agent
624 lines (558 loc) • 17.4 kB
JavaScript
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
var a = require('async')
var path = require('path')
var fs = require('./util/unwrapped-core').fs
var os = require('os')
var logger = require('./logger').child({component: 'environment'})
var stringify = require('json-stringify-safe')
// As of 1.7.0 you can no longer dynamically link v8
// https://github.com/nodejs/io.js/commit/d726a177ed
var 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?"
}
var 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.
*
* @return {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.
* @param {function} callback - Callback function.
*
* @return {Array} List of packages.
*/
function listPackages(root, packages, callback) {
// listPackages(root, callback)
if (typeof packages === 'function') {
callback = packages
packages = []
}
_log('Listing packages in %s', root)
a.waterfall([
a.apply(fs.readdir, root),
function iterateDirs(dirs, cb) {
a.eachLimit(dirs, 2, forEachDir, cb)
}
], function onAllDirsRead(err) {
_log('Done listing packages in %s', root)
if (err) {
logger.trace(err, 'Could not list packages in %s (probably not an error)', root)
return callback()
}
callback(null, packages)
})
function forEachDir(dir, cb) {
_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 setImmediate(cb)
}
// Recurse into module scopes.
if (dir[0] === '@') {
logger.trace('Recursing into scoped module directory %s', dir)
return listPackages(path.resolve(root, dir), packages, cb)
}
// Read the package and pull out the name and version of it.
var pkg = path.resolve(root, dir, 'package.json')
fs.readFile(pkg, function onPackageRead(err, pkgFile) {
_log('Read package at %s', pkg)
if (err) {
logger.debug(err, 'Could not read %s.', pkg)
return cb()
}
var name = null
var version = null
try {
var pkgData = JSON.parse(pkgFile)
name = pkgData.name
version = pkgData.version
} catch (e) {
logger.debug(err, 'Could not parse package file %s.', pkg)
}
packages.push([name || dir, version || '<unknown>'])
_log('Package from %s added (%s@%s)', pkg, name, version)
cb()
})
}
}
/**
* 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.
* @param {function} callback - Callback to send deps to.
*
* @return {Array} List of dependencies.
*/
function listDependencies(root, children, visited, callback) {
// listDependencies(root, callback)
if (typeof children === 'function') {
callback = children
children = []
visited = Object.create(null)
}
// listDependencies(root, {children|visited}, callback)
if (typeof visited === 'function') {
callback = visited
if (Array.isArray(children)) {
visited = Object.create(null)
} else {
visited = children
children = []
}
}
_log('Listing dependencies in %s', root)
a.waterfall([
a.apply(fs.readdir, root),
function iterateDirs(dirs, cb) {
a.eachLimit(dirs, 2, forEachEntry, cb)
}
], function onAllDirsRead(err) {
_log('Done listing dependencies in %s', root)
if (err) {
logger.trace(err, 'Could not read directories in %s (probably not an error)', root)
return callback()
}
callback(null, children)
})
function forEachEntry(entry, cb) {
_log('Checking dependencies in %s (%s)', entry, root)
var candidate = path.resolve(root, entry, 'node_modules')
fs.realpath(candidate, function realPathCb(err, realCandidate) {
_log('Resolved %s to real path %s', candidate, realCandidate)
if (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)
return cb()
}
// Make sure we haven't been to this directory before.
if (visited[realCandidate]) {
logger.trace('Not revisiting %s (from %s)', realCandidate, candidate)
return cb()
}
visited[realCandidate] = true
// Load the packages and dependencies for this directory.
a.series([
a.apply(listPackages, realCandidate, children),
a.apply(listDependencies, realCandidate, children, visited)
], function onRecurseListComplete(loadErr) {
_log('Done with dependencies in %s', realCandidate)
if (loadErr) {
logger.debug(loadErr, 'Failed to list dependencies in %s', realCandidate)
}
cb()
})
})
}
}
/**
* Build up a list of packages, starting from the current directory.
*
* @return {Object} Two lists, of packages and dependencies, with the
* appropriate names.
*/
function getLocalPackages(callback) {
var packages = []
var dependencies = []
var candidate = process.cwd()
var visited = Object.create(null)
_log('Getting local packages')
a.whilst(function checkCandidate(cb) {
return cb(null, candidate)
}, function iterate(cb) {
_log('Checking for local packages in %s', candidate)
var root = path.resolve(candidate, 'node_modules')
a.series([
a.apply(listPackages, root, packages),
a.apply(listDependencies, root, dependencies, visited)
], function onListComplete(err) {
_log('Done checking for local packages in %s', candidate)
var last = candidate
candidate = path.dirname(candidate)
if (last === candidate) {
candidate = null
}
cb(err)
})
}, function whileComplete(err) {
_log('Done getting local packages')
if (err) {
callback(err)
} else {
callback(null, {packages: packages, dependencies: 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.
*
* @return {Object} Two lists, of packages and dependencies, with the
* appropriate names.
*/
function getPackages(root, cb) {
var packages = []
var dependencies = []
_log('Getting packages from %s', root)
a.series([
a.apply(listPackages, root, packages),
a.apply(listDependencies, root, dependencies)
], function onListComplete(err) {
_log('Done getting packages from %s', root)
if (err) {
cb(err)
} else {
cb(null, {packages: packages, dependencies: dependencies})
}
})
}
/**
* Generate a list of globally-installed packages, if available / accessible
* via the environment.
*
* @return {Object} Two lists, of packages and dependencies, with the
* appropriate names.
*/
function getGlobalPackages(cb) {
_log('Getting global packages')
if (process.config && process.config.variables) {
var prefix = process.config.variables.node_prefix
if (prefix) {
var root = path.resolve(prefix, 'lib', 'node_modules')
_log('Getting global packages from %s', root)
return getPackages(root, cb)
}
}
_log('No global packages to get')
setImmediate(cb, null, {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.
*
* @return {Array.<string[]>} Sorted list of [name, version] pairs.
*/
function flattenVersions(packages) {
var info = Object.create(null)
packages.forEach(function cb_forEach(pair) {
var p = pair[0]
var 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(function cb_map(key) {
return [key, info[key].join(', ')]
})
.sort()
.map(function cb_map(pair) {
try {
return stringify(pair)
} catch (err) {
logger.debug(err, 'Unabled 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) {
var variables = process.config.variables
Object.keys(variables).forEach(function cb_forEach(key) {
if (remapping[key]) {
var value = variables[key]
if (value === true || value === 1) value = 'yes'
if (value === false || value === 0) value = 'no'
addSetting(remapping[key], value)
}
})
}
}
function getOtherPackages(callback) {
_log('Getting other packages')
var other = {packages: [], dependencies: []}
if (!process.env.NODE_PATH) {
return callback(null, other)
}
var 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)
a.eachLimit(paths, 2, function listEachOtherPackage(nodePath, cb) {
if (nodePath[0] !== '/') nodePath = path.resolve(process.cwd(), nodePath)
_log('Getting other packages from %s', nodePath)
getPackages(nodePath, function onGetPackageFinish(err, nextSet) {
_log('Done getting other packages from %s', nodePath)
if (!err && nextSet) {
other.packages.push.apply(other.packages, nextSet.packages)
other.dependencies.push.apply(other.dependencies, nextSet.dependencies)
}
cb(err)
})
}, function onOtherFinish(err) {
_log('Done getting other packages')
callback(err, other)
})
}
function getHomePackages(cb) {
var 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 cb(null, null)
}
a.mapSeries({
home: path.resolve(homeDir, '.node_modules'),
homeOld: path.resolve(homeDir, '.node_libraries')
}, getPackages, function onHomeFinish(err, packages) {
_log('Done getting home packages from %s', homeDir)
cb(err, packages)
})
}
/**
* 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.
*/
function findPackages(cb) {
_log('Finding all packages')
a.parallelLimit({
local: time(getLocalPackages),
global: time(getGlobalPackages),
other: time(getOtherPackages),
home: time(getHomePackages)
}, 2, function onPackageComplete(err, data) {
_log('Done finding all packages')
if (err) {
return cb(err)
}
var packages = data.local.packages
packages.push.apply(packages, data.global.packages)
packages.push.apply(packages, data.other.packages)
var dependencies = data.local.dependencies
dependencies.push.apply(dependencies, data.global.dependencies)
dependencies.push.apply(dependencies, data.other.dependencies)
if (data.home) {
if (data.home.home) {
packages.unshift.apply(packages, data.home.home.packages)
dependencies.unshift.apply(dependencies, data.home.home.dependencies)
}
if (data.home.homeOld) {
packages.unshift.apply(packages, data.home.homeOld.packages)
dependencies.unshift.apply(dependencies, data.home.homeOld.dependencies)
}
}
addSetting('Packages', flattenVersions(packages))
addSetting('Dependencies', flattenVersions(dependencies))
cb()
})
}
function time(fn) {
var name = fn.name
return function timeWrapper(cb) {
var start = Date.now()
logger.trace('Starting %s', name)
return fn(function wrappedCb() {
var end = Date.now()
logger.trace('Finished %s in %dms', name, end - start)
cb.apply(this, arguments)
})
}
}
/**
* 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
var framework = getSetting('Framework')
var dispatcher = getSetting('Dispatcher')
var 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.
*/
function refresh(cb) {
_log('Refreshing environment settings')
refreshSyncOnly()
var packages = getSetting('Packages')
var dependencies = getSetting('Dependencies')
if (packages.length && dependencies.length) {
settings.Packages = packages
settings.Dependencies = dependencies
_log('Using cached values')
setImmediate(cb)
} else {
_log('Fetching new package information')
findPackages(cb)
}
}
/**
* Refreshes settings and returns the settings object.
*
* @private
*
* @param {function} cb - Callback to send results to.
*/
function getJSON(cb) {
_log('Getting environment JSON')
refresh(function onRefreshFinish(err) {
_log('Environment refresh finished')
if (err) {
cb(err)
return
}
var items = []
Object.keys(settings).forEach(function settingKeysForEach(key) {
settings[key].forEach(function settingsValuesForEach(setting) {
items.push([key, setting])
})
})
_log('JSON got')
cb(null, items)
})
}
// At startup, do the synchronous environment scanning stuff.
refreshSyncOnly()
var 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: listPackages,
getJSON: getJSON,
get: getSetting,
refresh: refresh
}
/**
* For super verbose logging that we can disable completely, separate from the
* rest of logging.
*/
function _log() {
// logger.trace.apply(logger, arguments)
}