newrelic
Version:
New Relic agent
1,351 lines (1,198 loc) • 41.2 kB
JavaScript
'use strict'
const AttributeFilter = require('./attribute-filter')
const CollectorResponse = require('../collector/response')
const copy = require('../util/copy')
const defaultConfig = require('./default').config
const EventEmitter = require('events').EventEmitter
const feature_flag = require('../feature_flags')
const flatten = require('../util/flatten')
const fs = require('../util/unwrapped-core').fs
const hashes = require('../util/hashes')
const os = require('os')
const parseKey = require('../collector/key-parser').parseKey
const path = require('path')
const psemver = require('../util/process-version')
const stringify = require('json-stringify-safe')
const util = require('util')
/**
* CONSTANTS -- we gotta lotta 'em
*/
const AZURE_APP_NAME = 'APP_POOL_ID'
const DEFAULT_MAX_PAYLOAD_SIZE_IN_BYTES = 1000000
const DEFAULT_CONFIG_PATH = require.resolve('./default')
const DEFAULT_FILENAME = 'newrelic.js'
const CONFIG_FILE_LOCATIONS = [
process.env.NEW_RELIC_HOME,
process.cwd(),
process.env.HOME,
path.join(__dirname, '../../../..') // above node_modules
]
const HAS_ARBITRARY_KEYS = [
'labels'
]
const LASP_MAP = require('./lasp').LASP_MAP
const ENV = require('./env')
const HSM = require('./hsm')
const exists = fs.existsSync || path.existsSync
let logger = null // Lazy-loaded in `initialize`.
let _configInstance = null
// the REPL has no main module
if (process.mainModule && process.mainModule.filename) {
CONFIG_FILE_LOCATIONS.splice(2, 0, path.dirname(process.mainModule.filename))
}
function isTruthular(setting) {
if (setting === undefined || setting === null) return false
var normalized = setting.toString().toLowerCase()
switch (normalized) {
case 'false':
case 'f':
case 'no':
case 'n':
case 'disabled':
case '0':
return false
default:
return true
}
}
function fromObjectList(setting) {
try {
return JSON.parse('[' + setting + ']')
} catch (error) {
logger.error("New Relic configurator could not deserialize object list:")
logger.error(error.stack)
}
}
function _findConfigFile() {
var candidate
var filepath
for (var i = 0; i < CONFIG_FILE_LOCATIONS.length; i++) {
candidate = CONFIG_FILE_LOCATIONS[i]
if (!candidate) continue
filepath = path.join(path.resolve(candidate), DEFAULT_FILENAME)
if (!exists(filepath)) continue
return fs.realpathSync(filepath)
}
}
function Config(config) {
EventEmitter.call(this)
// 1. start by cloning the defaults
try {
var basis = defaultConfig()
Object.keys(basis).forEach(function setConfigKey(key) {
this[key] = basis[key]
}, this)
} catch (err) {
logger.warn('Unable to clone the default config, %s: %s', DEFAULT_CONFIG_PATH, err)
}
// 2. initialize undocumented, internal-only default values
// feature flags are mostly private settings for gating unreleased features
// flags are set in the feature_flags.js file
this.feature_flag = copy.shallow(feature_flag.prerelease)
// set by environment
this.newrelic_home = null
// set by configuration file loader
this.config_file_path = null
// set by collector on handshake
this.run_id = null
this.account_id = null
this.application_id = null
this.web_transactions_apdex = Object.create(null)
this.cross_process_id = null
this.encoding_key = null
this.obfuscatedId = null
this.primary_application_id = null
this.trusted_account_ids = null
this.trusted_account_key = null
this.sampling_target = 10
this.sampling_target_period_in_seconds = 60
this.max_payload_size_in_bytes = DEFAULT_MAX_PAYLOAD_SIZE_IN_BYTES
// how frequently harvester runs
this.data_report_period = 60
// this value is arbitrary
this.max_trace_segments = 900
// feature level of this account
this.product_level = 0
// product-level related
this.collect_traces = true
this.collect_errors = true
// override options for utilization stats
this.utilization.logical_processors = null
this.utilization.total_ram_mib = null
this.utilization.billing_hostname = null
this.browser_monitoring.loader = 'rum'
this.browser_monitoring.loader_version = ''
// Settings to play nice with DLPs (see NODE-1044).
this.compressed_content_encoding = "deflate" // Deflate or gzip
this.simple_compression = false // Disables subcomponent compression
this.put_for_data_send = false // Changes http verb for harvest
// 3. override defaults with values from the loaded / passed configuration
this._fromPassed(config)
// 3.5. special values (only Azure environment APP_POOL_ID for now)
this._fromSpecial()
// 4. override config with environment variables
this._featureFlagsFromEnv()
this._fromEnvironment()
// 5. clean up anything that requires postprocessing
this._canonicalize()
// 6. put the version in the config
this.version = require('../../package.json').version
// 7. apply high security overrides
if (this.high_security === true) {
if (this.security_policies_token) {
throw new Error(
"Security Policies and High Security Mode cannot both be present " +
"in the agent configuration. If Security Policies have been set " +
"for your account, please ensure the security_policies_token is " +
"set but high_security is disabled (default)."
)
}
this._applyHighSecurity()
}
// 8. serverless_mode specific config setting
this._enforceServerlessConfig()
// 9. Set instance attribute filter using updated context
this.attributeFilter = new AttributeFilter(this)
}
util.inherits(Config, EventEmitter)
/**
* Because this module and logger depend on each other, the logger needs
* a way to inject the actual logger instance once it's constructed.
* It's kind of a Rube Goldberg device, but it works.
*
* @param {Logger} bootstrapped The actual, configured logger.
*/
Config.prototype.setLogger = function setLogger(bootstrapped) {
logger = bootstrapped
}
/**
* Accept any configuration passed back from the server. Will log all
* recognized, unsupported, and unknown parameters. Some may not be set,
* depending on the setting of ignore_server_configuration.
*
* @param {object} json The config blob sent by New Relic.
*/
Config.prototype.onConnect = function onConnect(json, recursion) {
json = json || Object.create(null)
if (this.high_security === true && recursion !== true && json.high_security !== true) {
this.agent_enabled = false
this.emit('agent_enabled', false)
return
}
if (Object.keys(json).length === 0) return
Object.keys(json).forEach(function updateProp(key) {
this._fromServer(json, key)
}, this)
this.emit('change', this)
}
Config.prototype._getMostSecure = function getMostSecure(key, currentVal, newVal) {
var filter = LASP_MAP[key] && LASP_MAP[key].filter
if (!this.security_policies_token || !filter) {
// If we aren't applying something vetted by security policies we
// just return the new value.
return newVal
}
// Return the most secure if we have a filter to apply
return filter(currentVal, newVal)
}
/**
* The guts of the logic about how to deal with server-side configuration.
*
* @param {object} params A configuration dictionary.
* @param {string} key The particular configuration parameter to set.
*/
Config.prototype._fromServer = function _fromServer(params, key) {
switch (key) {
// handled by the connection
case 'messages':
break
// *sigh* Xzibit, etc.
case 'agent_config':
this.onConnect(params[key], true)
break
// if it's undefined or null, so be it
case 'agent_run_id':
this.run_id = params.agent_run_id
break
// handled by config.onConnect
case 'high_security':
break
// always accept these settings
case 'cross_process_id':
case 'encoding_key':
this._alwaysUpdateIfChanged(params, key)
if (this.cross_process_id && this.encoding_key) {
this.obfuscatedId =
hashes.obfuscateNameUsingKey(this.cross_process_id, this.encoding_key)
}
break
// always accept these settings
case 'account_id':
case 'application_id':
case 'collect_errors':
case 'collect_traces':
case 'primary_application_id':
case 'product_level':
case 'max_payload_size_in_bytes':
case 'sampling_target':
case 'sampling_target_period_in_seconds':
case 'trusted_account_ids':
case 'trusted_account_key':
this._alwaysUpdateIfChanged(params, key)
break
case 'collect_error_events':
if (params.collect_error_events === false) {
this._updateNestedIfChanged(
params,
this.error_collector,
key,
'capture_events'
)
}
break
// also accept these settings
case 'url_rules':
case 'metric_name_rules':
case 'transaction_name_rules':
case 'transaction_segment_terms':
this._emitIfSet(params, key)
break
case 'ssl':
if (!isTruthular(params.ssl)) {
logger.warn('SSL config key can no longer be disabled, not updating.')
}
break
// setting these can be disabled by ignore_server_configuration
case 'apdex_t':
case 'web_transactions_apdex':
case 'data_report_period':
this._updateIfChanged(params, key)
break
case 'ignored_params':
this._updateIfChanged(params, key)
this._canonicalize()
break
case 'collect_analytics_events':
// never enable from server-side
// but we allow the server to disable
if (params.collect_analytics_events === false)
this.transaction_events.enabled = false
break
case 'collect_custom_events':
// never enable from server-side
// but we allow the server to disable
if (params.collect_custom_events === false)
this.custom_insights_events.enabled = false
break
case 'allow_all_headers':
this._updateIfChanged(params, key)
this._canonicalize()
break
//
// Browser Monitoring
//
case 'browser_monitoring.loader':
this._updateNestedIfChangedRaw(
params,
this.browser_monitoring,
key,
'loader'
)
break
// these are used by browser_monitoring
// and the api.getRUMHeader() method
case 'js_agent_file':
case 'js_agent_loader_file':
case 'beacon':
case 'error_beacon':
case 'browser_key':
case 'js_agent_loader':
this._updateNestedIfChangedRaw(
params,
this.browser_monitoring,
key,
key
)
break
//
// Cross Application Tracer
//
case 'cross_application_tracer.enabled':
this._updateNestedIfChanged(
params,
this.cross_application_tracer,
key,
'enabled'
)
break
//
// Error Collector
//
case 'error_collector.enabled':
this._updateNestedIfChanged(
params,
this.error_collector,
'error_collector.enabled',
'enabled'
)
break
case 'error_collector.ignore_status_codes':
this._updateNestedIfChanged(
params,
this.error_collector,
'error_collector.ignore_status_codes',
'ignore_status_codes'
)
this._canonicalize()
break
case 'error_collector.capture_events':
this._updateNestedIfChanged(
params,
this.error_collector,
'error_collector.capture_events',
'capture_events'
)
break
case 'error_collector.max_event_samples_stored':
this._updateNestedIfChanged(
params,
this.error_collector,
'error_collector.max_event_samples_stored',
'max_event_samples_stored'
)
break
//
// Slow SQL
//
case 'slow_sql.enabled':
this._updateNestedIfChanged(params, this.slow_sql, key, 'enabled')
break
//
// Transaction Events
//
case 'transaction_events.enabled':
this._updateNestedIfChanged(
params,
this.transaction_events,
key,
'enabled'
)
break
case 'transaction_events.max_samples_stored':
this._updateNestedIfChanged(
params,
this.transaction_events,
key,
'max_samples_stored'
)
break
case 'transaction_events.max_samples_per_minute':
this._updateNestedIfChanged(
params,
this.transaction_events,
key,
'max_samples_per_minute'
)
break
//
// Transaction Tracer
//
case 'transaction_tracer.enabled':
this._updateNestedIfChanged(
params,
this.transaction_tracer,
'transaction_tracer.enabled',
'enabled'
)
break
case 'transaction_tracer.transaction_threshold':
this._updateNestedIfChanged(
params,
this.transaction_tracer,
'transaction_tracer.transaction_threshold',
'transaction_threshold'
)
break
// After 2015-02, the collector no longer supports the capture_params setting.
case 'capture_params':
break
// These settings aren't supported by the agent (yet).
case 'sampling_rate':
case 'episodes_file':
case 'episodes_url':
case 'rum.load_episodes_file':
// Ensure the most secure setting is applied to the settings below
// when enabling them.
case 'attributes.include_enabled':
case 'strip_exception_messages.enabled':
case 'transaction_tracer.record_sql':
this.logUnsupported(params, key)
break
// These settings are not allowed from the server.
case 'attributes.enabled':
case 'attributes.exclude':
case 'attributes.include':
case 'browser_monitoring.attributes.enabled':
case 'browser_monitoring.attributes.exclude':
case 'browser_monitoring.attributes.include':
case 'error_collector.attributes.enabled':
case 'error_collector.attributes.exclude':
case 'error_collector.attributes.include':
case 'transaction_events.attributes.enabled':
case 'transaction_events.attributes.exclude':
case 'transaction_events.attributes.include':
case 'transaction_tracer.attributes.enabled':
case 'transaction_tracer.attributes.exclude':
case 'transaction_tracer.attributes.include':
case 'serverless_mode.enabled':
break
default:
this.logUnknown(params, key)
}
}
/**
* Change a value sent by the collector if and only if it's different from the
* value we already have. Emit an event with the key name and the new value,
* and log that the value has changed.
*
* @param {object} json Config blob sent by collector.
* @param {string} key Value we're looking to set.
*/
Config.prototype._alwaysUpdateIfChanged = function _alwaysUpdateIfChanged(json, key) {
var value = json[key]
if (value != null && this[key] !== value) {
if (Array.isArray(value) && Array.isArray(this[key])) {
value.forEach(function pushIfNew(element) {
if (this[key].indexOf(element) === -1) this[key].push(element)
}, this)
} else {
this[key] = value
}
this.emit(key, value)
logger.debug('Configuration of %s was changed to %s by New Relic.', key, value)
}
}
/**
* Change a value sent by the collector if and only if it's different from the
* value we already have. Emit an event with the key name and the new value,
* and log that the value has changed. Parameter will be ignored if
* ignore_server_configuration is set.
*
* @param {object} json Config blob sent by collector.
* @param {string} key Value we're looking to set.
*/
Config.prototype._updateIfChanged = function _updateIfChanged(json, key) {
this._updateNestedIfChanged(json, this, key, key)
}
/**
* Some parameter values are nested, need a simple way to change them as well.
* Will merge local and remote if and only if both are arrays. Parameter will
* be ignored if ignore_server_configuration is set.
*
* @param {object} remote JSON sent from New Relic.
* @param {object} local A portion of this configuration object.
* @param {string} remoteKey The name sent by New Relic.
* @param {string} localKey The local name.
*/
Config.prototype._updateNestedIfChanged = _updateNestedIfChanged
function _updateNestedIfChanged(remote, local, remoteKey, localKey) {
if (this.ignore_server_configuration) return this.logDisabled(remote, remoteKey)
// if high-sec mode is enabled, we do not accept server changes to high-sec
if (this.high_security && HSM.HIGH_SECURITY_KEYS.indexOf(remoteKey) !== -1) {
return this.logDisabled(remote, remoteKey)
}
return this._updateNestedIfChangedRaw(remote, local, remoteKey, localKey)
}
Config.prototype._updateNestedIfChangedRaw = _updateNestedIfChangedRaw
function _updateNestedIfChangedRaw(remote, local, remoteKey, localKey) {
var value = remote[remoteKey]
if (value != null && local[localKey] !== value) {
if (Array.isArray(value) && Array.isArray(local[localKey])) {
value.forEach(function pushIfNew(element) {
if (local[localKey].indexOf(element) === -1) local[localKey].push(element)
})
} else {
local[localKey] = value
}
this.emit(remoteKey, value)
logger.debug('Configuration of %s was changed to %s by New Relic.', remoteKey, value)
}
}
/**
* Some parameter values are just to be passed on.
*
* @param {object} json Config blob sent by collector.
* @param {string} key Value we're looking to set.
*/
Config.prototype._emitIfSet = function _emitIfSet(json, key) {
var value = json[key]
if (value != null) this.emit(key, value)
}
/**
* The agent would normally do something with this parameter, but server-side
* configuration is disabled via ignore_server_configuration.
*
* @param {object} json Config blob sent by collector.
* @param {string} key Value the agent won't set.
*/
Config.prototype.logDisabled = function logDisabled(json, key) {
var value = json[key]
if (value != null) {
logger.debug(
'Server-side configuration of %s is currently disabled by local configuration. ' +
'(Server sent value of %s.)',
key,
value
)
}
}
/**
* Help support out by putting in the logs the fact that we don't currently
* support the provided configuration key, and including the sent value.
*
* @param {object} json Config blob sent by collector.
* @param {string} key Value the agent doesn't set.
*/
Config.prototype.logUnsupported = function logUnsupported(json, key) {
var flavor
if (this.ignore_server_configuration) {
flavor = "ignored"
} else {
flavor = "not supported by the Node.js agent"
}
var value = json[key]
if (value !== null && value !== undefined) {
logger.debug(
"Server-side configuration of %s is currently %s. (Server sent value of %s.)",
key,
flavor,
value
)
this.emit(key, value)
}
}
/**
* The agent knows nothing about this parameter.
*
* @param {object} json Config blob sent by collector.
* @param {string} key Value the agent knows nothing about.
*/
Config.prototype.logUnknown = function logUnknown(json, key) {
var value = json[key]
logger.debug(
"New Relic sent unknown configuration parameter %s with value %s.",
key,
value
)
}
/**
* Return the availability of async_hook for use by the agent.
*/
Config.prototype.checkAsyncHookStatus = function checkAsyncHookStatus() {
return (
this.feature_flag.await_support &&
(psemver.satisfies('>=8') || psemver.prerelease())
)
}
/**
* Gets the user set host display name. If not provided, it returns the default value.
*
* This function is written is this strange way becauase of the use of caching variables.
* I wanted to cache the DisplayHost, but if I attached the variable to the config object,
* it sends the extra variable to New Relic, which is not desired.
*
* @return {string} display host name
*/
Config.prototype.getDisplayHost = getDisplayHost
Config.prototype.clearDisplayHostCache = function clearDisplayHostCache() {
this.getDisplayHost = getDisplayHost
}
function getDisplayHost() {
var _displayHost
this.getDisplayHost = function getCachedDisplayHost() {
return _displayHost
}
if (this.process_host.display_name === '') {
_displayHost = this.getHostnameSafe()
return _displayHost
}
var stringBuffer = new Buffer(this.process_host.display_name, 'utf8')
var numBytes = stringBuffer.length
if (numBytes > 255) {
logger.warn('Custom host display name must be less than 255 bytes')
_displayHost = this.getHostnameSafe()
return _displayHost
}
_displayHost = this.process_host.display_name
return _displayHost
}
/**
* Gets the system's host name. If that fails, it just returns ipv4/6 based on the user's
* process_host.ipv_preferenece setting.
*
* This function is written is this strange way becauase of the use of caching variables.
* I wanted to cache the Hostname, but if I attached the variable to the config object,
* it sends the extra variable to New Relic, which is not desired.
*
* @return {string} host name
*/
Config.prototype.getHostnameSafe = getHostnameSafe
Config.prototype.clearHostnameCache = function clearHostnameCache() {
this.getHostnameSafe = getHostnameSafe
}
Config.prototype.getIPAddresses = function getIPAddresses() {
var addresses = Object.create(null)
var interfaces = os.networkInterfaces()
for (var interfaceKey in interfaces) {
if (interfaceKey.match(/^lo/)) continue
var interfaceDescriptions = interfaces[interfaceKey]
for (var i = 0; i < interfaceDescriptions.length; i++) {
var description = interfaceDescriptions[i]
var family = description.family.toLowerCase()
addresses[family] = description.address
}
}
return addresses
}
function getHostnameSafe() {
var _hostname
this.getHostnameSafe = function getCachedHostname() {
return _hostname
}
try {
_hostname = os.hostname()
return _hostname
} catch (e) {
var addresses = this.getIPAddresses()
if (this.process_host.ipv_preference === '6' && addresses.ipv6) {
_hostname = addresses.ipv6
} else if (addresses.ipv4) {
logger.info('Defaulting to ipv4 address for host name')
_hostname = addresses.ipv4
} else if (addresses.ipv6) {
logger.info('Defaulting to ipv6 address for host name')
_hostname = addresses.ipv6
} else {
logger.info('No hostname, ipv4, or ipv6 address found for machine')
_hostname = 'UNKNOWN_BOX'
}
return _hostname
}
}
/**
* Ensure that the apps names are always returned as a list.
*/
Config.prototype.applications = function applications() {
var apps = this.app_name
if (Array.isArray(apps) && apps.length > 0) {
return apps
}
if (apps && typeof apps === 'string') {
return [apps]
}
return []
}
/**
* Safely overwrite defaults with values passed to constructor.
*
* @param {object} external The configuration being loaded.
* @param {object} internal Whichever chunk of the config being overridden.
*/
Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrary) {
if (!external) return
if (!internal) internal = this
Object.keys(external).forEach(function overwrite(key) {
// if it's not in the defaults, it doesn't exist
if (!arbitrary && internal[key] === undefined) return
if (key === 'ssl' && !isTruthular(external.ssl)) {
logger.warn('SSL config key can no longer be disabled, not updating.')
return
}
if (key === 'ignored_params') {
warnDeprecated(key, 'attributes.exclude')
}
if (key === 'capture_params') {
warnDeprecated(key, 'attributes.enabled')
}
try {
var node = external[key]
} catch (err) {
logger.warn('Error thrown on access of user config for key: %s', key)
return
}
if (typeof node === 'object' && !Array.isArray(node)) {
// is top level and can have arbitrary keys
var isTop = internal === this && HAS_ARBITRARY_KEYS.indexOf(key) !== -1
this._fromPassed(node, internal[key], isTop)
} else {
internal[key] = node
}
}, this)
function warnDeprecated(key, replacement) {
logger.warn(
'Config key %s is deprecated, please use %s instead',
key,
replacement
)
}
}
/**
* Some values should be picked up only if they're not otherwise set, like
* the Windows / Azure application name. Don't set it if there's already
* a non-empty value set via the configuration file, and allow these
* values to be overwritten by environment variables. Just saves a step for
* PaaS users who don't want to have multiple settings for a single piece
* of configuration.
*/
Config.prototype._fromSpecial = function _fromSpecial() {
var name = this.app_name
if (name === null || name === undefined || name === '' ||
(Array.isArray(name) && name.length === 0)) {
var azureName = process.env[AZURE_APP_NAME]
if (azureName) this.app_name = azureName.split(',')
}
}
/**
* Iterate over all feature flags and check for the corresponding environment variable
* (of the form NEW_RELIC_FEATURE_FLAG_<feature flag name in upper case>).
*/
Config.prototype._featureFlagsFromEnv = function _featureFlagsFromEnv() {
const flags = Object.keys(feature_flag.prerelease).concat(feature_flag.released)
const config = this
flags.forEach(function checkFlag(flag) {
const envVal = process.env['NEW_RELIC_FEATURE_FLAG_' + flag.toUpperCase()]
if (envVal) {
config.feature_flag[flag] = isTruthular(envVal)
}
})
}
/**
* Recursively visit the nodes of the constant containing the mapping between
* environment variable names, overriding any configuration values that are
* found in the environment. Operates purely via side effects.
*
* @param object metadata The current level of the mapping object. Should never
* need to set this yourself.
* @param object data The current level of the configuration object. Should
* never need to set this yourself.
*/
Config.prototype._fromEnvironment = function _fromEnvironment(metadata, data) {
if (!metadata) metadata = ENV.ENV_MAPPING
if (!data) data = this
Object.keys(metadata).forEach(function applyEnvDefault(value) {
// if it's not in the config, it doesn't exist
if (data[value] === undefined) {
return
}
var node = metadata[value]
if (typeof node === 'string') {
var setting = process.env[node]
if (setting) {
if (ENV.LIST_VARS.has(node)) {
data[value] = setting.split(',').map(function trimVal(k) {
return k.trim()
})
} else if (ENV.OBJECT_LIST_VARS.has(node)) {
data[value] = fromObjectList(setting)
} else if (ENV.BOOLEAN_VARS.has(node)) {
if (value === 'ssl' && !isTruthular(setting)) {
logger.warn('SSL config key can no longer be disabled, not updating.')
return
}
data[value] = isTruthular(setting)
} else if (ENV.FLOAT_VARS.has(node)) {
data[value] = parseFloat(setting, 10)
} else if (ENV.INT_VARS.has(node)) {
data[value] = parseInt(setting, 10)
} else {
data[value] = setting
}
}
} else {
// don't crash if the mapping has config keys the current config doesn't.
if (!data[value]) data[value] = Object.create(null)
this._fromEnvironment(node, data[value])
}
}, this)
}
/**
* Enforces config rules specific to running in serverless_mode:
* - disables cross_application_tracer.enabled if set
* - verifies data specific to running DT is defined either in config file of env vars
*/
Config.prototype._enforceServerlessConfig = function _enforceServerlessConfig() {
const DT_KEYS = ['trusted_account_key', 'application_id', 'account_id']
if (this.serverless_mode.enabled) {
// Explicitly disable old CAT in serverless_mode
if (this.cross_application_tracer.enabled) {
this.cross_application_tracer.enabled = false
logger.info('Cross application tracing is explicitly disabled in serverless_mode.')
}
// If DT and serverless_mode are set, enforce that DT config values are set.
const allSet = DT_KEYS.every((key) => {
return this[key]
})
if (this.distributed_tracing.enabled && !allSet) {
throw new Error(
'Using distributed tracing in serverless mode requires the following ' +
'config values be defined, either in your newrelic.js file ' +
'or via environment variables: ' + DT_KEYS.join(', ') + '.'
)
}
} else {
// Don't allow DT config settings to be set if serverless_mode is disabled
DT_KEYS.forEach((key) => {
if (this[key]) {
logger.warn(
key + ' was configured locally without enabling serverless_mode. ' +
'This local value will be ignored and set by the New Relic servers.'
)
this[key] = null
}
})
}
}
/**
* Depending on how the status codes are set, they could be strings, which
* makes strict equality testing / indexOf fail. To keep things cheap, parse
* them once, after configuration has finished loading. Other one-off shims
* based on special properties of configuration values should go here as well.
*/
Config.prototype._canonicalize = function _canonicalize() {
var statusCodes = this.error_collector && this.error_collector.ignore_status_codes
if (statusCodes) {
this.error_collector.ignore_status_codes = _parseCodes(statusCodes)
}
var logAliases = {
verbose: 'trace',
debugging: 'debug',
warning: 'warn',
err: 'error'
}
var level = this.logging.level
this.logging.level = logAliases[level] || level
if (this.host === '') {
var region = parseKey(this.license_key)
if (region) {
this.host = 'collector.' + region + '.nr-data.net'
} else {
this.host = 'collector.newrelic.com'
}
}
// If new props are explicitly set (ie, not the default), use those
this.attributes.exclude = this.attributes.exclude.length
? this.attributes.exclude
: this.ignored_params
this.attributes.enabled = this.attributes.enabled
? this.attributes.enabled
: this.capture_params
this.api.custom_attributes_enabled = !this.api.custom_attributes_enabled
? this.api.custom_attributes_enabled
: this.api.custom_parameters_enabled
this.serverless_mode.enabled = this.serverless_mode.enabled
&& this.feature_flag.serverless_mode
}
function _parseCodes(codes) {
// range does not support negative values
function parseRange(range, parsed) {
var split = range.split('-')
if (split.length !== 2) {
logger.warn('Failed to parse range %s', range)
return parsed
}
if (split[0] === '') { // catch negative code. ex. -7
return parsed.push(parseInt(range, 10))
}
var lower = parseInt(split[0], 10)
var upper = parseInt(split[1], 10)
if (Number.isNaN(lower) || Number.isNaN(upper)) {
logger.warn('Range must contain two numbers %s', range)
return parsed
}
if (lower > upper) {
logger.warn('Range must start with lower bound %s', range)
} else if (lower < 0 || upper > 1000) {
logger.warn('Range must be between 0 and 1000 %s', range)
} else { // success
for (var i = lower; i <= upper; i++) {
parsed.push(i)
}
}
return parsed
}
var parsedCodes = []
for (var i = 0; i < codes.length; i++) {
var code = codes[i]
var parsedCode
if (typeof code === 'string' && code.indexOf('-') !== -1) {
parseRange(code, parsedCodes)
} else {
parsedCode = parseInt(code, 10)
if (!Number.isNaN(parsedCode)) {
parsedCodes.push(parsedCode)
} else {
logger.warn('Failed to parse status code %s', code)
}
}
}
return parsedCodes
}
/**
* This goes through the settings that high security mode needs and coerces
* them to be correct.
*/
Config.prototype._applyHighSecurity = function _applyHighSecurity() {
var config = this
checkNode('', this, HSM.HIGH_SECURITY_SETTINGS)
function checkNode(base, target, settings) {
Object.keys(settings).forEach(checkKey.bind(null, base, target, settings))
}
function checkKey(base, target, settings, key) {
var hsValue = settings[key]
if (hsValue && typeof hsValue === 'object' && !(hsValue instanceof Array)) {
if (typeof target[key] !== 'object') {
logger.warn(
'High Security Mode: %s should be an object, found %s',
key,
target[key]
)
target[key] = Object.create(null)
}
return checkNode(base + key + '.', target[key], hsValue)
}
if (target[key] !== hsValue) {
logger.warn(
'High Security Mode: %s was set to %s, coercing to %s',
key,
target[key],
hsValue
)
target[key] = hsValue
config.emit(base + key, hsValue)
}
}
}
/**
* Checks policies received from preconnect against those expected
* by the agent, if LASP-enabled. Responds with an error to shut down
* the agent if necessary.
*
* @param {Agent} agent
* @param {object} policies
*
* @returns {CollectorResponse} The result of the processing, with the known
* policies as the response payload.
*/
Config.prototype.applyLasp = function applyLasp(agent, policies) {
var config = this
var keys = Object.keys(policies)
if (!config.security_policies_token) {
if (keys.length) {
logger.error(
'The agent received one or more unexpected security policies and will shut down.'
)
return CollectorResponse.fatal(null)
}
return CollectorResponse.success(null)
}
var missingLASP = []
var missingRequired = []
var finalPolicies = keys.reduce(function applyPolicy(obj, name) {
var policy = policies[name]
var localMapping = LASP_MAP[name]
if (!localMapping) {
if (!policy.required) {
// policy is not implemented in agent -- don't send to connect
return obj
}
// policy is required but does not exist in agent -- fail
missingRequired.push(name)
} else {
var splitConfigName = localMapping.path.split('.')
var settingBlock = config[splitConfigName[0]]
// pull out the configuration subsection that the option lives in
for (var i = 1; i < splitConfigName.length - 1; ++i) {
settingBlock = settingBlock[splitConfigName[i]]
}
var valueName = splitConfigName[splitConfigName.length - 1]
var localVal = settingBlock[valueName]
var policyValues = localMapping.allowedValues
var policyValue = policyValues[policy.enabled ? 1 : 0]
// get the most secure setting between local config and the policy
var finalValue = settingBlock[valueName] = config._getMostSecure(
name,
localVal,
policyValue
)
policy.enabled = policyValues.indexOf(finalValue) === 1
obj[name] = policy
if (finalValue !== localVal) {
// finalValue is more secure than original local val,
// so drop corresponding data
localMapping.clearData(agent)
}
}
return obj
}, Object.create(null))
Object.keys(LASP_MAP).forEach(function checkPolicy(name) {
if (!policies[name]) {
// agent is expecting a policy that was not sent from server -- fail
missingLASP.push(name)
}
})
let fatalMessage = null
if (missingLASP.length) {
fatalMessage =
'The agent did not receive one or more security policies that it ' +
'expected and will shut down: ' + missingLASP.join(', ') + '.'
} else if (missingRequired.length) {
fatalMessage =
'The agent received one or more required security policies that it ' +
'does not recognize and will shut down: ' + missingRequired.join(', ') +
'. Please check if a newer agent version supports these policies ' +
'or contact support.'
}
if (fatalMessage) {
logger.error(fatalMessage)
return CollectorResponse.fatal(null)
}
return CollectorResponse.success(finalPolicies)
}
Config.prototype.validateFlags = function validateFlags() {
Object.keys(this.feature_flag).forEach(function forEachFlag(key) {
if (feature_flag.released.indexOf(key) > -1) {
logger.warn('Feature flag %s has been released', key)
}
if (feature_flag.unreleased.indexOf(key) > -1) {
logger.warn('Feature flag %s has been deprecated', key)
}
})
}
/**
* Get a JSONifiable object containing all settings we want to report to the
* collector and store in the environment_values table.
*
* @return Object containing simple key-value pairs of settings
*/
Config.prototype.publicSettings = function publicSettings() {
var settings = Object.create(null)
for (var key in this) {
if (this.hasOwnProperty(key)) {
if (HSM.REDACT_BEFORE_SEND.has(key)) {
settings[key] = '****'
} else if (!HSM.REMOVE_BEFORE_SEND.has(key)) {
settings[key] = this[key]
}
}
}
// Agent-side setting is 'enable', but collector-side setting is
// 'auto_instrument'. Send both values up.
settings.browser_monitoring.auto_instrument = settings.browser_monitoring.enable
try {
settings = stringify(settings)
// Remove simple circular references
return flatten(Object.create(null), '', JSON.parse(settings))
} catch (err) {
logger.error(err, 'Unable to stringify settings object')
}
}
/**
* Create a configuration, either from a configuration file or the node
* process's environment.
*
* For configuration via file, check these directories, in order, for a
* file named 'newrelic.js':
*
* 1. The process's current working directory at startup.
* 2. The same directory as the process's main module (i.e. the filename
* passed to node on the command line).
* 3. The directory pointed to by the environment variable NEW_RELIC_HOME.
* 4. The current process's HOME directory.
* 5. If this module is installed as a dependency, the directory above the
* node_modules folder in which newrelic is installed.
*
* For configration via environment (useful on Joyent, Azure, Heroku, or
* other PaaS offerings), set NEW_RELIC_NO_CONFIG_FILE to something truthy
* and read README.md for details on what configuration variables are
* necessary, as well as a complete enumeration of the other available
* variables.
*
* @param {object} config Optional configuration to be used in place of a
* config file.
*/
function initialize(config) {
/* When the logger is required here, it bootstraps itself and then
* injects itself into this module's closure via setLogger on the
* instance of the logger it creates.
*/
logger = require('../logger')
if (config) return new Config(config)
if (isTruthular(process.env.NEW_RELIC_NO_CONFIG_FILE)) {
config = new Config(Object.create(null))
if (config.newrelic_home) delete config.newrelic_home
return config
}
var filepath = _findConfigFile()
if (!filepath) {
_noConfigFile()
return null
}
var userConf
try {
userConf = require(filepath).config
} catch (error) {
logger.error(error)
throw new Error(
"Unable to read configuration file " + filepath + ". A default\n" +
"configuration file can be copied from " + DEFAULT_CONFIG_PATH + "\n" +
"and renamed to 'newrelic.js' in the directory from which you'll be starting\n" +
"your application."
)
}
config = new Config(userConf)
config.config_file_path = filepath
logger.debug("Using configuration file %s.", filepath)
config.validateFlags()
return config
}
function _noConfigFile() {
const mainpath = path.resolve(path.join(process.cwd(), DEFAULT_FILENAME))
// If agent was loaded with -r flag, default to the path of the file being executed
const mainModule = process.mainModule && process.mainModule.filename || process.argv[1]
const altpath = path.resolve(
path.dirname(mainModule),
DEFAULT_FILENAME
)
var locations
if (mainpath !== altpath) {
locations = mainpath + " or\n" + altpath
} else {
locations = mainpath
}
/* eslint-disable no-console */
console.error(
"Unable to find New Relic module configuration. A default\n" +
"configuration file can be copied from " + DEFAULT_CONFIG_PATH + "\n" +
"and put at " + locations + ". If you are not using file based config\n" +
"please set the environment variable NEW_RELIC_NO_CONFIG_FILE=true"
)
/* eslint-enable no-console */
}
/**
* This function honors the singleton nature of this module while allowing
* consumers to just request an instance without having to worry if one was
* already created.
*/
function getOrCreateInstance() {
if (_configInstance === null) {
_configInstance = initialize()
}
return _configInstance
}
/**
* Preserve the legacy initializer, but also allow consumers to manage their
* own configuration if they choose.
*/
Config.initialize = initialize
Config.getOrCreateInstance = getOrCreateInstance
module.exports = Config