newrelic
Version:
New Relic agent
1,586 lines (1,403 loc) • 50.8 kB
JavaScript
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const util = require('util')
const logger = require('./lib/logger').child({component: 'api'})
const recordWeb = require('./lib/metrics/recorders/http')
const recordBackground = require('./lib/metrics/recorders/other')
const customRecorder = require('./lib/metrics/recorders/custom')
const hashes = require('./lib/util/hashes')
const properties = require('./lib/util/properties')
const stringify = require('json-stringify-safe')
const shimmer = require('./lib/shimmer')
const shims = require('./lib/shim')
const isValidType = require('./lib/util/attribute-types')
const TransactionShim = require('./lib/shim/transaction-shim')
const TransactionHandle = require('./lib/transaction/handle')
const AwsLambda = require('./lib/serverless/aws-lambda')
const ATTR_DEST = require('./lib/config/attribute-filter').DESTINATIONS
const MODULE_TYPE = require('./lib/shim/constants').MODULE_TYPE
const NAMES = require('./lib/metrics/names')
/*
*
* CONSTANTS
*
*/
const RUM_STUB = "<script type='text/javascript' %s>window.NREUM||(NREUM={});" +
"NREUM.info = %s; %s</script>"
// these messages are used in the _gracefail() method below in getBrowserTimingHeader
const RUM_ISSUES = [
'NREUM: no browser monitoring headers generated; disabled',
'NREUM: transaction missing or ignored while generating browser monitoring headers',
'NREUM: config.browser_monitoring missing, something is probably wrong',
'NREUM: browser_monitoring headers need a transaction name',
'NREUM: browser_monitoring requires valid application_id',
'NREUM: browser_monitoring requires valid browser_key',
'NREUM: browser_monitoring requires js_agent_loader script',
'NREUM: browser_monitoring disabled by browser_monitoring.loader config'
]
// Can't overwrite internal parameters or all heck will break loose.
const CUSTOM_DENYLIST = new Set([
'nr_flatten_leading'
])
const CUSTOM_EVENT_TYPE_REGEX = /^[a-zA-Z0-9:_ ]+$/
/**
* The exported New Relic API. This contains all of the functions meant to be
* used by New Relic customers. For now, that means transaction naming.
*
* You do not need to directly instantiate this class, as an instance of this is
* the return from `require('newrelic')`.
*
* @constructor
*/
function API(agent) {
this.agent = agent
this.shim = new TransactionShim(agent, 'NewRelicAPI')
this.awsLambda = new AwsLambda(agent)
}
/**
* Give the current transaction a custom name. Overrides any New Relic naming
* rules set in configuration or from New Relic's servers.
*
* IMPORTANT: this function must be called when a transaction is active. New
* Relic transactions are tied to web requests, so this method may be called
* from within HTTP or HTTPS listener functions, Express routes, or other
* contexts where a web request or response object are in scope.
*
* @param {string} name The name you want to give the web request in the New
* Relic UI. Will be prefixed with 'Custom/' when sent.
*/
API.prototype.setTransactionName = function setTransactionName(name) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/setTransactionName'
)
metric.incrementCallCount()
var transaction = this.agent.tracer.getTransaction()
if (!transaction) {
return logger.warn("No transaction found when setting name to '%s'.", name)
}
if (!name) {
if (transaction && transaction.url) {
logger.error(
"Must include name in setTransactionName call for URL %s.",
transaction.url
)
} else {
logger.error("Must include name in setTransactionName call.")
}
return
}
logger.trace('Setting transaction %s name to %s', transaction.id, name)
transaction.forceName = NAMES.CUSTOM + '/' + name
}
/**
* This method returns an object with the following methods:
* - end: end the transaction that was active when `API#getTransaction`
* was called.
*
* - ignore: set the transaction that was active when
* `API#getTransaction` was called to be ignored.
*
* @returns {TransactionHandle} The transaction object with the `end` and
* `ignore` methods on it.
*/
API.prototype.getTransaction = function getTransaction() {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/getTransaction'
)
metric.incrementCallCount()
var transaction = this.agent.tracer.getTransaction()
if (!transaction) {
logger.debug("No transaction found when calling API#getTransaction")
return new TransactionHandle.Stub()
}
transaction.handledExternally = true
return new TransactionHandle(transaction, this.agent.metrics)
}
/**
* This method returns an object with the following keys/data:
* - `trace.id`: The current trace ID
* - `span.id`: The current span ID
* - `entity.name`: The application name specified in the connect request as
* app_name. If multiple application names are specified this will only be
* the first name
* - `entity.type`: The string "SERVICE"
* - `entity.guid`: The entity ID returned in the connect reply as entity_guid
* - `hostname`: The hostname as specified in the connect request as
* utilization.full_hostname. If utilization.full_hostname is null or empty,
* this will be the hostname specified in the connect request as host.
*
* @returns {LinkingMetadata} The linking object with the data above
*/
API.prototype.getLinkingMetadata = function getLinkingMetadata(omitSupportability) {
if (omitSupportability !== true) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/getLinkingMetadata'
)
metric.incrementCallCount()
}
const agent = this.agent
const segment = agent.tracer.getSegment()
const config = agent.config
const linkingMetadata = {
'entity.name': config.applications()[0],
'entity.type': 'SERVICE',
'hostname': config.getHostnameSafe()
}
if (config.distributed_tracing.enabled && segment) {
linkingMetadata['trace.id'] = segment.transaction.traceId
const spanId = segment.getSpanId()
if (spanId) {
linkingMetadata['span.id'] = spanId
}
} else {
logger.debug('getLinkingMetadata with no active transaction')
}
if (config.entity_guid) {
linkingMetadata['entity.guid'] = config.entity_guid
}
return linkingMetadata
}
/**
* Specify the `Dispatcher` and `Dispatcher Version` environment values.
* A dispatcher is typically the service responsible for brokering
* the request with the process responsible for responding to the
* request. For example Node's `http` module would be the dispatcher
* for incoming HTTP requests.
*
* @param {string} name The string you would like to report to New Relic
* as the dispatcher.
*
* @param {string} [version] The dispatcher version you would like to
* report to New Relic
*/
API.prototype.setDispatcher = function setDispatcher(name, version) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/setDispatcher'
)
metric.incrementCallCount()
if (!name || typeof name !== 'string') {
logger.error("setDispatcher must be called with a name, and name must be a string.")
return
}
// No objects allowed.
if (version && typeof version !== 'object') {
version = String(version)
} else {
logger.info('setDispatcher was called with an object as the version parameter')
version = null
}
this.agent.environment.setDispatcher(name, version, true)
}
/**
* Give the current transaction a name based on your own idea of what
* constitutes a controller in your Node application. Also allows you to
* optionally specify the action being invoked on the controller. If the action
* is omitted, then the API will default to using the HTTP method used in the
* request (e.g. GET, POST, DELETE). Overrides any New Relic naming rules set
* in configuration or from New Relic's servers.
*
* IMPORTANT: this function must be called when a transaction is active. New
* Relic transactions are tied to web requests, so this method may be called
* from within HTTP or HTTPS listener functions, Express routes, or other
* contexts where a web request or response object are in scope.
*
* @param {string} name The name you want to give the controller in the New
* Relic UI. Will be prefixed with 'Controller/' when
* sent.
* @param {string} action The action being invoked on the controller. Defaults
* to the HTTP method used for the request.
*/
API.prototype.setControllerName = function setControllerName(name, action) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/setControllerName'
)
metric.incrementCallCount()
var transaction = this.agent.tracer.getTransaction()
if (!transaction) {
return logger.warn("No transaction found when setting controller to %s.", name)
}
if (!name) {
if (transaction && transaction.url) {
logger.error(
"Must include name in setControllerName call for URL %s.",
transaction.url
)
} else {
logger.error("Must include name in setControllerName call.")
}
return
}
action = action || transaction.verb || 'GET'
transaction.forceName = NAMES.CONTROLLER + '/' + name + '/' + action
}
/**
* Add a custom attribute to the current transaction. Some attributes are
* reserved (see CUSTOM_DENYLIST for the current, very short list), and
* as with most API methods, this must be called in the context of an
* active transaction. Most recently set value wins.
*
* @param {string} key The key you want displayed in the RPM UI.
* @param {string} value The value you want displayed. Must be serializable.
*/
API.prototype.addCustomAttribute = function addCustomAttribute(key, value) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/addCustomAttribute'
)
metric.incrementCallCount()
// If high security mode is on, custom attributes are disabled.
if (this.agent.config.high_security) {
logger.warnOnce(
'Custom attributes',
'Custom attributes are disabled by high security mode.'
)
return false
} else if (!this.agent.config.api.custom_attributes_enabled) {
logger.debug(
'Config.api.custom_attributes_enabled set to false, not collecting value'
)
return false
}
const transaction = this.agent.tracer.getTransaction()
if (!transaction) {
logger.warn('No transaction found for custom attributes.')
return false
}
const trace = transaction.trace
if (!trace.custom) {
logger.warn(
'Could not add attribute %s to nonexistent custom attributes.',
key
)
return false
}
if (CUSTOM_DENYLIST.has(key)) {
logger.warn('Not overwriting value of NR-only attribute %s.', key)
return false
}
trace.addCustomAttribute(key, value)
const spanContext = this.agent.tracer.getSpanContext()
if (!spanContext) {
logger.debug('No span found for custom attributes.')
// success/failure is ambiguous here. since at least 1 attempt tried, not returning false
return
}
spanContext.addCustomAttribute(key, value, spanContext.ATTRIBUTE_PRIORITY.LOW)
}
/**
* Adds all custom attributes in an object to the current transaction.
*
* See documentation for newrelic.addCustomAttribute for more information on
* setting custom attributes.
*
* An example of setting a custom attribute object:
*
* newrelic.addCustomAttributes({test: 'value', test2: 'value2'});
*
* @param {object} [atts]
* @param {string} [atts.KEY] The name you want displayed in the RPM UI.
* @param {string} [atts.KEY.VALUE] The value you want displayed. Must be serializable.
*/
API.prototype.addCustomAttributes = function addCustomAttributes(atts) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/addCustomAttributes'
)
metric.incrementCallCount()
for (var key in atts) {
if (!properties.hasOwn(atts, key)) {
continue
}
this.addCustomAttribute(key, atts[key])
}
}
/**
* Add custom span attributes in an object to the current segment/span.
*
* See documentation for newrelic.addCustomSpanAttribute for more information.
*
* An example of setting a custom span attribute:
*
* newrelic.addCustomSpanAttribute({test: 'value', test2: 'value2'})
*
* @param {object} [atts]
* @param {string} [atts.KEY] The name you want displayed in the RPM UI.API.
* @param {string} [atts.KEY.VALUE] The value you want displayed. Must be serializable.
*/
API.prototype.addCustomSpanAttributes = function addCustomSpanAttributes(atts) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/addCustomSpanAttributes'
)
metric.incrementCallCount()
for (let key in atts) {
if (properties.hasOwn(atts, key)) {
this.addCustomSpanAttribute(key, atts[key])
}
}
}
/**
* Add a custom span attribute to the current transaction. Some attributes
* are reserved (see CUSTOM_DENYLIST for the current, very short list), and
* as with most API methods, this must be called in the context of an
* active segment/span. Most recently set value wins.
*
* @param {string} key The key you want displayed in the RPM UI.
* @param {string} value The value you want displayed. Must be serializable.
*/
API.prototype.addCustomSpanAttribute = function addCustomSpanAttribute(key, value) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/addCustomSpanAttribute'
)
metric.incrementCallCount()
// If high security mode is on, custom attributes are disabled.
if (this.agent.config.high_security) {
logger.warnOnce(
'Custom span attributes',
'Custom span attributes are disabled by high security mode.'
)
return false
} else if (!this.agent.config.api.custom_attributes_enabled) {
logger.debug(
'Config.api.custom_attributes_enabled set to false, not collecting value'
)
return false
}
const spanContext = this.agent.tracer.getSpanContext()
if (!spanContext) {
logger.debug(
'Could not add attribute %s. No available span.',
key
)
return false
}
if (CUSTOM_DENYLIST.has(key)) {
logger.warn('Not overwriting value of NR-only attribute %s.', key)
return false
}
spanContext.addCustomAttribute(key, value)
}
/**
* Send errors to New Relic that you've already handled yourself. Should be an
* `Error` or one of its subtypes, but the API will handle strings and objects
* that have an attached `.message` or `.stack` property.
*
* NOTE: Errors that are recorded using this method do _not_ obey the
* `ignore_status_codes` configuration.
*
* @param {Error} error
* The error to be traced.
*
* @param {object} [customAttributes]
* Optional. Any custom attributes to be displayed in the New Relic UI.
*/
API.prototype.noticeError = function noticeError(error, customAttributes) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/noticeError'
)
metric.incrementCallCount()
if (!this.agent.config.api.notice_error_enabled) {
logger.debug(
'Config.api.notice_error_enabled set to false, not collecting error'
)
return false
}
// If high security mode is on or custom attributes are disabled,
// noticeError does not collect custom attributes.
if (this.agent.config.high_security) {
logger.debug(
'Passing custom attributes to notice error API is disabled in high security mode.'
)
} else if (!this.agent.config.api.custom_attributes_enabled) {
logger.debug(
'Config.api.custom_attributes_enabled set to false, ' +
'ignoring custom error attributes.'
)
}
if (typeof error === 'string') {
error = new Error(error)
}
// Filter all object type valued attributes out
let filteredAttributes = customAttributes
if (customAttributes) {
filteredAttributes = _filterAttributes(customAttributes, 'noticeError')
}
const transaction = this.agent.tracer.getTransaction()
this.agent.errors.addUserError(transaction, error, filteredAttributes)
}
/**
* If the URL for a transaction matches the provided pattern, name the
* transaction with the provided name. If there are capture groups in the
* pattern (which is a standard JavaScript regular expression, and can be
* passed as either a RegExp or a string), then the substring matches ($1, $2,
* etc.) are replaced in the name string. BE CAREFUL WHEN USING SUBSTITUTION.
* If the replacement substrings are highly variable (i.e. are identifiers,
* GUIDs, or timestamps), the rule will generate too many metrics and
* potentially get your application blocked by New Relic.
*
* An example of a good rule with replacements:
*
* newrelic.addNamingRule('^/storefront/(v[1-5])/(item|category|tag)',
* 'CommerceAPI/$1/$2')
*
* An example of a bad rule with replacements:
*
* newrelic.addNamingRule('^/item/([0-9a-f]+)', 'Item/$1')
*
* Keep in mind that the original URL and any query parameters will be sent
* along with the request, so slow transactions will still be identifiable.
*
* Naming rules can not be removed once added. They can also be added via the
* agent's configuration. See configuration documentation for details.
*
* @param {RegExp} pattern The pattern to rename (with capture groups).
* @param {string} name The name to use for the transaction.
*/
API.prototype.addNamingRule = function addNamingRule(pattern, name) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/addNamingRule'
)
metric.incrementCallCount()
if (!name) return logger.error("Simple naming rules require a replacement name.")
this.agent.userNormalizer.addSimple(pattern, '/' + name)
}
/**
* If the URL for a transaction matches the provided pattern, ignore the
* transaction attached to that URL. Useful for filtering socket.io connections
* and other long-polling requests out of your agents to keep them from
* distorting an app's apdex or mean response time. Pattern may be a (standard
* JavaScript) RegExp or a string.
*
* Example:
*
* newrelic.addIgnoringRule('^/socket\\.io/')
*
* @param {RegExp} pattern The pattern to ignore.
*/
API.prototype.addIgnoringRule = function addIgnoringRule(pattern) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/addIgnoringRule'
)
metric.incrementCallCount()
if (!pattern) return logger.error("Must include a URL pattern to ignore.")
this.agent.userNormalizer.addSimple(pattern, null)
}
/**
* Get the <script>...</script> header necessary for Browser Monitoring
* This script must be manually injected into your templates, as high as possible
* in the header, but _after_ any X-UA-COMPATIBLE HTTP-EQUIV meta tags.
* Otherwise you may hurt IE!
*
* This method must be called _during_ a transaction, and must be called every
* time you want to generate the headers.
*
* Do *not* reuse the headers between users, or even between requests.
*
* @param {string} [options.nonce] - Nonce to inject into `<script>` header.
*
* @returns {string} The `<script>` header to be injected.
*/
API.prototype.getBrowserTimingHeader = function getBrowserTimingHeader(options) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/getBrowserTimingHeader'
)
metric.incrementCallCount()
var config = this.agent.config
/**
* Gracefully fail.
*
* Output an HTML comment and log a warning the comment is meant to be
* innocuous to the end user.
*
* @param {number} num - Error code from `RUM_ISSUES`.
* @param {bool} [quite=false] - Be quiet about this failure.
*
* @see RUM_ISSUES
*/
function _gracefail(num, quiet) {
if (quiet) {
logger.debug(RUM_ISSUES[num])
} else {
logger.warn(RUM_ISSUES[num])
}
return '<!-- NREUM: (' + num + ') -->'
}
var browser_monitoring = config.browser_monitoring
// config.browser_monitoring should always exist, but we don't want the agent
// to bail here if something goes wrong
if (!browser_monitoring) return _gracefail(2)
/* Can control header generation with configuration this setting is only
* available in the newrelic.js config file, it is not ever set by the
* server.
*/
if (!browser_monitoring.enable) {
// It has been disabled by the user; no need to warn them about their own
// settings so fail quietly and gracefully.
return _gracefail(0, true)
}
var trans = this.agent.getTransaction()
// bail gracefully outside a transaction
if (!trans || trans.isIgnored()) return _gracefail(1)
var name = trans.getFullName()
/* If we're in an unnamed transaction, add a friendly warning this is to
* avoid people going crazy, trying to figure out why browser monitoring is
* not working when they're missing a transaction name.
*/
if (!name) return _gracefail(3)
var time = trans.timer.getDurationInMillis()
/*
* Only the first 13 chars of the license should be used for hashing with
* the transaction name.
*/
var key = config.license_key.substr(0, 13)
var appid = config.application_id
/* This is only going to work if the agent has successfully handshaked with
* the collector. If the networks is bad, or there is no license key set in
* newrelic.js, there will be no application_id set. We bail instead of
* outputting null/undefined configuration values.
*/
if (!appid) return _gracefail(4)
/* If there is no browser_key, the server has likely decided to disable
* browser monitoring.
*/
var licenseKey = browser_monitoring.browser_key
if (!licenseKey) return _gracefail(5)
/* If there is no agent_loader script, there is no point
* in setting the rum data
*/
var js_agent_loader = browser_monitoring.js_agent_loader
if (!js_agent_loader) return _gracefail(6)
/* If rum is enabled, but then later disabled on the server,
* this is the only parameter that gets updated.
*
* This condition should only be met if rum is disabled during
* the lifetime of an application, and it should be picked up
* on the next ForceRestart by the collector.
*/
var loader = browser_monitoring.loader
if (loader === 'none') return _gracefail(7)
// This hash gets written directly into the browser.
var rum_hash = {
agent: browser_monitoring.js_agent_file,
beacon: browser_monitoring.beacon,
errorBeacon: browser_monitoring.error_beacon,
licenseKey: licenseKey,
applicationID: appid,
applicationTime: time,
transactionName: hashes.obfuscateNameUsingKey(name, key),
queueTime: trans.queueTime,
ttGuid: trans.id,
// we don't use these parameters yet
agentToken: null
}
var attrs = Object.create(null)
const customAttrs = trans.trace.custom.get(ATTR_DEST.BROWSER_EVENT)
if (!properties.isEmpty(customAttrs)) {
attrs.u = customAttrs
}
const agentAttrs = trans.trace.attributes.get(ATTR_DEST.BROWSER_EVENT)
if (!properties.isEmpty(agentAttrs)) {
attrs.a = agentAttrs
}
if (!properties.isEmpty(attrs)) {
rum_hash.atts = hashes.obfuscateNameUsingKey(JSON.stringify(attrs), key)
}
// if debugging, do pretty format of JSON
var tabs = config.browser_monitoring.debug ? 2 : 0
var json = JSON.stringify(rum_hash, null, tabs)
// set nonce attribute if passed in options
var nonce = options && options.nonce ? 'nonce="' + options.nonce + '"' : ''
// the complete header to be written to the browser
var out = util.format(
RUM_STUB,
nonce,
json,
js_agent_loader
)
logger.trace('generating RUM header', out)
return out
}
/**
* @callback startSegmentCallback
* @param {function} cb
* The function to time with the created segment.
* @return {Promise=} Returns a promise if cb returns a promise.
*/
/**
* Wraps the given handler in a segment which may optionally be turned into a
* metric.
*
* @example
* newrelic.startSegment('mySegment', false, function handler() {
* // The returned promise here will signify the end of the segment.
* return myAsyncTask().then(myNextTask)
* })
*
* @param {string} name
* The name to give the new segment. This will also be the name of the metric.
*
* @param {bool} record
* Indicates if the segment should be recorded as a metric. Metrics will show
* up on the transaction breakdown table and server breakdown graph. Segments
* just show up in transaction traces.
*
* @param {startSegmentCallback} handler
* The function to track as a segment.
*
* @param {function} [callback]
* An optional callback for the handler. This will indicate the end of the
* timing if provided.
*
* @return {*} Returns the result of calling `handler`.
*/
API.prototype.startSegment = function startSegment(name, record, handler, callback) {
this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/startSegment'
).incrementCallCount()
// Check that we have usable arguments.
if (!name || typeof handler !== 'function') {
logger.warn('Name and handler function are both required for startSegment')
if (typeof handler === 'function') {
return handler(callback)
}
return
}
if (callback && typeof callback !== 'function') {
logger.warn('If using callback, it must be a function')
return handler(callback)
}
// Are we inside a transaction?
if (!this.shim.getActiveSegment()) {
logger.debug('startSegment(%j) called outside of a transaction, not recording.', name)
return handler(callback)
}
// Create the segment and call the handler.
var wrappedHandler = this.shim.record(handler, function handlerNamer(shim) {
return {
name: name,
recorder: record ? customRecorder : null,
callback: callback ? shim.FIRST : null,
promise: !callback
}
})
return wrappedHandler(callback)
}
/**
* Creates and starts a web transaction to record work done in
* the handle supplied. This transaction will run until the handle
* synchronously returns UNLESS:
* 1. The handle function returns a promise, where the end of the
* transaction will be tied to the end of the promise returned.
* 2. {@link API#getTransaction} is called in the handle, flagging the
* transaction as externally handled. In this case the transaction
* will be ended when {@link TransactionHandle#end} is called in the user's code.
*
* @example
* var newrelic = require('newrelic')
* newrelic.startWebTransaction('/some/url/path', function() {
* var transaction = newrelic.getTransaction()
* setTimeout(function() {
* // do some work
* transaction.end()
* }, 100)
* })
*
* @param {string} url
* The URL of the transaction. It is used to name and group related transactions in APM,
* so it should be a generic name and not iclude any variable parameters.
*
* @param {Function} handle
* Function that represents the transaction work.
*/
API.prototype.startWebTransaction = function startWebTransaction(url, handle) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/startWebTransaction'
)
metric.incrementCallCount()
if (typeof handle !== 'function') {
logger.warn('startWebTransaction called with a handle arg that is not a function')
return null
}
if (!url) {
logger.warn('startWebTransaction called without a url, transaction not started')
return handle()
}
logger.debug(
'starting web transaction %s (%s).',
url,
handle && handle.name
)
var shim = this.shim
var tracer = this.agent.tracer
var parent = tracer.getTransaction()
return tracer.transactionNestProxy('web', function startWebSegment() {
var tx = tracer.getTransaction()
if (!tx) {
return handle.apply(this, arguments)
}
if (tx === parent) {
logger.debug(
'not creating nested transaction %s using transaction %s',
url,
tx.id
)
return tracer.addSegment(url, null, null, true, handle)
}
logger.debug(
'creating web transaction %s (%s) with transaction id: %s',
url,
handle && handle.name,
tx.id
)
tx.nameState.setName(NAMES.CUSTOM, null, NAMES.ACTION_DELIMITER, url)
tx.url = url
tx.applyUserNamingRules(tx.url)
tx.baseSegment = tracer.createSegment(url, recordWeb)
tx.baseSegment.start()
var boundHandle = tracer.bindFunction(handle, tx.baseSegment)
var returnResult = boundHandle.call(this)
if (returnResult && shim.isPromise(returnResult)) {
returnResult = shim.interceptPromise(returnResult, tx.end.bind(tx))
} else if (!tx.handledExternally) {
logger.debug('Ending unhandled web transaction immediately.')
tx.end()
}
return returnResult
})()
}
API.prototype.startBackgroundTransaction = startBackgroundTransaction
/**
* Creates and starts a background transaction to record work done in
* the handle supplied. This transaction will run until the handle
* synchronously returns UNLESS:
* 1. The handle function returns a promise, where the end of the
* transaction will be tied to the end of the promise returned.
* 2. {@link API#getTransaction} is called in the handle, flagging the
* transaction as externally handled. In this case the transaction
* will be ended when {@link TransactionHandle#end} is called in the user's code.
*
* @example
* var newrelic = require('newrelic')
* newrelic.startBackgroundTransaction('Red October', 'Subs', function() {
* var transaction = newrelic.getTransaction()
* setTimeout(function() {
* // do some work
* transaction.end()
* }, 100)
* })
*
* @param {string} name
* The name of the transaction. It is used to name and group related
* transactions in APM, so it should be a generic name and not iclude any
* variable parameters.
*
* @param {string} [group]
* Optional, used for grouping background transactions in APM. For more
* information see:
* https://docs.newrelic.com/docs/apm/applications-menu/monitoring/transactions-page#txn-type-dropdown
*
* @param {Function} handle
* Function that represents the background work.
*
* @memberOf API#
*/
function startBackgroundTransaction(name, group, handle) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/startBackgroundTransaction'
)
metric.incrementCallCount()
if (handle === undefined && typeof group === 'function') {
handle = group
group = 'Nodejs'
}
if (typeof handle !== 'function') {
logger.warn('startBackgroundTransaction called with a handle that is not a function')
return null
}
if (!name) {
logger.warn('startBackgroundTransaction called without a name')
return handle()
}
logger.debug(
'starting background transaction %s:%s (%s)',
name,
group,
handle && handle.name
)
var tracer = this.agent.tracer
var shim = this.shim
var txName = group + '/' + name
var parent = tracer.getTransaction()
return tracer.transactionNestProxy('bg', function startBackgroundSegment() {
var tx = tracer.getTransaction()
if (!tx) {
return handle.apply(this, arguments)
}
if (tx === parent) {
logger.debug(
'not creating nested transaction %s using transaction %s',
txName,
tx.id
)
return tracer.addSegment(txName, null, null, true, handle)
}
logger.debug(
'creating background transaction %s:%s (%s) with transaction id: %s',
name,
group,
handle && handle.name,
tx.id
)
tx._partialName = txName
tx.baseSegment = tracer.createSegment(name, recordBackground)
tx.baseSegment.partialName = group
tx.baseSegment.start()
var boundHandle = tracer.bindFunction(handle, tx.baseSegment)
var returnResult = boundHandle.call(this)
if (returnResult && shim.isPromise(returnResult)) {
returnResult = shim.interceptPromise(returnResult, tx.end.bind(tx))
} else if (!tx.handledExternally) {
logger.debug('Ending unhandled background transaction immediately.')
tx.end()
}
return returnResult
})()
}
/**
* End the current web or background custom transaction. This method requires being in
* the correct transaction context when called.
*/
API.prototype.endTransaction = function endTransaction() {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/endTransaction'
)
metric.incrementCallCount()
var tracer = this.agent.tracer
var tx = tracer.getTransaction()
if (tx) {
if (tx.baseSegment) {
if (tx.type === 'web') {
tx.finalizeNameFromUri(tx.url, 0)
}
tx.baseSegment.end()
}
tx.end()
logger.debug('ended transaction with id: %s and name: %s', tx.id, tx.name)
} else {
logger.debug('endTransaction() called while not in a transaction.')
}
}
/**
* Record a custom metric, usually associated with a particular duration.
* The `name` must be a string following standard metric naming rules. The `value` will
* usually be a number, but it can also be an object.
* * When `value` is a numeric value, it should represent the magnitude of a measurement
* associated with an event; for example, the duration for a particular method call.
* * When `value` is an object, it must contain count, total, min, max, and sumOfSquares
* keys, all with number values. This form is useful to aggregate metrics on your own
* and report them periodically; for example, from a setInterval. These values will
* be aggregated with any previously collected values for the same metric. The names
* of these keys match the names of the keys used by the platform API.
*
* @param {string} name The name of the metric.
* @param {number|object} value
*/
API.prototype.recordMetric = function recordMetric(name, value) {
const supportMetric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/recordMetric'
)
supportMetric.incrementCallCount()
if (typeof name !== 'string') {
logger.warn('Metric name must be a string')
return
}
const metricName = NAMES.CUSTOM + NAMES.ACTION_DELIMITER + name
const metric = this.agent.metrics.getOrCreateMetric(metricName)
if (typeof value === 'number') {
metric.recordValue(value)
return
}
if (typeof value !== 'object') {
logger.warn('Metric value must be either a number, or a metric object')
return
}
const stats = Object.create(null)
const required = ['count', 'total', 'min', 'max', 'sumOfSquares']
const keyMap = {count: 'callCount'}
for (let i = 0, l = required.length; i < l; ++i) {
if (typeof value[required[i]] !== 'number') {
logger.warn('Metric object must include %s as a number', required[i])
return
}
const key = keyMap[required[i]] || required[i]
stats[key] = value[required[i]]
}
if (typeof value.totalExclusive === 'number') {
stats.totalExclusive = value.totalExclusive
} else {
stats.totalExclusive = value.total
}
metric.merge(stats)
}
/**
* Create or update a custom metric that acts as a simple counter.
* The count of the given metric will be incremented by the specified amount,
* defaulting to 1.
*
* @param {string} name The name of the metric.
* @param {number} [value] The amount that the count of the metric should be incremented
* by. Defaults to 1.
*/
API.prototype.incrementMetric = function incrementMetric(name, value) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/incrementMetric'
)
metric.incrementCallCount()
if (!value && value !== 0) {
value = 1
}
if (typeof value !== 'number' || value % 1 !== 0) {
logger.warn('Metric Increment value must be an integer')
return
}
this.recordMetric(name, {
count: value,
total: 0,
min: 0,
max: 0,
sumOfSquares: 0
})
}
/**
* Record custom event data which can be queried in New Relic Insights.
*
* @param {string} eventType The name of the event. It must be an alphanumeric string
* less than 255 characters.
* @param {object} attributes Object of key and value pairs. The keys must be shorter
* than 255 characters, and the values must be string, number,
* or boolean.
*/
API.prototype.recordCustomEvent = function recordCustomEvent(eventType, attributes) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/recordCustomEvent'
)
metric.incrementCallCount()
// If high security mode is on, custom events are disabled.
if (this.agent.config.high_security) {
logger.warnOnce(
"Custom Event",
"Custom events are disabled by high security mode."
)
return false
} else if (!this.agent.config.api.custom_events_enabled) {
logger.debug(
"Config.api.custom_events_enabled set to false, not collecting value"
)
return false
}
if (!this.agent.config.custom_insights_events.enabled) {
return
}
// Check all the arguments before bailing to give maximum information in a
// single invocation.
var fail = false
if (!eventType || typeof eventType !== 'string') {
logger.warn(
'recordCustomEvent requires a string for its first argument, got %s (%s)',
stringify(eventType),
typeof eventType
)
fail = true
} else if (!CUSTOM_EVENT_TYPE_REGEX.test(eventType)) {
logger.warn(
'recordCustomEvent eventType of %s is invalid, it must match /%s/',
eventType,
CUSTOM_EVENT_TYPE_REGEX.source
)
fail = true
} else if (eventType.length > 255) {
logger.warn(
'recordCustomEvent eventType must have a length less than 256, got %s (%s)',
eventType,
eventType.length
)
fail = true
}
// If they don't pass an attributes object, or the attributes argument is not
// an object, or if it is an object and but is actually an array, log a
// warning and set the fail bit.
if (!attributes || typeof attributes !== 'object' || Array.isArray(attributes)) {
logger.warn(
'recordCustomEvent requires an object for its second argument, got %s (%s)',
stringify(attributes),
typeof attributes
)
fail = true
} else if (_checkKeyLength(attributes, 255)) {
fail = true
}
if (fail) {
return
}
// Filter all object type valued attributes out
const filteredAttributes = _filterAttributes(attributes, `${eventType} custom event`)
var instrinics = {
type: eventType,
timestamp: Date.now()
}
var tx = this.agent.getTransaction()
var priority = tx && tx.priority || Math.random()
this.agent.customEventAggregator.add([instrinics, filteredAttributes], priority)
}
/**
* Registers an instrumentation function.
*
* - `newrelic.instrument(moduleName, onRequire [,onError])`
* - `newrelic.instrument(options)`
*
* @param {object} options
* The options for this custom instrumentation.
*
* @param {string} options.moduleName
* The module name given to require to load the module
*
* @param {function} options.onRequire
* The function to call when the module is required
*
* @param {function} [options.onError]
* If provided, should `onRequire` throw an error, the error will be passed to
* this function.
*/
API.prototype.instrument = function instrument(moduleName, onRequire, onError) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/instrument'
)
metric.incrementCallCount()
var opts = moduleName
if (typeof opts === 'string') {
opts = {
moduleName: moduleName,
onRequire: onRequire,
onError: onError
}
}
opts.type = MODULE_TYPE.GENERIC
shimmer.registerInstrumentation(opts)
}
/**
* Registers an instrumentation function.
*
* - `newrelic.instrumentConglomerate(moduleName, onRequire [, onError])`
* - `newrelic.isntrumentConglomerate(options)`
*
* @param {object} options
* The options for this custom instrumentation.
*
* @param {string} options.moduleName
* The module name given to require to load the module
*
* @param {function} options.onRequire
* The function to call when the module is required
*
* @param {function} [options.onError]
* If provided, should `onRequire` throw an error, the error will be passed to
* this function.
*/
API.prototype.instrumentConglomerate =
function instrumentConglomerate(moduleName, onRequire, onError) {
this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/instrumentConglomerate'
).incrementCallCount()
let opts = moduleName
if (typeof opts === 'string') {
opts = {moduleName, onRequire, onError}
}
opts.type = MODULE_TYPE.CONGLOMERATE
shimmer.registerInstrumentation(opts)
}
/**
* Registers an instrumentation function.
*
* - `newrelic.instrumentDatastore(moduleName, onRequire [,onError])`
* - `newrelic.instrumentDatastore(options)`
*
* @param {object} options
* The options for this custom instrumentation.
*
* @param {string} options.moduleName
* The module name given to require to load the module
*
* @param {function} options.onRequire
* The function to call when the module is required
*
* @param {function} [options.onError]
* If provided, should `onRequire` throw an error, the error will be passed to
* this function.
*/
API.prototype.instrumentDatastore =
function instrumentDatastore(moduleName, onRequire, onError) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/instrumentDatastore'
)
metric.incrementCallCount()
var opts = moduleName
if (typeof opts === 'string') {
opts = {
moduleName: moduleName,
onRequire: onRequire,
onError: onError
}
}
opts.type = MODULE_TYPE.DATASTORE
shimmer.registerInstrumentation(opts)
}
/**
* Applies an instrumentation to an already loaded module.
*
* // oh no, express was loaded before newrelic
* const express = require('express')
* const newrelic = require('newrelic')
*
* // phew, we can use instrumentLoadedModule to make
* // sure express is still instrumented
* newrelic.instrumentLoadedModule('express', express)
*
* @param {string} moduleName
* The module's name/identifier. Will be normalized
* into an instrumentation key.
*
* @param {object} module
* The actual module object or function we're instrumenting
*/
API.prototype.instrumentLoadedModule =
function instrumentLoadedModule(moduleName, module) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/instrumentLoadedModule'
)
metric.incrementCallCount()
try {
const instrumentationName = shimmer.getInstrumentationNameFromModuleName(moduleName)
if (!shimmer.registeredInstrumentations[instrumentationName]) {
logger.warn("No instrumentation registered for '%s'.", instrumentationName)
return false
}
const instrumentation = shimmer.registeredInstrumentations[instrumentationName]
if (!instrumentation.onRequire) {
logger.warn("No onRequire function registered for '%s'.", instrumentationName)
return false
}
const resolvedName = require.resolve(moduleName)
const shim = shims.createShimFromType(
instrumentation.type,
this.agent,
moduleName,
resolvedName
)
instrumentation.onRequire(shim, module, moduleName)
return true
} catch (error) {
logger.error(
'instrumentLoadedModule encountered an error, module not instrumentend: %s',
error
)
}
}
/**
* Registers an instrumentation function.
*
* - `newrelic.instrumentWebframework(moduleName, onRequire [,onError])`
* - `newrelic.instrumentWebframework(options)`
*
* @param {object} options
* The options for this custom instrumentation.
*
* @param {string} options.moduleName
* The module name given to require to load the module
*
* @param {function} options.onRequire
* The function to call when the module is required
*
* @param {function} [options.onError]
* If provided, should `onRequire` throw an error, the error will be passed to
* this function.
*/
API.prototype.instrumentWebframework =
function instrumentWebframework(moduleName, onRequire, onError) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/instrumentWebframework'
)
metric.incrementCallCount()
var opts = moduleName
if (typeof opts === 'string') {
opts = {
moduleName: moduleName,
onRequire: onRequire,
onError: onError
}
}
opts.type = MODULE_TYPE.WEB_FRAMEWORK
shimmer.registerInstrumentation(opts)
}
/**
* Registers an instrumentation function for instrumenting message brokers.
*
* - `newrelic.instrumentMessages(moduleName, onRequire [,onError])`
* - `newrelic.instrumentMessages(options)`
*
* @param {object} options
* The options for this custom instrumentation.
*
* @param {string} options.moduleName
* The module name given to require to load the module
*
* @param {function} options.onRequire
* The function to call when the module is required
*
* @param {function} [options.onError]
* If provided, should `onRequire` throw an error, the error will be passed to
* this function.
*/
API.prototype.instrumentMessages =
function instrumentMessages(moduleName, onRequire, onError) {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/instrumentMessages'
)
metric.incrementCallCount()
var opts = moduleName
if (typeof opts === 'string') {
opts = {
moduleName: moduleName,
onRequire: onRequire,
onError: onError
}
}
opts.type = MODULE_TYPE.MESSAGE
shimmer.registerInstrumentation(opts)
}
/**
* Returns the current trace and span id.
*
* @returns {*} The object containing the current trace and span ids
*/
API.prototype.getTraceMetadata = function getTraceMetadata() {
var metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/getTraceMetadata'
)
metric.incrementCallCount()
const metadata = {}
const segment = this.agent.tracer.getSegment()
if (!segment) {
logger.debug("No transaction found when calling API#getTraceMetadata")
} else if (!this.agent.config.distributed_tracing.enabled) {
logger.debug("Distributed tracing disabled when calling API#getTraceMetadata")
} else {
metadata.traceId = segment.transaction.traceId
const spanId = segment.getSpanId()
if (spanId) {
metadata.spanId = spanId
}
}
return metadata
}
/**
* Shuts down the agent.
*
* @param {object} [options]
* Object with shut down options.
*
* @param {boolean} [options.collectPendingData=false]
* If true, the agent will send any pending data to the collector before
* shutting down.
*
* @param {number} [options.timeout=0]
* Time in milliseconds to wait before shutting down.
*
* @param {boolean} [options.waitForIdle=false]
* If true, the agent will not shut down until there are no active transactions.
*
* @param {function} [callback]
* Callback function that runs when agent stops.
*/
API.prototype.shutdown = function shutdown(options, cb) {
this.agent.metrics.getOrCreateMetric(`${NAMES.SUPPORTABILITY.API}/shutdown`)
.incrementCallCount()
let callback = cb
if (typeof options === 'function') {
// shutdown(cb)
callback = options
options = {}
} else if (typeof callback !== 'function') {
// shutdown([options])
callback = () => {}
}
if (!options) {
// shutdown(null, cb)
options = {}
}
_doShutdown(this, options, callback)
}
function _doShutdown(api, options, callback) {
const agent = api.agent
// If we need to wait for idle and there are currently active transactions,
// listen for transactions ending and check if we're ready to go.
if (options.waitForIdle && agent.activeTransactions) {
options.waitForIdle = false // To prevent recursive waiting.
agent.on('transactionFinished', function onTransactionFinished() {
if (agent.activeTransactions === 0) {
setImmediate(_doShutdown, api, options, callback)
}
})
return
}
function afterHarvest(error) {
if (error) {
logger.error(
error,
'An error occurred while running last harvest before shutdown.'
)
}
agent.stop(callback)
}
if (options.collectPendingData && agent._state !== 'started') {
if (typeof options.timeout === 'number') {
setTimeout(function shutdownTimeout() {
agent.stop(callback)
}, options.timeout).unref()
} else if (options.timeout) {
logger.warn(
'options.timeout should be of type "number". Got %s',
typeof options.timeout
)
}
agent.on('started', function shutdownHarvest() {
agent.forceHarvestAll(afterHarvest)
})
agent.on('errored', function logShutdownError(error) {
agent.stop(callback)
if (error) {
logger.error(
error,
'The agent encountered an error after calling shutdown.'
)
}
})
} else if (options.collectPendingData) {
agent.forceHarvestAll(afterHarvest)
} else {
agent.stop(callback)
}
}
function _checkKeyLength(object, maxLength) {
var keys = Object.keys(object)
var badKey = false
var len = keys.length
var key = '' // init to string because gotta go fast
for (var i = 0; i < len; i++) {
key = keys[i]
if (key.length > maxLength) {
logger.warn(
'recordCustomEvent requires keys to be less than 256 chars got %s (%s)',
key,
key.length
)
badKey = true