UNPKG

newrelic

Version:
1,580 lines (1,413 loc) 59.5 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' const AttributeFilter = require('./attribute-filter') const CollectorResponse = require('../collector/response') const copy = require('../util/copy') const { config: defaultConfig, definition, setNestedKey } = require('./default') const EventEmitter = require('events').EventEmitter const featureFlag = 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 stringify = require('json-stringify-safe') const util = require('util') const MergeServerConfig = require('./merge-server-config') const harvestConfigValidator = require('./harvest-config-validator') const mergeServerConfig = new MergeServerConfig() const { boolean: isTruthular } = require('./formatters') const configDefinition = definition() const parseLabels = require('../util/label-parser') /** * CONSTANTS -- we gotta lotta 'em */ const AZURE_APP_NAME = 'APP_POOL_ID' const DEFAULT_MAX_PAYLOAD_SIZE_IN_BYTES = 1_000_000 const BASE_CONFIG_PATH = require.resolve('../../newrelic') const HAS_ARBITRARY_KEYS = new Set(['ignore_messages', 'expected_messages', 'labels']) const LASP_MAP = require('./lasp').LASP_MAP const HSM = require('./hsm') const REMOVE_BEFORE_SEND = new Set(['attributeFilter']) const SSL_WARNING = 'SSL config key can no longer be disabled, not updating.' const SERVERLESS_DT_KEYS = ['account_id', 'primary_application_id', 'trusted_account_key'] const exists = fs.existsSync let logger = null // Lazy-loaded in `initialize`. let _configInstance = null const getConfigFileNames = () => [process.env.NEW_RELIC_CONFIG_FILENAME, 'newrelic.js', 'newrelic.cjs', 'newrelic.mjs'].filter( Boolean ) const getConfigFileLocations = () => [ process.env.NEW_RELIC_HOME, process.cwd(), process.env.HOME, path.join(__dirname, '../../../..'), // above node_modules // the REPL has no main module process.mainModule && process.mainModule.filename ? path.dirname(process.mainModule.filename) : undefined ].filter(Boolean) function _findConfigFile() { const configFileCandidates = getConfigFileLocations().reduce((files, configPath) => { const configFiles = getConfigFileNames().map((filename) => path.join(path.resolve(configPath), filename) ) return files.concat(configFiles) }, []) return configFileCandidates.find(exists) } /** * Indicates if the agent should be enabled or disabled based upon the * processed configuration. * * @event Config#agent_enabled * @param {boolean} indication */ /** * Indicates that the configuration has changed due to some external factor, * e.g. the configuration from the New Relic server has overridden some * local setting. * * @event Config#change * @param {Config} currentConfig */ /** * Parses and manages the agent configuration. * * @param {object} config Configuration object as read from a `newrelic.js` * file. This is used as the baseline, which is the overridden by environment * variables and feature flags. * * @class */ function Config(config) { EventEmitter.call(this) // 1. start by cloning the defaults Object.assign(this, defaultConfig()) // 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(featureFlag.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 // this value is arbitrary this.max_trace_segments = 900 this.entity_guid = null // feature level of this account this.product_level = 0 // product-level related this.collect_traces = true this.collect_errors = true this.collect_span_events = true this.browser_monitoring.loader = 'rum' this.browser_monitoring.loader_version = '' // Settings to play nice with DLPs (see NODE-1044). 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 // TODO: this may belong in canonicalize. if (!this.event_harvest_config) { this.event_harvest_config = { report_period_ms: 60000, harvest_limits: { analytic_event_data: this.transaction_events.max_samples_stored, custom_event_data: this.custom_insights_events.max_samples_stored, error_event_data: this.error_collector.max_event_samples_stored, span_event_data: this.span_events.max_samples_stored, log_event_data: this.application_logging.forwarding.max_samples_stored } } } // 7. serverless_mode specific settings this._enforceServerless(config) // 8. apply high security overrides if (this.high_security) { 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() } // 9. Set instance attribute filter using updated context this.attributeFilter = new AttributeFilter(this) // 10. Setup labels for both `collector/facts` and application logging this.parsedLabels = parseLabels(this.labels, logger) this.loggingLabels = this._setApplicationLoggingLabels() } util.inherits(Config, EventEmitter) /** * Compares the labels list to the application logging excluded label list and removes any labels that need to be excluded. * Then prefixing each label with "tags." * * assigns labels to `config.loggingLabels` */ Config.prototype._setApplicationLoggingLabels = function setApplicationLoggingLabels() { if (this.application_logging.forwarding.labels.enabled === false) { return } this.application_logging.forwarding.labels.exclude = this.application_logging.forwarding.labels.exclude.map((k) => k.toLowerCase()) return this.parsedLabels.reduce((filteredLabels, label) => { if ( !this.application_logging.forwarding.labels.exclude.includes(label.label_type.toLowerCase()) ) { filteredLabels[`tags.${label.label_type}`] = label.label_value } return filteredLabels }, {}) } /** * 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 } /** * helper object for merging server side values */ Config.prototype.mergeServerConfig = mergeServerConfig /** * Accept any configuration passed back from the server. Will log all * recognized, unsupported, and unknown parameters. * * @param {object} json The config blob sent by New Relic. * @param {boolean} recursion flag indicating coming from server side config * * @fires Config#agent_enabled When there is a conflict between the local * setting of high security mode and what the New Relic server has returned. * @fires Config#change */ Config.prototype.onConnect = function onConnect(json, recursion) { json = json || Object.create(null) if (this.high_security && recursion !== true && !json.high_security) { 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._warnDeprecations() this.emit('change', this) } Config.prototype._getMostSecure = function getMostSecure(key, currentVal, newVal) { const 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) } /** * Helper that checks if value from server is false * then updates the corresponding configuration enabled flag. * We never allow server-side config to enable a feature but you can disable * * @param {*} serverValue value from server * @param {string} key within configuration to disable its enabled flag */ Config.prototype._disableOption = function _disableOption(serverValue, key) { if (serverValue === false) { this[key].enabled = false } } /** * Updates harvest_limits for event_harvest_config if they are valid values * * @param {object} serverConfig harvest config from server * @param {string} key value from server side config that stores the event harvest config */ Config.prototype._updateHarvestConfig = function _updateHarvestConfig(serverConfig, key) { const val = serverConfig[key] const isValidConfig = harvestConfigValidator.isValidHarvestConfig(val) if (!isValidConfig) { this.emit(key, null) return } logger.info('Valid event_harvest_config received. Updating harvest cycles.', val) const limits = Object.keys(val.harvest_limits).reduce((acc, k) => { const v = val.harvest_limits[k] if (harvestConfigValidator.isValidHarvestValue(v)) { acc[k] = v } else { logger.info(`Omitting limit for ${k} due to invalid value ${v}`) } return acc }, {}) val.harvest_limits = limits this[key] = val this.emit(key, val) } /** * 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. * * @fires Config#change */ Config.prototype._fromServer = function _fromServer(params, key) { /* eslint-disable-next-line sonarjs/max-switch-cases */ switch (key) { // handled by the connection case 'messages': break // per the spec this is the key where all server side configuration values will come from. case 'agent_config': if (this.ignore_server_configuration) { this.logDisabled(params, key) } else { 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 // if it's undefined or null, so be it case 'request_headers_map': this.request_headers_map = params.request_headers_map break // handled by config.onConnect case 'high_security': break // interpret AI Monitoring account setting case 'collect_ai': this._disableOption(params.collect_ai, 'ai_monitoring') this.emit('change', this) 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_WARNING) } break case 'apdex_t': case 'web_transactions_apdex': this._updateIfChanged(params, key) break case 'event_harvest_config': this._updateHarvestConfig(params, key) break case 'collect_analytics_events': this._disableOption(params.collect_analytics_events, 'transaction_events') break case 'collect_custom_events': this._disableOption(params.collect_custom_events, 'custom_insights_events') break case 'collect_span_events': this._disableOption(params.collect_span_events, 'span_events') 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._validateThenUpdateStatusCodes( params, this.error_collector, 'error_collector.ignore_status_codes', 'ignore_status_codes' ) this._canonicalize() break case 'error_collector.expected_status_codes': this._validateThenUpdateStatusCodes( params, this.error_collector, 'error_collector.expected_status_codes', 'expected_status_codes' ) this._canonicalize() break case 'error_collector.ignore_classes': this._validateThenUpdateErrorClasses( params, this.error_collector, 'error_collector.ignore_classes', 'ignore_classes' ) break case 'error_collector.expected_classes': this._validateThenUpdateErrorClasses( params, this.error_collector, 'error_collector.expected_classes', 'expected_classes' ) break case 'error_collector.ignore_messages': this._validateThenUpdateErrorMessages( params, this.error_collector, 'error_collector.ignore_messages', 'ignore_messages' ) break case 'error_collector.expected_messages': this._validateThenUpdateErrorMessages( params, this.error_collector, 'error_collector.expected_messages', 'expected_messages' ) 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 // // 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 // Entity GUID case 'entity_guid': this.entity_guid = params[key] 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': // eslint-disable-line no-fallthrough case 'strip_exception_messages.enabled': case 'transaction_tracer.record_sql': this.logUnsupported(params, key) break // DT span event harvest config limits case 'span_event_harvest_config': this.span_event_harvest_config = { ...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_events.max_samples_stored': 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) { const 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. * * @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) } /** * Expected and Ignored status code configuration values should look like this * * [500,'501','503-507'] * * If the server side config is not in this format, it might put the agent * in a world of hurt. So, before we pass everything on to * _updateNestedIfChanged, we'll do some validation. * * @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._validateThenUpdateStatusCodes = _validateThenUpdateStatusCodes function _validateThenUpdateStatusCodes(remote, local, remoteKey, localKey) { const valueToTest = remote[remoteKey] if (!Array.isArray(valueToTest)) { logger.warn( 'Saw SSC (ignore|expect)_status_codes that is not an array, will not merge: %s', valueToTest ) return } let valid = true valueToTest.forEach(function validateArray(thingToTest) { if (!(typeof thingToTest === 'string' || typeof thingToTest === 'number')) { logger.warn( 'Saw SSC (ignore|expect)_status_code that is not a number or string,' + 'will not merge: %s', thingToTest ) valid = false } }) if (!valid) { return } return this._updateNestedIfChanged(remote, local, remoteKey, localKey) } /** * Expected and Ignored classes configuration values should look like this * * ['Error','Again'] * * If the server side config is not in this format, it might put the agent * in a world of hurt. So, before we pass everything on to * _updateNestedIfChanged, we'll do some validation. * * @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._validateThenUpdateErrorClasses = _validateThenUpdateErrorClasses function _validateThenUpdateErrorClasses(remote, local, remoteKey, localKey) { const valueToTest = remote[remoteKey] if (!Array.isArray(valueToTest)) { logger.warn( 'Saw SSC (ignore|expect)_classes that is not an array, will not merge: %s', valueToTest ) return } let valid = true Object.keys(valueToTest).forEach(function validateArray(key) { const thingToTest = valueToTest[key] if (typeof thingToTest !== 'string') { logger.warn( 'Saw SSC (ignore|expect)_class that is not a string, will not merge: %s', thingToTest ) valid = false } }) if (!valid) { return } return this._updateNestedIfChanged(remote, local, remoteKey, localKey) } /** * Expected and Ignore messages configuration values should look like this * * {'ErrorType':['Error Message']} * * If the server side config is not in this format, it might put the agent * in a world of hurt. So, before we pass everything on to * _updateNestedIfChanged, we'll do some validation. * * @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._validateThenUpdateErrorMessages = _validateThenUpdateErrorMessages function _validateThenUpdateErrorMessages(remote, local, remoteKey, localKey) { const valueToTest = remote[remoteKey] if (Array.isArray(valueToTest)) { logger.warn('Saw SSC (ignore|expect)_message that is an Array, will not merge: %s', valueToTest) return } if (!valueToTest) { logger.warn('SSC ignore|expect_message is null or undefined, will not merge') return } if (typeof valueToTest !== 'object') { logger.warn( 'Saw SSC (ignore|expect)_message that is primitive/scaler, will not merge: %s', valueToTest ) return } let valid = true Object.keys(valueToTest).forEach(function validateArray(key) { const arrayToTest = valueToTest[key] if (!Array.isArray(arrayToTest)) { logger.warn('Saw SSC message array that is not an array, will not merge: %s', arrayToTest) valid = false } }) if (!valid) { return } return this._updateNestedIfChanged(remote, local, remoteKey, localKey) } /** * 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. * * @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 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) { return this.mergeServerConfig.updateNestedIfChanged( this, remote, local, remoteKey, localKey, logger ) } /** * 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) { const 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 local settings or HSM. * * @param {object} json Config blob sent by collector. * @param {string} key Value the agent won't set. */ Config.prototype.logDisabled = function logDisabled(json, key) { const 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) { const value = json[key] if (value !== null && value !== undefined) { logger.debug( 'Server-side configuration of %s is currently not supported by the ' + 'Node.js agent. (Server sent value of %s.)', key, 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) { const value = json[key] logger.debug('New Relic sent unknown configuration parameter %s with value %s.', key, value) } /** * Gets the user set host display name. If not provided, it returns the default value. * * This function is written in this strange way because 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. * * @returns {string} display host name */ Config.prototype.getDisplayHost = getDisplayHost Config.prototype.clearDisplayHostCache = function clearDisplayHostCache() { this.getDisplayHost = getDisplayHost } function getDisplayHost() { let _displayHost this.getDisplayHost = function getCachedDisplayHost() { return _displayHost } if (this.process_host.display_name === '') { _displayHost = this.getHostnameSafe() return _displayHost } const stringBuffer = Buffer.from(this.process_host.display_name, 'utf8') const 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_preference setting. * * This function is written is this strange way because 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. * * @returns {string} host name */ Config.prototype.getHostnameSafe = getHostnameSafe Config.prototype.clearHostnameCache = function clearHostnameCache() { this.getHostnameSafe = getHostnameSafe } Config.prototype.getIPAddresses = function getIPAddresses() { const addresses = Object.create(null) const interfaces = os.networkInterfaces() for (const interfaceKey in interfaces) { if (interfaceKey.match(/^lo/)) { continue } const interfaceDescriptions = interfaces[interfaceKey] for (let i = 0; i < interfaceDescriptions.length; i++) { const description = interfaceDescriptions[i] const family = description.family.toLowerCase() addresses[family] = description.address } } return addresses } /** * Gets the system's host name. If that fails, it just returns ipv4/6 * based on the user's process_host.ipv_preference setting. * @param {string} gcpId GCP metadata ID * @returns {string} host name */ function getHostnameSafe(gcpId = null) { let _hostname const config = this this.getHostnameSafe = function getCachedHostname() { return _hostname } try { // Check for any special hostname scenarios. // If not applicable, use the os.hostname() if (config.heroku.use_dyno_names && process.env.DYNO) { _hostname = process.env.DYNO || os.hostname() } else if (config.utilization.gcp_use_instance_as_host && process.env.K_SERVICE && gcpId) { _hostname = gcpId } else { _hostname = os.hostname() } return _hostname } catch { const 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. * * @returns {Array} list of applications */ Config.prototype.applications = function applications() { const 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. * @param {boolean} arbitrary flag indicating if it is in the HAS_ARBITRARY_KEYS set */ 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 && !(key in internal)) { return } if (key === 'ssl' && !isTruthular(external.ssl)) { logger.warn(SSL_WARNING) return } let node = null try { node = external[key] } catch { logger.warn('Error thrown on access of user config for key: %s', key) return } if (typeof node === 'object' && !Array.isArray(node) && !(node instanceof RegExp)) { // is top level and can have arbitrary keys const allowArbitrary = internal === this || HAS_ARBITRARY_KEYS.has(key) this._fromPassed(node, internal[key], allowArbitrary) } else { internal[key] = node } }, this) } /** * 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() { const name = this.app_name if ( name === null || name === undefined || name === '' || (Array.isArray(name) && name.length === 0) ) { const 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(featureFlag.prerelease).concat(featureFlag.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) } }) } /** * Creates an env var mapper from the configuration path value * * @param {string} key of configuration value * @param {Array} paths list of leaf nodes leading to configuration value * @returns {string} formatted env var name */ function deriveEnvVar(key, paths) { let configPath = paths.join('_') configPath = configPath ? `${configPath}_` : configPath return `NEW_RELIC_${configPath.toUpperCase()}${key.toUpperCase()}` } /** * Assigns the value of an env var to its corresponding configuration path * * @param {object} params object passed to fn * @param {object} params.config agent config * @param {string} params.key key to assign value * @param {string} params.envVar name of env var * @param {Function} params.formatter function to coerce env var as they are all strings * @param {Array} params.paths list of leaf nodes leading to the configuration value */ function setFromEnv({ config, key, envVar, formatter, paths }) { const setting = process.env[envVar] if (setting) { const formattedSetting = formatter ? formatter(setting, logger) : setting // `setting` _should_ be a string when reading from the actual process // environment. But we have tests that construct an environment with // non-string values. So we need to convert them before accessing. const prefix = setting.toString().at(0) const suffix = setting.toString().at(-1) const redacted = prefix + '*'.repeat(setting.length) + suffix logger.trace({ env: { [envVar]: redacted } }, 'setting value from environment variable') setNestedKey(config, [...paths, key], formattedSetting) } } /** * Recursively visit the nodes of the config definition and look for environment variable names, overriding any configuration values that are found. * * @param {object} [config] The current level of the configuration object. * @param {object} [data] The current level of the config definition object. * @param {Array} [paths] keeps track of the nested path to properly derive the env var * @param {number} [objectKeys] indicator of how many keys exist in current node to know when to remove current node after all keys are processed */ Config.prototype._fromEnvironment = function _fromEnvironment( config = this, data = configDefinition, paths = [], objectKeys = 1 ) { let keysSeen = 0 for (const [key, value] of Object.entries(data)) { const type = typeof value keysSeen += 1 if (type !== 'string' && type !== 'object') { continue } if (type === 'string') { const envVar = deriveEnvVar(key, paths) setFromEnv({ config, key, envVar, paths }) continue } if (Object.prototype.hasOwnProperty.call(value, 'env') === true) { const envVar = value.env setFromEnv({ config, key, paths, envVar, formatter: value.formatter }) continue } if (Object.prototype.hasOwnProperty.call(value, 'default') === true) { const envVar = deriveEnvVar(key, paths) setFromEnv({ config, key, paths, envVar, formatter: value.formatter }) continue } paths.push(key) const { length } = Object.keys(value) this._fromEnvironment(config, value, paths, length) } // we have traversed every key in current object leaf node, remove wrapping key // to properly derive env vars of future leaf nodes if (keysSeen === objectKeys) { paths.pop() } } /** * Disables `logging.enabled` if not set in configuration file or environment variable. * * @param {*} inputConfig configuration passed to the Config constructor * @returns {void} */ Config.prototype._serverlessLogging = function _serverlessLogging(inputConfig) { const inputEnabled = inputConfig?.logging?.enabled const envEnabled = process.env.NEW_RELIC_LOG_ENABLED if (inputEnabled === undefined && envEnabled === undefined) { this.logging.enabled = false logger.info( 'Logging is disabled by default when serverless_mode is enabled. ' + 'If desired, enable logging via config file or environment variable and ' + 'set filepath to a valid path for current environment, stdout or stderr.' ) } } /** * Returns true if native-metrics has been manually enabled via configuration * file or environment variable * * @param {*} inputConfig configuration pass to the Config constructor * @returns {void} */ Config.prototype._serverlessNativeMetrics = function _serverlessNativeMetrics(inputConfig) { const inputEnabled = inputConfig?.plugins?.native_metrics?.enabled const envEnabled = process.env.NEW_RELIC_NATIVE_METRICS_ENABLED if ( (inputEnabled !== undefined || envEnabled !== undefined) && this.plugins.native_metrics.enabled ) { logger.info( 'Enabling the native-metrics module when in serverless mode may greatly ' + 'increase cold-start times. Given the limited benefit of the VM metrics' + 'and general lack of control in a serverless environment, we do not ' + 'recommend this trade-off.' ) } else { this.plugins.native_metrics.enabled = false logger.info( 'The native-metrics module is disabled by default when serverless_mode ' + 'is enabled. If desired, enable the native-metrics module via config file ' + 'or environment variable.' ) } } /** * Application name is not currently leveraged by our Lambda product (March 2021). * Defaulting the name removes burden on customers to set while avoiding * breaking should it be used in the future. * * @returns {void} */ Config.prototype._serverlessAppName = function _serverlessAppName() { if (!this.app_name || this.app_name.length === 0) { const namingSource = process.env.AWS_LAMBDA_FUNCTION_NAME ? 'process.env.AWS_LAMBDA_FUNCTION_NAME' : 'DEFAULT' const name = process.env.AWS_LAMBDA_FUNCTION_NAME || 'Serverless Application' this.app_name = [name] logger.info("Auto-naming serverless application to ['%s'] from: %s", name, namingSource) } } /** * Disables CAT in serverless mode */ Config.prototype._serverlessCAT = function _serverlessCAT() { if (this.cross_application_tracer.enabled) { this.cross_application_tracer.enabled = false logger.info('Cross application tracing is explicitly disabled in serverless_mode.') } } Config.prototype._serverlessInfiniteTracing = function _serverlessInfiniteTracing() { if (this.infinite_tracing.trace_observer.host) { this.infinite_tracing.trace_observer.host = '' this.infinite_tracing.trace_observer.port = '' logger.info('Infinite tracing is explicitly disabled in serverless_mode.') } } /** * Disables DT if account_id is not set. * Otherwise it will set trusted_account_key and primary_application_id accordingly. * * @returns {void} */ Config.prototype._serverlessDT = function _serverlessDT() { if (!this.account_id) { if (this.distributed_tracing.enabled) { logger.warn( 'Using distributed tracing in serverless mode requires account_id be ' + 'defined, either in your newrelic.js file or via environment variables. ' + 'Disabling distributed tracing.' ) this.distributed_tracing.enabled = false } } else { // default trusted_account_key to account_id this.trusted_account_key = this.trusted_account_key || this.account_id // Not required in serverless mode but must default to Unknown to function. this.primary_application_id = this.primary_application_id || 'Unknown' } } /** * In serverless mode we allow defer auth to the downstream serverless entities. * This means we set account_id, primary_application_id, and trusted_account_key in configuration. * This function sets all those to null because this.serverless_mode.enabled is falsey. * * @returns {void} */ Config.prototype._preventServerlessDT = function _preventServerlessDT() { // Don't allow DT config settings to be set if serverless_mode is disabled SERVERLESS_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 } }) } /** * Enforces config rules specific to running in serverless_mode: * - disables cross_application_tracer.enabled if set * - defaults logging to disabled * - verifies data specific to running DT is defined either in config file of env vars * * @param {*} inputConfig configuration passed to the Config constructor */ Config.prototype._enforceServerless = function _enforceServerless(inputConfig) { if (this.serverless_mode.enabled) { this._serverlessAppName() this._serverlessCAT() this._serverlessInfiniteTracing() this._serverlessLogging(inputConfig) this._serverlessNativeMetrics(inputConfig) this._serverlessDT(inputConfig) } else { this._preventServerlessDT() } } /** * 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() { const statusCodes = this?.error_collector?.ignore_status_codes if (statusCodes) { this.error_collector.ignore_status_codes = _parseCodes(statusCodes) } const expectedCodes = this?.error_collector?.expected_status_codes if (expectedCodes) { this.error_collector.expected_status_codes = _parseCodes(expectedCodes) } const grpcStatusCodes = this?.grpc?.ignore_status_codes if (grpcStatusCodes) { this.grpc.ignore_status_codes = _parseCodes(grpcStatusCodes) } const logAliases = { verbose: 'trace', debugging: 'debug', warning: 'warn', err: 'error' } const level = this.logging.level this.logging.level = logAliases[level] || level const region = parseKey(this.license_key) if (this.host === '') { if (region) { this.host = `collector.${region}.nr-data.net` } else { this.host = 'collector.newrelic.com' } } if (this.otlp_endpoint === '') { if (region) { this.otlp_endpoint = `otlp.${region}.nr-data.net` } else { this.otlp_endpoint = 'otlp.nr-data.net' } } if (this.license_key) { this.license_key = this.license_key.trim() } } /** * Splits a range of status codes. It will not * allow negative values, non-numbers, or numbers above 1000. * * @param {string} range range of status codes 400-421 * @param {Array} parsed list of parsed codes * @returns {Array} adds to the cleansed list of status codes * by removing any ranges and adds as elements */ function _parseRange(range, parsed) { const 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)) } const lower = parseInt(split[0], 10) const 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 (let i = lower; i <= upper; i++) { parsed.push(i) } } return parsed } /** * Parses a list of status codes. It can also * parse a range of status codes * * @param {Array} codes list of status codes * @returns {Array} cleansed list of status codes */ function _parseCodes(codes) { const parsedCodes = [] for (let i = 0; i < codes.length; i++) { const code = codes[i] if (typeof code === 'string' && code.indexOf('-') !== -1) { _parseRange(code, parsedCodes) } else { const 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() { const config = this checkNode('', this, HSM.HIGH_SECURITY_SETTINGS) // as a one off, we add a global exclude rule to the list to keep from // clobbering user defined rules this.attributes.exclude.push('request.parameters.*') function checkNode(base, target, settings) { Object.keys(settings).forEach(checkKey.bind(null, base, target, settings)) } function checkKey(base, target, settings, key) { const 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) } } } /** * Sends a response to collector for LASP application * * @param {Array} keys keys from a LASP policy * @returns {CollectorResponse} creates CollectorResponse with either preserve or shutdown */ function _laspReponse(keys) { 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) } /** * Applies the server side LASP policies to a local configuration object * * @param agent * @param {object} policies server side LASP policy * @returns {object} { missingRequired, finalPolicies } list of missing required fields and finalized LASP policy */ Config.prototype._buildLaspPolicy = function _buildLaspPolicy(agent, policies) { const config = this const keys = Object.keys(policies) const missingRequired = [] const finalPolicies = keys.reduce(function applyPolicy(obj, name) { const policy = policies[name] const 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 { const splitConfigName = localMapping.path.split('.') let settingBlock = config[splitConfigName[0]] // pull out the configuration subsection that the option lives in for (let i = 1; i < splitConfigName.length - 1; ++i) { settingBlock = settingBlock[splitConfigName[i]] } const valueName = splitConfigName[splitConfigName.length - 1] const localVal = settingBlock[valueName] // Indexes into "allowed values" based on "enabled" setting // to retrieve proper mapping. const policyValues = localMapping.allowedValues const policyValue = policyValues[policy.enabled ? 1 : 0] // get the most secure setting between local config and the policy const finalValue = (settingBlock[valueName] = config._getMostSecure( name, localVal, policyValue )) policy.enabled = policyValues.indexOf(finalValue) === 1 obj[name] = policy if (!