UNPKG

newrelic

Version:
809 lines (708 loc) 25.9 kB
'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 ErrorAggregator = require('./errors/aggregator') const EventEmitter = require('events').EventEmitter const Harvest = require('./harvest') const hashes = require('./util/hashes') const logger = require('./logger') const MetricMapper = require('./metrics/mapper') const MetricNormalizer = require('./metrics/normalizer') const Metrics = require('./metrics') const NAMES = require('./metrics/names') const PriorityQueue = require('./priority-queue') const QueryTracer = require('./db/tracer') const sampler = require('./sampler') const SpanAggregator = require('./spans/aggregator') const TraceAggregator = require('./transaction/trace/aggregator') const Tracer = require('./transaction/tracer') const TxSegmentNormalizer = require('./metrics/normalizer/tx_segment') const uninstrumented = require('./uninstrumented') const util = require('util') const AGENT_RUN_BEHAVIOR = require('./collector/response').AGENT_RUN_BEHAVIOR const STATES = [ 'stopped', // start state 'starting', // starting agent 'connecting', // handshaking with NR 'connected', // connected to collector 'disconnected', // disconnected from collector 'started', // up and running 'stopping', // shutting down 'errored' // stopped due to error ] // just to make clear what's going on const TO_MILLIS = 1e3 const SERVERLESS_SAMPLING_LIMIT = Infinity /** * 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) } // Reset the agent to add all the sub-objects it needs. These object are the // ones that get re-created if the agent is told to restart from the collector. this.events = null this.customEvents = null this.errors = null this.mapper = null this.metricNameNormalizer = null this.metrics = null this.spans = null this.transactionNameNormalizer = null this.txSegmentNormalizer = null this.urlNormalizer = null this.userNormalizer = null this.reset() // Transaction tracing. this.tracer = new Tracer(this) this.traces = new TraceAggregator(this.config) this.transactionSampler = new AdaptiveSampler({ agent: this, serverless: config.serverless_mode.enabled, period: config.sampling_target_period_in_seconds * 1000, target: config.sampling_target }) // Query tracing. this.queries = new QueryTracer(this.config) // 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 this.activeTransactions = 0 this.transactionsCreatedInHarvest = 0 // Harvest attributes. this.harvesterHandle = null this._lastHarvest = null // 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 onConnect(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.setState('started') const config = response.payload if (agent.config.no_immediate_harvest) { agent._scheduleHarvester(agent.config.data_report_period) callback(null, config) } else { // 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() { agent.harvest(function onHarvest(harvestError) { callback(harvestError, config) }) }, 1000) } } else { callback(new Error('Collector did not connect and did not error')) } }) } /** * 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._stopHarvester() 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 { process.nextTick(callback) } } /** * Resets queries. * * @param {boolean} forceReset * Flag signalling unconditional reset, sent during LASP application. */ Agent.prototype._resetQueries = function resetQueries(forceReset) { if (!this.queries || forceReset) { this.queries = new QueryTracer(this.config) } } /** * Resets errors. * * @param {boolean} forceReset * Flag signalling unconditional reset, sent during LASP application. */ Agent.prototype._resetErrors = function resetErrors(forceReset) { if (!this.errors || forceReset) { this.errors = new ErrorAggregator(this.config) } this.errors.reconfigure(this.config) } /** * Resets events. */ Agent.prototype._resetEvents = function resetEvents() { if (!this.events) { this.events = new PriorityQueue() } this.events.setLimit( this.config.serverless_mode.enabled ? SERVERLESS_SAMPLING_LIMIT : this.config.transaction_events.max_samples_per_minute ) if (!this.customEvents) { this.customEvents = new PriorityQueue() } } /** * Resets custom events. * * @param {boolean} forceReset * Flag signalling unconditional reset, sent during LASP application. */ Agent.prototype._resetCustomEvents = function resetCustomEvents(forceReset) { if (!this.customEvents || forceReset) { this.customEvents = new PriorityQueue() } this.customEvents.setLimit( this.config.serverless_mode.enabled ? SERVERLESS_SAMPLING_LIMIT : this.config.custom_insights_events.max_samples_stored ) } /** * Builds all of the sub-properties of the agent that rely on configurations. */ Agent.prototype.reset = function reset() { // Insights events. this._resetEvents() this._resetCustomEvents() // Error tracing. this._resetErrors() // Open tracing. this.spans = new SpanAggregator( this.config.serverless_mode.enabled ? SERVERLESS_SAMPLING_LIMIT : null ) // Metrics. this.mapper = new MetricMapper() this.metricNameNormalizer = new MetricNormalizer(this.config, 'metric name') this.metrics = new Metrics(this.config.apdex_t, this.mapper, this.metricNameNormalizer) // Transaction naming. this.transactionNameNormalizer = new MetricNormalizer(this.config, 'transaction name') this.urlNormalizer = new MetricNormalizer(this.config, 'URL') // Segment term based tx renaming for MGI mitigation. this.txSegmentNormalizer = new TxSegmentNormalizer() // User naming and ignoring rules. this.userNormalizer = new MetricNormalizer(this.config, 'user') this.userNormalizer.loadFromConfig() } /** * On agent startup, an interval timer is started that calls this method once * a minute, which in turn invokes the pieces of the harvest cycle. It calls * the various collector API methods in order, bailing out if one of them fails, * to ensure that the agents don't pummel the collector if it's already * struggling. */ Agent.prototype.harvest = function harvest(callback) { if (!callback) { throw new TypeError('callback required!') } // Generate metrics for this harvest and then check we are connected to the // collector. this._generateHarvestMetrics() if (!this.collector.isConnected()) { return setImmediate(function immediatelyError() { callback(new Error('Not connected to New Relic!')) }) } // We have a connection, create a new harvest. this.emit('harvestStarted') this._lastHarvest = new Harvest(this) this._lastHarvest.prepare(Harvest.ALL_ENDPOINTS) // Reset all our collections. The harvest has all the data it needs at this point. this._resetHarvestables() // Send the harvest! const collector = this.collector const agent = this this._lastHarvest.send(function afterHarvest(err, agentRunAction) { if (err) { return callback(err) } // The serverless collector will never tell us anything interesting to do, // but has an awkward, final step that the normal collector does not. if (collector instanceof ServerlessCollector) { return collector.flushPayload(function afterFlush() { agent.emit('harvestFinished') callback() }) } // Do we need to do anything to the agent run? if (agentRunAction === AGENT_RUN_BEHAVIOR.SHUTDOWN) { agent.emit('harvestFinished') agent.stop(callback) } else if (agentRunAction === AGENT_RUN_BEHAVIOR.RESTART) { collector.restart(function afterRestart(restartError) { // TODO: What if preconnect/connect respond with shutdown here? if (restartError) { logger.warn('Failed to restart agent run after harvest') callback(restartError) } else { _finish() } }) } else { _finish() } function _finish() { agent.emit('harvestFinished') agent._scheduleHarvester(agent.config.data_report_period) callback() } }) } Agent.prototype._generateHarvestMetrics = 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, spansCollected: this.spans.length, spansSeen: this.spans.seen }, 'Entity stats on harvest') } this.recordSupportability( 'Nodejs/Transactions/Created', this.transactionsCreatedInHarvest ) // Send uninstrumented supportability metrics every harvest cycle uninstrumented.createMetrics(this.metrics) // Reset the counters. this.segmentsCreatedInHarvest = 0 this.segmentsClearedInHarvest = 0 this.transactionsCreatedInHarvest = 0 } Agent.prototype._resetHarvestables = function _resetHarvestables() { // TODO: Make each aggregator able to compose its own payload and clean itself // up. Then the Harvest class can just iterate over all aggregations without // having to know bespoke reset information. this.metrics = new Metrics( this.config.apdex_t, this.mapper, this.metricNameNormalizer ) this.events = new PriorityQueue( this.config.serverless_mode.enabled ? SERVERLESS_SAMPLING_LIMIT : this.config.transaction_events.max_samples_per_minute ) this.customEvents = new PriorityQueue( this.config.serverless_mode.enabled ? SERVERLESS_SAMPLING_LIMIT : this.config.custom_insights_events.max_samples_stored ) this.errors.clearEvents() this.errors.clearErrors() this.traces.reset() this.queries = new QueryTracer(this.config) this.spans.clearEvents() } /** * 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) } /** * Make it easier to determine what state the agent thinks it's in (needed * for a few tests, but fragile). * * FIXME: remove the need for this * * @param {string} newState The new state of the agent. */ Agent.prototype.setState = function setState(newState) { if (STATES.indexOf(newState) === -1) { throw new TypeError('Invalid state ' + newState) } logger.debug('Agent state changed from %s to %s.', this._state, newState) this._state = newState this.emit(this._state) } /** * Server-side configuration value. * * @param {number} apdexT Apdex tolerating value, in seconds. */ Agent.prototype._apdexTChange = function _apdexTChange(apdexT) { logger.debug('Apdex tolerating value changed to %s.', apdexT) this.metrics.apdexT = apdexT } /** * Server-side configuration value. When run, forces a harvest cycle * so as to not cause the agent to go too long without reporting. * * @param {number} interval Time in seconds between harvest runs. */ Agent.prototype._harvesterIntervalChange = _harvesterIntervalChange function _harvesterIntervalChange(interval, callback) { const agent = this // only change the setup if the harvester is currently running if (this.harvesterHandle) { // force a harvest now, to be safe this.harvest(function onHarvest(error) { agent._restartHarvester(interval) if (callback) callback(error) }) } else if (callback) { process.nextTick(callback) } } /** * Restart the harvest cycle timer. * * @param {number} harvestSeconds How many seconds between harvests. */ Agent.prototype._restartHarvester = function _restartHarvester(harvestSeconds) { this._stopHarvester() this._scheduleHarvester(harvestSeconds) } /** * Safely stop the harvest cycle timer. */ Agent.prototype._stopHarvester = function _stopHarvester() { if (this.harvesterHandle) { clearTimeout(this.harvesterHandle) } this._lastHarvest = null this.harvesterHandle = null } /** * Safely start the harvest cycle timer, and ensure that the harvest * cycle won't keep an application from exiting if nothing else is * happening to keep it up. * * @param {number} harvestSeconds - How many seconds between harvests. */ Agent.prototype._scheduleHarvester = function _scheduleHarvester(harvestSeconds) { const agent = this let harvestDelay = harvestSeconds * TO_MILLIS // If there was a previous harvest, we want to schedule the next one based on // its start time. if (this._lastHarvest && this._lastHarvest.startTime) { const timeSinceHarvest = Date.now() - this._lastHarvest.startTime harvestDelay = Math.max(0, harvestDelay - timeSinceHarvest) } this.harvesterHandle = setTimeout(function doHarvest() { // Agent#harvest handles scheduling the next harvest and properly reacting to // any errors or commands. All we need to do is note any errors it spits out. agent.harvest(function harvestError(error) { if (error) { logger.warn(error, 'Error on submission to New Relic.') } }) }, harvestDelay) this.harvesterHandle.unref() } /** * `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.events.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) this.errors.onTransactionFinished(transaction, this.metrics) 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 if (this.config.serverless_mode.enabled) { this.harvest(function onServerlessHarvest(err) { if (err) { logger.error('Serverless mode harvest error', err) } }) } } Agent.prototype.setLambdaArn = function setLambdaArn(arn) { if (this.collector instanceof ServerlessCollector) { this.collector.setLambdaArn(arn) } } /** * 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('apdex_t', this._apdexTChange.bind(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( 'transaction_events.max_samples_per_minute', function updateEventSampleLimit(maxSamples) { self.events.setLimit(maxSamples) } ) } module.exports = Agent