UNPKG

newrelic

Version:
918 lines (781 loc) 28.3 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' const AdaptiveSampler = require('./adaptive-sampler') const CollectorAPI = require('./collector/api') const ServerlessCollector = require('./collector/serverless') const DESTINATIONS = require('./config/attribute-filter').DESTINATIONS const CustomEventAggregator = require('./custom-events/custom-event-aggregator') const ErrorCollector = require('./errors/error-collector') const ErrorTraceAggregator = require('./errors/error-trace-aggregator') const ErrorEventAggregator = require('./errors/error-event-aggregator') const EventEmitter = require('events').EventEmitter const hashes = require('./util/hashes') const logger = require('./logger') const MetricMapper = require('./metrics/mapper') const MetricNormalizer = require('./metrics/normalizer') const MetricAggregator = require('./metrics/metric-aggregator') const NAMES = require('./metrics/names') const QueryTraceAggregator = require('./db/query-trace-aggregator') const sampler = require('./sampler') const TransactionTraceAggregator = require('./transaction/trace/aggregator') const TransactionEventAggregator = require('./transaction/transaction-event-aggregator') const Tracer = require('./transaction/tracer') const TxSegmentNormalizer = require('./metrics/normalizer/tx_segment') const uninstrumented = require('./uninstrumented') const util = require('util') const createSpanEventAggregator = require('./spans/create-span-event-aggregator') // Map of valid states to whether or not data collection is valid const STATES = { stopped: false, starting: true, connecting: true, connected: true, started: true, disconnected: false, stopping: false, errored: false } const MAX_ERROR_TRACES_DEFAULT = 20 const INITIAL_HARVEST_DELAY_MS = 1000 const DEFAULT_HARVEST_INTERVAL_MS = 60000 /** * There's a lot of stuff in this constructor, due to Agent acting as the * orchestrator for New Relic within instrumented applications. * * This constructor can throw if, for some reason, the configuration isn't * available. Don't try to recover here, because without configuration the * agent can't be brought up to a useful state. */ function Agent(config) { EventEmitter.call(this) if (!config) throw new Error('Agent must be created with a configuration!') // The agent base attributes which last throughout its lifetime. this._state = 'stopped' this.config = config this.environment = require('./environment') this.version = this.config.version if (config.serverless_mode.enabled) { this.collector = new ServerlessCollector(this) } else { this.collector = new CollectorAPI(this) } this.mapper = new MetricMapper() this.metricNameNormalizer = new MetricNormalizer(this.config, 'metric name') this.metrics = new MetricAggregator( { periodMs: DEFAULT_HARVEST_INTERVAL_MS, apdexT: this.config.apdex_t, mapper: this.mapper, normalizer: this.metricNameNormalizer }, this.collector ) this.metrics.on( 'starting metric_data data send.', this._beforeMetricDataSend.bind(this) ) this.spanEventAggregator = createSpanEventAggregator(config, this.collector, this.metrics) this.transactionNameNormalizer = new MetricNormalizer(this.config, 'transaction name') // Segment term based tx renaming for MGI mitigation. this.txSegmentNormalizer = new TxSegmentNormalizer() // User naming and ignoring rules. this.urlNormalizer = new MetricNormalizer(this.config, 'URL') this.userNormalizer = new MetricNormalizer(this.config, 'user') this.userNormalizer.loadFromConfig() this.transactionEventAggregator = new TransactionEventAggregator( { periodMs: config.event_harvest_config.report_period_ms, limit: config.event_harvest_config.harvest_limits.analytic_event_data }, this.collector, this.metrics ) this.customEventAggregator = new CustomEventAggregator( { periodMs: config.event_harvest_config.report_period_ms, limit: config.event_harvest_config.harvest_limits.custom_event_data }, this.collector, this.metrics ) const errorTraceAggregator = new ErrorTraceAggregator( { periodMs: DEFAULT_HARVEST_INTERVAL_MS, limit: MAX_ERROR_TRACES_DEFAULT }, this.collector ) const errorEventAggregator = new ErrorEventAggregator( { periodMs: config.event_harvest_config.report_period_ms, limit: config.event_harvest_config.harvest_limits.error_event_data }, this.collector, this.metrics ) this.errors = new ErrorCollector( config, errorTraceAggregator, errorEventAggregator, this.metrics ) // Transaction tracing. this.tracer = new Tracer(this) this.traces = new TransactionTraceAggregator( { periodMs: DEFAULT_HARVEST_INTERVAL_MS, config: this.config, isAsync: !config.serverless_mode.enabled, method: 'transaction_sample_data' }, this.collector ) this.transactionSampler = new AdaptiveSampler({ agent: this, serverless: config.serverless_mode.enabled, period: config.sampling_target_period_in_seconds * 1000, target: config.sampling_target }) this.queries = new QueryTraceAggregator( { config: this.config, periodMs: DEFAULT_HARVEST_INTERVAL_MS, method: 'sql_trace_data', isAsync: !config.serverless_mode.enabled }, this.collector ) // Set up all the configuration events the agent needs to listen for. this._listenForConfigChanges() // Entity tracking metrics. this.totalActiveSegments = 0 this.segmentsCreatedInHarvest = 0 this.segmentsClearedInHarvest = 0 // Used by shutdown code as well as entity tracking stats this.activeTransactions = 0 // Finally, add listeners for the agent's own events. this.on('transactionFinished', this._transactionFinished.bind(this)) } util.inherits(Agent, EventEmitter) /** * The agent is meant to only exist once per application, but the singleton is * managed by index.js. An agent will be created even if the agent's disabled by * the configuration. * * @config {boolean} agent_enabled Whether to start up the agent. * * @param {Function} callback Continuation and error handler. */ Agent.prototype.start = function start(callback) { if (!callback) throw new TypeError('callback required!') const agent = this this.setState('starting') if (this.config.agent_enabled !== true) { logger.warn('The New Relic Node.js agent is disabled by its configuration. ' + 'Not starting!') this.setState('stopped') return process.nextTick(callback) } sampler.start(agent) if (this.config.serverless_mode.enabled) { return this._serverlessModeStart(callback) } if (!this.config.license_key) { logger.error('A valid account license key cannot be found. ' + 'Has a license key been specified in the agent configuration ' + 'file or via the NEW_RELIC_LICENSE_KEY environment variable?') this.setState('errored') sampler.stop() return process.nextTick(function onNextTick() { callback(new Error('Not starting without license key!')) }) } logger.info('Starting New Relic for Node.js connection process.') this.collector.connect(function onStartConnect(error, response) { if (error || response.shouldShutdownRun()) { agent.setState('errored') sampler.stop() callback( error || new Error('Failed to connect to collector'), response && response.payload ) return } if (agent.collector.isConnected()) { agent.onConnect() agent.setState('started') const config = response.payload if (agent.config.no_immediate_harvest) { agent.startAggregators() callback(null, config) } else { // For data collection that streams immediately, dont delay capture/sending until // the harvest of everything else has completed. agent.startStreaming() // Harvest immediately for quicker data display, but after at least 1 // second or the collector will throw away the data. // // NOTE: this setTimeout is deliberately NOT unref'd due to it being // the last step in the Agent startup process setTimeout(function afterTimeout() { logger.info(`Starting initial ${INITIAL_HARVEST_DELAY_MS}ms harvest.`) agent.forceHarvestAll(function afterAllAggregatorsSend() { agent.startAggregators() callback(null, config) }) }, INITIAL_HARVEST_DELAY_MS) } } else { callback(new Error('Collector did not connect and did not error')) } }) } /** * Forces all aggregators to send the data collected. * @param {Function} callback The callback to invoke when all data types have been sent. */ Agent.prototype.forceHarvestAll = function forceHarvestAll(callback) { const agent = this const promises = [] const metricPromise = new Promise((resolve) => { agent.metrics.once( 'finished metric_data data send.', function onMetricsFinished() { resolve() } ) agent.metrics.send() }) promises.push(metricPromise) // TODO: plumb config through to aggregators so they can do their own checking. if ( agent.config.distributed_tracing.enabled && agent.config.span_events.enabled && !agent.spanEventAggregator.isStream // Not valid to send on streaming aggregator ) { const spanPromise = new Promise((resolve) => { agent.spanEventAggregator.once( 'finished span_event_data data send.', function onSpansFinished() { resolve() } ) agent.spanEventAggregator.send() }) promises.push(spanPromise) } if (agent.config.custom_insights_events.enabled) { const customEventPromise = new Promise((resolve) => { agent.customEventAggregator.once( 'finished custom_event_data data send.', function onCustomEventsFinished() { resolve() } ) agent.customEventAggregator.send() }) promises.push(customEventPromise) } if (agent.config.transaction_events.enabled) { const transactionEventPromise = new Promise((resolve) => { agent.transactionEventAggregator.once( 'finished analytic_event_data data send.', function onTransactionEventsFinished() { resolve() } ) agent.transactionEventAggregator.send() }) promises.push(transactionEventPromise) } if (agent.config.transaction_tracer.enabled && agent.config.collect_traces) { const transactionTracePromise = new Promise((resolve) => { agent.traces.once( 'finished transaction_sample_data data send.', function onTracesFinished() { resolve() } ) agent.traces.send() }) promises.push(transactionTracePromise) } if (agent.config.slow_sql.enabled) { const sqlTracePromise = new Promise((resolve) => { agent.queries.once( 'finished sql_trace_data data send.', function onSqlTracesFinished() { resolve() } ) agent.queries.send() }) promises.push(sqlTracePromise) } const errorCollectorEnabled = agent.config.error_collector && agent.config.error_collector.enabled if (errorCollectorEnabled && agent.config.collect_errors) { const errorTracePromise = new Promise((resolve) => { agent.errors.traceAggregator.once( 'finished error_data data send.', function onErrorTracesFinished() { resolve() } ) agent.errors.traceAggregator.send() }) promises.push(errorTracePromise) } if (errorCollectorEnabled && agent.config.error_collector.capture_events) { const errorEventPromise = new Promise((resolve) => { agent.errors.eventAggregator.once( 'finished error_event_data data send.', function onErrorEventsFinished() { resolve() } ) agent.errors.eventAggregator.send() }) promises.push(errorEventPromise) } Promise.all(promises).then(() => { // Get out of the promise so callback errors aren't treated as // promise rejections. setImmediate(callback) }) } Agent.prototype.stopAggregators = function stopAggregators() { this.metrics.stop() this.errors.stop() this.traces.stop() this.queries.stop() this.spanEventAggregator.stop() this.transactionEventAggregator.stop() this.customEventAggregator.stop() } Agent.prototype.startStreaming = function startStreaming() { if ( this.spanEventAggregator.isStream && this.config.distributed_tracing.enabled && this.config.span_events.enabled ) { this.spanEventAggregator.start() } } Agent.prototype.startAggregators = function startAggregators() { this.metrics.start() this.errors.start() if (this.config.transaction_tracer.enabled && this.config.collect_traces) { this.traces.start() } if (this.config.slow_sql.enabled) { this.queries.start() } if (this.config.distributed_tracing.enabled && this.config.span_events.enabled) { this.spanEventAggregator.start() } if (this.config.transaction_events.enabled) { this.transactionEventAggregator.start() } if (this.config.custom_insights_events.enabled) { this.customEventAggregator.start() } } Agent.prototype.onConnect = function onConnect() { this.metrics.reconfigure(this.config) this.errors.reconfigure(this.config) this.traces.reconfigure(this.config) this.queries.reconfigure(this.config) this.spanEventAggregator.reconfigure(this.config) this.transactionEventAggregator.reconfigure(this.config) this.customEventAggregator.reconfigure(this.config) if (this.config.certificates && this.config.certificates.length > 0) { this.metrics.getOrCreateMetric(NAMES.FEATURES.CERTIFICATES).incrementCallCount() } } /** * Bypasses standard collector connection by immediately invoking the startup * callback, after gathering local environment details. * * @param {Function} callback */ Agent.prototype._serverlessModeStart = function _serverlessModeStart(callback) { logger.info( 'New Relic for Node.js starting in serverless mode -- skipping connection process.' ) setImmediate(() => callback(null, this.config)) } /** * Any memory claimed by the agent will be retained after stopping. * * FIXME: make it possible to dispose of the agent, as well as do a * "hard" restart. This requires working with shimmer to strip the * current instrumentation and patch to the module loader. */ Agent.prototype.stop = function stop(callback) { if (!callback) throw new TypeError('callback required!') const agent = this this.setState('stopping') this.stopAggregators() sampler.stop() if (this.collector.isConnected()) { this.collector.shutdown(function onShutdown(error) { if (error) { agent.setState('errored') logger.warn(error, 'Got error shutting down connection to New Relic:') } else { agent.setState('stopped') logger.info('Stopped New Relic for Node.js.') } callback(error) }) } else { logger.trace('Collector was not connected, invoking callback.') process.nextTick(callback) } } /** * Resets queries. */ Agent.prototype._resetQueries = function resetQueries() { this.queries.clear() } Agent.prototype._resetErrors = function resetErrors() { this.errors.clearAll() // TODO: is this still necessary? // Likely do more direct with new config this.errors.reconfigure(this.config) } /** * Resets events. */ Agent.prototype._resetEvents = function resetEvents() { this.transactionEventAggregator.clear() } /** * Resets custom events. * * @param {boolean} forceReset * Flag signalling unconditional reset, sent during LASP application. */ Agent.prototype._resetCustomEvents = function resetCustomEvents() { this.customEventAggregator.clear() } /** * This method invokes a harvest synchronously. * * NOTE: this doesn't currently work outside of serverless mode. */ Agent.prototype.harvestSync = function harvestSync() { logger.trace('Peparing to harvest.') if (!this.collector.isConnected()) { throw new Error('Sync harvest not connected/enabled!') } // We have a connection, create a new harvest. this.emit('harvestStarted') logger.info('Harvest started.') const collector = this.collector const agent = this // "Sends" data to the serverless collector collection this.metrics.send() this.errors.traceAggregator.send() this.errors.eventAggregator.send() this.traces.send() this.queries.send() this.spanEventAggregator.send() this.transactionEventAggregator.send() this.customEventAggregator.send() // Write serverless output collector.flushPayloadSync() agent.emit('harvestFinished') logger.info('Harvest finished.') } Agent.prototype._beforeMetricDataSend = function _beforeMetricDataSend() { this._generateEntityStatsAndClear() // Send uninstrumented supportability metrics every metric harvest cycle uninstrumented.createMetrics(this.metrics) if (this.spanEventAggregator.isStream) { this.spanEventAggregator.createMetrics() } } Agent.prototype._generateEntityStatsAndClear = function _generateHarvestMetrics() { // Note some information about the size of this harvest. if (logger.traceEnabled()) { logger.trace({ segmentTotal: this.totalActiveSegments, harvestCreated: this.segmentsCreatedInHarvest, harvestCleared: this.segmentsClearedInHarvest, activeTransactions: this.activeTransactions }, 'Entity stats on metric harvest') } // Reset the counters. this.segmentsCreatedInHarvest = 0 this.segmentsClearedInHarvest = 0 } /** * Public interface for passing configuration data from the collector * on to the configuration, in an effort to keep them at least somewhat * decoupled. * * @param {object} configuration New config JSON from the collector. */ Agent.prototype.reconfigure = function reconfigure(configuration) { if (!configuration) throw new TypeError('must pass configuration') this.config.onConnect(configuration) } /** * Set the current state of the agent. Some states will not allow the * creation of Transactions. * * @param {string} newState The new state of the agent. */ Agent.prototype.setState = function setState(newState) { if (!STATES.hasOwnProperty(newState)) { throw new TypeError('Invalid state ' + newState) } logger.info('Agent state changed from %s to %s.', this._state, newState) this._state = newState this.emit(this._state) } /** * Return true if the agent is in a run state that can collect and * process data. */ Agent.prototype.canCollectData = function canCollectData() { return STATES[this._state] } /** * `agent_enabled` changed. This will generally only happen because of a high * security mode mismatch between the agent and the collector. This only * expects to have to stop the agent. No provisions have been made, nor * testing have been done to make sure it is safe to start the agent back up. */ Agent.prototype._enabledChange = function _enabledChange() { if (this.config.agent_enabled === false) { logger.warn('agent_enabled has been changed to false, stopping the agent.') this.stop(function nop() {}) } } /** * Report new settings to collector after a configuration has changed. This * always occurs after handling a response from a connect call. */ Agent.prototype._configChange = function _configChange() { this.collector.reportSettings() } Agent.prototype._addIntrinsicAttrsFromTransaction = _addIntrinsicAttrsFromTransaction function _addIntrinsicAttrsFromTransaction(transaction) { const intrinsicAttributes = { webDuration: transaction.timer.getDurationInMillis() / 1000, timestamp: transaction.timer.start, name: transaction.getFullName(), duration: transaction.timer.getDurationInMillis() / 1000, totalTime: transaction.trace.getTotalTimeDurationInMillis() / 1000, type: 'Transaction', error: transaction.hasErrors() } let metric = transaction.metrics.getMetric(NAMES.QUEUETIME) if (metric) { intrinsicAttributes.queueDuration = metric.total } metric = transaction.metrics.getMetric(NAMES.EXTERNAL.ALL) if (metric) { intrinsicAttributes.externalDuration = metric.total intrinsicAttributes.externalCallCount = metric.callCount } metric = transaction.metrics.getMetric(NAMES.DB.ALL) if (metric) { intrinsicAttributes.databaseDuration = metric.total intrinsicAttributes.databaseCallCount = metric.callCount } if (this.config.distributed_tracing.enabled) { transaction.addDistributedTraceIntrinsics(intrinsicAttributes) if (transaction.parentSpanId) { intrinsicAttributes.parentSpanId = transaction.parentSpanId } if (transaction.parentId) { intrinsicAttributes.parentId = transaction.parentId } } else if ( this.config.cross_application_tracer.enabled && !transaction.invalidIncomingExternalTransaction && ( transaction.referringTransactionGuid || transaction.includesOutboundRequests() ) ) { intrinsicAttributes['nr.guid'] = transaction.id intrinsicAttributes['nr.tripId'] = transaction.tripId || transaction.id intrinsicAttributes['nr.pathHash'] = hashes.calculatePathHash( this.config.applications()[0], transaction.getFullName(), transaction.referringPathHash ) if (transaction.referringPathHash) { intrinsicAttributes['nr.referringPathHash'] = transaction.referringPathHash } if (transaction.referringTransactionGuid) { var refId = transaction.referringTransactionGuid intrinsicAttributes['nr.referringTransactionGuid'] = refId } var alternatePathHashes = transaction.alternatePathHashes() if (alternatePathHashes) { intrinsicAttributes['nr.alternatePathHashes'] = alternatePathHashes } if (transaction.baseSegment && transaction.type === 'web') { var apdex = ( this.config.web_transactions_apdex[transaction.getFullName()] || this.config.apdex_t ) var duration = transaction.baseSegment.getDurationInMillis() / 1000 intrinsicAttributes['nr.apdexPerfZone'] = calculateApdexZone(duration, apdex) } } if (transaction.syntheticsData) { intrinsicAttributes['nr.syntheticsResourceId'] = transaction.syntheticsData.resourceId intrinsicAttributes['nr.syntheticsJobId'] = transaction.syntheticsData.jobId intrinsicAttributes['nr.syntheticsMonitorId'] = transaction.syntheticsData.monitorId } return intrinsicAttributes } function calculateApdexZone(duration, apdexT) { if (duration <= apdexT) { return 'S' // satisfied } if (duration <= apdexT * 4) { return 'T' // tolerating } return 'F' // frustrated } Agent.prototype._addEventFromTransaction = function _addEventFromTransaction(tx) { if (!this.config.transaction_events.enabled) return const intrinsicAttributes = this._addIntrinsicAttrsFromTransaction(tx) const userAttributes = tx.trace.custom.get(DESTINATIONS.TRANS_EVENT) const agentAttributes = tx.trace.attributes.get(DESTINATIONS.TRANS_EVENT) const event = [ intrinsicAttributes, userAttributes, agentAttributes ] this.transactionEventAggregator.add(event, tx.priority || Math.random()) } /** * Put all the logic for handing finalized transactions off to the tracers and * metric collections in one place. * * @param {Transaction} transaction Newly-finalized transaction. */ Agent.prototype._transactionFinished = function _transactionFinished(transaction) { // Allow the API to explicitly set the ignored status. if (transaction.forceIgnore !== null) { transaction.ignore = transaction.forceIgnore } if (!transaction.ignore) { if (transaction.forceIgnore === false) { logger.debug('Explicitly not ignoring %s (%s).', transaction.name, transaction.id) } this.metrics.merge(transaction.metrics, false) this.errors.onTransactionFinished(transaction) this.traces.add(transaction) const trace = transaction.trace trace.intrinsics = transaction.getIntrinsicAttributes() this._addEventFromTransaction(transaction) } else if (transaction.forceIgnore === true) { logger.debug('Explicitly ignoring %s (%s).', transaction.name, transaction.id) } else { logger.debug('Ignoring %s (%s).', transaction.name, transaction.id) } --this.activeTransactions this.totalActiveSegments -= transaction.numSegments this.segmentsClearedInHarvest += transaction.numSegments } Agent.prototype.setLambdaArn = function setLambdaArn(arn) { if (this.collector instanceof ServerlessCollector) { this.collector.setLambdaArn(arn) } } Agent.prototype.setLambdaFunctionVersion = function setLambdaFunctionVersion(function_version) { if (this.collector instanceof ServerlessCollector) { this.collector.setLambdaFunctionVersion(function_version) } } /** * Get the current transaction (if there is one) from the tracer. * * @returns {Transaction} The current transaction. */ Agent.prototype.getTransaction = function getTransaction() { return this.tracer.getTransaction() } Agent.prototype.recordSupportability = function recordSupportability(name, value) { const metric = this.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.PREFIX + name) if (value != null) { metric.recordValue(value) } else { metric.incrementCallCount() } } Agent.prototype._listenForConfigChanges = function _listenForConfigChanges() { const self = this this.config.on('agent_enabled', this._enabledChange.bind(this)) this.config.on('change', this._configChange.bind(this)) this.config.on('metric_name_rules', function updateMetricNameNormalizer() { self.metricNameNormalizer.load.apply(self.metricNameNormalizer, arguments) }) this.config.on('transaction_name_rules', function updateTransactionNameNormalizer() { self.transactionNameNormalizer.load.apply(self.transactionNameNormalizer, arguments) }) this.config.on('url_rules', function updateUrlNormalizer() { self.urlNormalizer.load.apply(self.urlNormalizer, arguments) }) this.config.on('transaction_segment_terms', function updateSegmentNormalizer() { self.txSegmentNormalizer.load.apply(self.txSegmentNormalizer, arguments) }) this.config.on('sampling_target', function updateSamplingTarget(target) { self.transactionSampler.samplingTarget = target }) this.config.on( 'sampling_target_period_in_seconds', function updateSamplePeriod(period) { self.transactionSampler.samplingPeriod = period * 1000 } ) this.config.on('event_harvest_config', function onHarvestConfigReceived(harvestConfig) { if (harvestConfig) { generateEventHarvestSupportMetrics(self, harvestConfig) } }) } function generateEventHarvestSupportMetrics(agent, harvestConfig) { const harvestLimits = harvestConfig.harvest_limits const harvestNames = NAMES.EVENT_HARVEST const harvestLimitNames = harvestNames.HARVEST_LIMIT const reportPeriodMetric = agent.metrics.getOrCreateMetric(harvestNames.REPORT_PERIOD) reportPeriodMetric.recordValue(harvestConfig.report_period_ms) const analyticLimit = harvestLimits.analytic_event_data if (analyticLimit) { const analyticLimitMetric = agent.metrics.getOrCreateMetric( harvestLimitNames.ANALYTIC ) analyticLimitMetric.recordValue(analyticLimit) } const customLimit = harvestLimits.custom_event_data if (customLimit) { const customLimitMetric = agent.metrics.getOrCreateMetric(harvestLimitNames.CUSTOM) customLimitMetric.recordValue(customLimit) } const errorLimit = harvestLimits.error_event_data if (errorLimit) { const errorLimitMetric = agent.metrics.getOrCreateMetric(harvestLimitNames.ERROR) errorLimitMetric.recordValue(errorLimit) } const spanLimit = harvestLimits.span_event_data if (spanLimit) { const spanLimitMetric = agent.metrics.getOrCreateMetric(harvestLimitNames.SPAN) spanLimitMetric.recordValue(spanLimit) } } module.exports = Agent