elastic-apm-node
Version:
The official Elastic APM agent for Node.js
924 lines (815 loc) • 29.9 kB
JavaScript
/*
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*/
'use strict';
const http = require('http');
const path = require('path');
const isError = require('core-util-is').isError;
const Filters = require('object-filter-sequence');
const { agentActivationMethodFromStartStack } = require('./activation-method');
const {
CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS,
CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES,
} = require('./constants');
const config = require('./config/config');
const connect = require('./middleware/connect');
const constants = require('./constants');
const errors = require('./errors');
const { InflightEventSet } = require('./InflightEventSet');
const { Instrumentation } = require('./instrumentation');
const { elasticApmAwsLambda } = require('./lambda');
const Metrics = require('./metrics');
const parsers = require('./parsers');
const symbols = require('./symbols');
const { frameCacheStats, initStackTraceCollection } = require('./stacktraces');
const Span = require('./instrumentation/span');
const Transaction = require('./instrumentation/transaction');
const {
isOTelMetricsFeatSupported,
createOTelMeterProvider,
} = require('./opentelemetry-metrics');
const { createApmClient } = require('./apm-client/apm-client');
const IncomingMessage = http.IncomingMessage;
const ServerResponse = http.ServerResponse;
const version = require('../package').version;
// ---- Agent
module.exports = Agent;
function Agent() {
// Early configuration to ensure `agent.logger` works before `agent.start()`.
this.logger = config.configLogger();
// Get an initial pre-.start() configuration of agent defaults. This is a
// crutch for Agent APIs that depend on `agent._conf`.
this._conf = config.initialConfig(this.logger);
this._httpClient = null;
this._uncaughtExceptionListener = null;
this._inflightEvents = new InflightEventSet();
this._instrumentation = new Instrumentation(this);
this._metrics = new Metrics(this);
this._otelMeterProvider = null;
this._errorFilters = new Filters();
this._transactionFilters = new Filters();
this._spanFilters = new Filters();
this._apmClient = null;
this.lambda = elasticApmAwsLambda(this);
this.middleware = { connect: connect.bind(this) };
}
Object.defineProperty(Agent.prototype, 'currentTransaction', {
get() {
return this._instrumentation.currTransaction();
},
});
Object.defineProperty(Agent.prototype, 'currentSpan', {
get() {
return this._instrumentation.currSpan();
},
});
Object.defineProperty(Agent.prototype, 'currentTraceparent', {
get() {
const current =
this._instrumentation.currSpan() ||
this._instrumentation.currTransaction();
return current ? current.traceparent : null;
},
});
Object.defineProperty(Agent.prototype, 'currentTraceIds', {
get() {
return this._instrumentation.ids();
},
});
// Destroy this agent. This prevents any new agent processing, communication
// with APM server, and resets changed global state *as much as is possible*.
//
// In the typical uses case -- a singleton Agent running for the full process
// lifetime -- it is *not* necessary to call `agent.destroy()`. It is used
// for some testing.
//
// Limitations:
// - Patching/wrapping of functions for instrumentation *is* undone, but
// references to the wrapped versions can remain.
// - There may be in-flight tasks (in ins.addEndedSpan() and
// agent.captureError() for example) that will complete after this destroy
// completes. They should have no impact other than CPU/resource use.
Agent.prototype.destroy = async function () {
if (this._otelMeterProvider) {
// This will call `Agent#flush()`.
try {
await this._otelMeterProvider.shutdown({ timeoutMillis: 1000 });
} catch (reason) {
this.logger.warn('failed to shutdown OTel MeterProvider:', reason);
}
this._otelMeterProvider = null;
}
if (this._apmClient && this._apmClient.destroy) {
this._apmClient.destroy();
}
// So in-flight tasks in ins.addEndedSpan() and agent.captureError() do
// not use the destroyed transport.
this._apmClient = null;
// So in-flight tasks do not call user-added filters after the agent has
// been destroyed.
this._errorFilters = new Filters();
this._transactionFilters = new Filters();
this._spanFilters = new Filters();
if (this._uncaughtExceptionListener) {
process.removeListener(
'uncaughtException',
this._uncaughtExceptionListener,
);
}
this._metrics.stop();
this._instrumentation.stop();
// Allow a new Agent instance to `.start()`. Typically this is only relevant
// for tests that may use multiple Agent instances in a single test process.
global[symbols.agentInitialized] = null;
if (
this._origStackTraceLimit &&
Error.stackTraceLimit !== this._origStackTraceLimit
) {
Error.stackTraceLimit = this._origStackTraceLimit;
}
};
// These are metrics about the agent itself -- separate from the metrics
// gathered on behalf of the using app and sent to APM server. Currently these
// are only useful for internal debugging of the APM agent itself.
//
// **These stats are NOT a promised interface.**
Agent.prototype._getStats = function () {
const stats = {
frameCache: frameCacheStats,
};
if (
this._instrumentation._runCtxMgr &&
this._instrumentation._runCtxMgr._runContextFromAsyncId
) {
stats.runContextFromAsyncIdSize =
this._instrumentation._runCtxMgr._runContextFromAsyncId.size;
}
if (this._apmClient && typeof this._apmClient._getStats === 'function') {
stats.apmclient = this._apmClient._getStats();
}
return stats;
};
Agent.prototype.addPatch = function (modules, handler) {
return this._instrumentation.addPatch.apply(this._instrumentation, arguments);
};
Agent.prototype.removePatch = function (modules, handler) {
return this._instrumentation.removePatch.apply(
this._instrumentation,
arguments,
);
};
Agent.prototype.clearPatches = function (modules) {
return this._instrumentation.clearPatches.apply(
this._instrumentation,
arguments,
);
};
Agent.prototype.startTransaction = function (
name,
type,
{ startTime, childOf } = {},
) {
return this._instrumentation.startTransaction.apply(
this._instrumentation,
arguments,
);
};
Agent.prototype.endTransaction = function (result, endTime) {
return this._instrumentation.endTransaction.apply(
this._instrumentation,
arguments,
);
};
Agent.prototype.setTransactionName = function (name) {
return this._instrumentation.setTransactionName.apply(
this._instrumentation,
arguments,
);
};
/**
* Sets outcome value for current transaction
*
* The setOutcome method allows users to override the default
* outcome handling in the agent and set their own value.
*
* @param {string} outcome must be one of `failure`, `success`, or `unknown`
*/
Agent.prototype.setTransactionOutcome = function (outcome) {
return this._instrumentation.setTransactionOutcome.apply(
this._instrumentation,
arguments,
);
};
Agent.prototype.startSpan = function (
name,
type,
subtype,
action,
{ startTime, childOf, exitSpan } = {},
) {
return this._instrumentation.startSpan.apply(
this._instrumentation,
arguments,
);
};
/**
* Sets outcome value for current active span
*
* The setOutcome method allows users to override the default
* outcome handling in the agent and set their own value.
*
* @param {string} outcome must be one of `failure`, `success`, or `unknown`
*/
Agent.prototype.setSpanOutcome = function (outcome) {
return this._instrumentation.setSpanOutcome.apply(
this._instrumentation,
arguments,
);
};
Agent.prototype._config = function (opts) {
this._conf = config.createConfig(opts, this.logger);
this.logger = this._conf.logger;
};
Agent.prototype.isStarted = function () {
return global[symbols.agentInitialized];
};
Agent.prototype.start = function (opts) {
if (this.isStarted()) {
throw new Error('Do not call .start() more than once');
}
global[symbols.agentInitialized] = true;
this._config(opts);
if (!this._conf.active) {
this.logger.debug('Elastic APM agent disabled (`active` is false)');
return this;
}
// Log preamble showing agent, environment and config relevant info
const preambleData = this._conf.loggingPreambleData;
const isPreviewVersion = version.indexOf('-') !== -1;
const startStack = {};
if (this._conf.active && this._conf.serviceName) {
this._origStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 20; // require more than default 10 for `agentActivationMethodFromStartStack()`
Error.captureStackTrace(startStack);
Error.stackTraceLimit = this._origStackTraceLimit;
this._agentActivationMethod = agentActivationMethodFromStartStack(
startStack,
this.logger,
);
preambleData.activationMethod = this._agentActivationMethod;
if (this._conf.logLevel === 'trace') {
// Attempt to load package.json from process.argv.
let pkg = null;
try {
var basedir = path.dirname(process.argv[1] || '.');
pkg = require(path.join(basedir, 'package.json'));
} catch (e) {}
// Add stack & dependencies for extra information
preambleData.dependencies = pkg
? pkg.dependencies
: '<could not determine>';
preambleData.startTrace = startStack.stack.split(/\n */).slice(1);
}
}
this.logger.info(preambleData, 'Elastic APM Node.js Agent v%s', version);
if (isPreviewVersion) {
this.logger.warn(
'Version %s is a pre-release and not intended for use in production environments',
version,
);
}
if (!this._conf.serverUrl) {
this.logger.error(
'Elastic APM is incorrectly configured: Invalid serverUrl (APM will be disabled)',
);
this._conf.active = false;
return this;
}
if (!this._conf.serviceName) {
this.logger.error(
'Elastic APM is incorrectly configured: Missing serviceName (APM will be disabled)',
);
this._conf.active = false;
return this;
}
initStackTraceCollection();
this._apmClient = createApmClient(this._conf, this);
let runContextClass;
if (this._conf.opentelemetryBridgeEnabled) {
const {
setupOTelBridge,
OTelBridgeRunContext,
} = require('./opentelemetry-bridge');
runContextClass = OTelBridgeRunContext;
setupOTelBridge(this);
}
this._instrumentation.start(runContextClass);
if (this._isMetricsEnabled()) {
this._metrics.start();
}
Error.stackTraceLimit = this._conf.stackTraceLimit;
if (this._conf.captureExceptions) this.handleUncaughtExceptions();
return this;
};
Agent.prototype._isMetricsEnabled = function () {
return this._conf.metricsInterval !== 0 && !this._conf.contextPropagationOnly;
};
/**
* Lazily create a singleton OTel MeterProvider that periodically exports
* metrics to APM server. This may return null if the MeterProvider is
* unsupported for this node version, metrics are disabled, etc.
*
* @returns {import('@opentelemetry/api').MeterProvider | null}
*/
Agent.prototype._getOrCreateOTelMeterProvider = function () {
if (this._otelMeterProvider) {
return this._otelMeterProvider;
}
if (!this._isMetricsEnabled()) {
return null;
}
if (!isOTelMetricsFeatSupported) {
return null;
}
this.logger.trace('create Elastic APM MeterProvider for @opentelemetry/api');
this._otelMeterProvider = createOTelMeterProvider(this);
return this._otelMeterProvider;
};
Agent.prototype.getServiceName = function () {
return this._conf ? this._conf.serviceName : undefined;
};
Agent.prototype.getServiceVersion = function () {
return this._conf ? this._conf.serviceVersion : undefined;
};
Agent.prototype.getServiceEnvironment = function () {
return this._conf ? this._conf.environment : undefined;
};
Agent.prototype.getServiceNodeName = function () {
return this._conf ? this._conf.serviceNodeName : undefined;
};
Agent.prototype.setFramework = function ({ name, version, overwrite = true }) {
if (!this._apmClient || !this._conf) {
return;
}
const conf = {};
if (name && (overwrite || !this._conf.frameworkName))
this._conf.frameworkName = conf.frameworkName = name;
if (version && (overwrite || !this._conf.frameworkVersion))
this._conf.frameworkVersion = conf.frameworkVersion = version;
this._apmClient.config(conf);
};
Agent.prototype.setUserContext = function (context) {
var trans = this._instrumentation.currTransaction();
if (!trans) return false;
trans.setUserContext(context);
return true;
};
Agent.prototype.setCustomContext = function (context) {
var trans = this._instrumentation.currTransaction();
if (!trans) return false;
trans.setCustomContext(context);
return true;
};
Agent.prototype.setGlobalLabel = function (key, value) {
if (!this._conf.globalLabels) this._conf.globalLabels = [];
const foundPos = this._conf.globalLabels.findIndex(([name]) => key === name);
if (foundPos > -1) {
this._conf.globalLabels[foundPos][1] = value;
} else {
this._conf.globalLabels.push([key, value]);
}
if (!this._apmClient) {
this.logger.warn('cannot setGlobalLabel on inactive or unconfigured agent');
return;
}
this._apmClient.config({
globalLabels: this._conf.globalLabels.reduce((acc, [k, v]) => {
acc[k] = v;
return acc;
}, {}),
});
};
Agent.prototype.setLabel = function (key, value, stringify) {
var trans = this._instrumentation.currTransaction();
if (!trans) return false;
return trans.setLabel(key, value, stringify);
};
Agent.prototype.addLabels = function (labels, stringify) {
var trans = this._instrumentation.currTransaction();
if (!trans) return false;
return trans.addLabels(labels, stringify);
};
Agent.prototype.addFilter = function (fn) {
this.addErrorFilter(fn);
this.addTransactionFilter(fn);
this.addSpanFilter(fn);
// Note: This does *not* add to *metadata* filters, partly for backward
// compat -- the structure of metadata objects is quite different and could
// break existing filters -- and partly because that different structure
// means it makes less sense to re-use the same function to filter them.
};
Agent.prototype.addErrorFilter = function (fn) {
if (typeof fn !== 'function') {
this.logger.error("Can't add filter of type %s", typeof fn);
return;
}
this._errorFilters.push(fn);
};
Agent.prototype.addTransactionFilter = function (fn) {
if (typeof fn !== 'function') {
this.logger.error("Can't add filter of type %s", typeof fn);
return;
}
this._transactionFilters.push(fn);
};
Agent.prototype.addSpanFilter = function (fn) {
if (typeof fn !== 'function') {
this.logger.error("Can't add filter of type %s", typeof fn);
return;
}
this._spanFilters.push(fn);
};
Agent.prototype.addMetadataFilter = function (fn) {
if (typeof fn !== 'function') {
this.logger.error("Can't add filter of type %s", typeof fn);
return;
} else if (!this._apmClient) {
this.logger.error(
'cannot add metadata filter to inactive or unconfigured agent (agent has no transport)',
);
return;
} else if (typeof this._apmClient.addMetadataFilter !== 'function') {
// Graceful failure if unexpectedly using a too-old APM client.
this.logger.error(
'cannot add metadata filter: transport does not support addMetadataFilter',
);
return;
}
// Metadata filters are handled by the APM client, where metadata is
// processed.
this._apmClient.addMetadataFilter(fn);
};
const EMPTY_OPTS = {};
// Capture an APM server "error" event for the given `err` and send it to APM
// server.
//
// Usage:
// captureError(err, opts, cb)
// captureError(err, opts)
// captureError(err, cb)
//
// where:
// - `err` is an Error instance, or a string message, or a "parameterized string
// message" object, e.g.:
// {
// message: "this is my message template: %d %s"},
// params: [ 42, "another param" ]
// }
// - `opts` can include any of the following (all optional):
// - `opts.timestamp` - Milliseconds since the Unix epoch. Defaults to now.
// - `opts.user` - Object to add to `error.context.user`.
// - `opts.tags` - Deprecated, use `opts.labels`. Object to add to
// `error.context.labels`.
// - `opts.labels` - Object to add to `error.context.labels`.
// - `opts.custom` - Object to add to `error.context.custom`.
// - `opts.message` - If `err` is an Error instance, this string is added to
// `error.log.message` (unless it matches err.message).
// - `opts.request` - HTTP request (node `IncomingMessage` instance) to use
// for `error.context.request`. If not given, a `req` on the parent
// transaction (see `opts.parent` below) will be used.
// - `opts.response` - HTTP response (node `ServerResponse` instance) to use
// for `error.context.response`. If not given, a `res` on the parent
// transaction (see `opts.parent` below) will be used.
// - `opts.handled` - Boolean indicating if this exception was handled by
// application code. Default true. Setting to `false` also results in the
// error being flushed to APM server as soon as it is processed.
// - `opts.captureAttributes` - Boolean. Default true. Set to false to *not*
// include properties of `err` as attributes on the APM error event.
// - `opts.skipOutcome` - Boolean. Default false. Set to true to not have
// this captured error set `<currentSpan>.outcome = failure`.
// - `opts.parent` - A Transaction or Span instance to make the parent of
// this error. If not given (undefined), then the current span or
// transaction will be used. If `null` is given, then no span or transaction
// will be used.
// - `opts.exceptionType` - A string to use for `error.exception.type`. By
// default this is `err.name`. This option is only relevant if `err` is an
// Error instance.
// - `cb` is a callback `function (captureErr, apmErrorIdString)`. If provided,
// the error will be flushed to APM server as soon as it is processed, and
// `cb` will be called when that send is complete.
Agent.prototype.captureError = function (err, opts, cb) {
if (typeof opts === 'function') {
cb = opts;
opts = EMPTY_OPTS;
} else if (!opts) {
opts = EMPTY_OPTS;
}
const id = errors.generateErrorId();
if (!this.isStarted()) {
if (cb) {
cb(new Error('cannot capture error before agent is started'), id);
}
return;
}
// Avoid unneeded error/stack processing if only propagating trace-context.
if (this._conf.contextPropagationOnly) {
if (cb) {
process.nextTick(cb, null, id);
}
return;
}
const agent = this;
let callSiteLoc = null;
const errIsError = isError(err);
const handled = opts.handled !== false; // default true
const shouldCaptureAttributes = opts.captureAttributes !== false; // default true
const skipOutcome = Boolean(opts.skipOutcome);
const timestampUs = opts.timestamp
? Math.floor(opts.timestamp * 1000)
: Date.now() * 1000;
// Determine transaction/span context to associate with this error.
let parent;
let span;
let trans;
if (opts.parent === undefined) {
parent =
this._instrumentation.currSpan() ||
this._instrumentation.currTransaction();
} else if (opts.parent === null) {
parent = null;
} else {
parent = opts.parent;
}
if (parent instanceof Transaction) {
span = null;
trans = parent;
} else if (parent instanceof Span) {
span = parent;
trans = parent.transaction;
}
const traceContext = (span || trans || {})._context;
const req =
opts.request instanceof IncomingMessage ? opts.request : trans && trans.req;
const res =
opts.response instanceof ServerResponse
? opts.response
: trans && trans.res;
// As an added feature, for *some* cases, we capture a stacktrace at the point
// this `captureError` was called. This is added to `error.log.stacktrace`.
if (
handled &&
(agent._conf.captureErrorLogStackTraces ===
CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS ||
(!errIsError &&
agent._conf.captureErrorLogStackTraces ===
CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES))
) {
callSiteLoc = {};
Error.captureStackTrace(callSiteLoc, Agent.prototype.captureError);
}
if (span && !skipOutcome) {
span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE);
}
// Note this error as an "inflight" event. See Agent#flush().
const inflightEvents = this._inflightEvents;
inflightEvents.add(id);
// Move the remaining captureError processing to a later tick because:
// 1. This allows the calling code to continue processing. For example, for
// Express instrumentation this can significantly improve latency in
// the app's endpoints because the response does not proceed until the
// error handlers return.
// 2. Gathering `error.context.response` in the same tick results in data
// for a response that hasn't yet completed (no headers, unset status_code,
// etc.).
setImmediate(() => {
// Gather `error.context.*`.
const errorContext = {
user: Object.assign(
{},
req && parsers.getUserContextFromRequest(req),
trans && trans._user,
opts.user,
),
tags: Object.assign({}, trans && trans._labels, opts.tags, opts.labels),
custom: Object.assign({}, trans && trans._custom, opts.custom),
};
if (req) {
errorContext.request = parsers.getContextFromRequest(
req,
agent._conf,
'errors',
);
}
if (res) {
errorContext.response = parsers.getContextFromResponse(
res,
agent._conf,
true,
);
}
errors.createAPMError(
{
log: agent.logger,
id,
exception: errIsError ? err : null,
logMessage: errIsError ? null : err,
shouldCaptureAttributes,
timestampUs,
handled,
callSiteLoc,
message: opts.message,
sourceLinesAppFrames: agent._conf.sourceLinesErrorAppFrames,
sourceLinesLibraryFrames: agent._conf.sourceLinesErrorLibraryFrames,
trans,
traceContext,
errorContext,
exceptionType: opts.exceptionType,
},
function filterAndSendError(_err, apmError) {
// _err is always null from createAPMError.
apmError = agent._errorFilters.process(apmError);
if (!apmError) {
agent.logger.debug('error ignored by filter %o', { id });
inflightEvents.delete(id);
if (cb) {
cb(null, id);
}
return;
}
if (agent._apmClient) {
agent.logger.debug('Sending error to Elastic APM: %o', { id });
agent._apmClient.sendError(apmError);
inflightEvents.delete(id);
if (!handled || cb) {
// Immediately flush *unhandled* errors -- those from
// `uncaughtException` -- on the assumption that the process may
// soon crash. Also flush when a `cb` is provided.
agent.flush(function (flushErr) {
if (cb) {
cb(flushErr, id);
}
});
}
} else {
inflightEvents.delete(id);
if (cb) {
cb(new Error('cannot send error: missing transport'), id);
}
}
},
);
});
};
// The optional callback will be called with the error object after the error
// have been sent to the intake API. If no callback have been provided we will
// automatically terminate the process, so if you provide a callback you must
// remember to terminate the process manually.
Agent.prototype.handleUncaughtExceptions = function (cb) {
var agent = this;
if (this._uncaughtExceptionListener) {
process.removeListener(
'uncaughtException',
this._uncaughtExceptionListener,
);
}
this._uncaughtExceptionListener = function (err) {
agent.logger.debug({ err }, 'Elastic APM caught unhandled exception');
// This somewhat mimics Node.js's core printing of the error to stderr, but
// is not quite the same output. Compare to:
// https://github.com/nodejs/node/blob/v20.5.0/src/node_errors.cc#L246-L266
console.error(err);
agent.captureError(err, { handled: false }, function () {
cb ? cb(err) : process.exit(1);
});
};
process.on('uncaughtException', this._uncaughtExceptionListener);
};
// Flush all ended APM events (transactions, spans, errors, metricsets) to APM
// server as soon as possible. If the optional `cb` is given, it will be called
// `cb(flushErr)` when this is complete. If no `cb` is given, then a `Promise`
// will be returned, which will either resolve with `void` or reject with
// `flushErr`.
//
// Encoding and passing event data to the agent's transport is *asynchronous*
// for some event types: spans and errors. This flush will make a *best effort*
// attempt to wait for those "inflight" events to finish processing before
// flushing data to APM server. To avoid `.flush()` hanging, this times out
// after one second.
//
// If flush is called while still waiting for inflight events in an earlier
// flush call, then the more recent flush will only wait for events that were
// newly inflight *since the last .flush()* call. I.e. the second flush does
// *not* wait for the set of events the first flush is waiting on. This makes
// the semantics of flush less than ideal (one cannot blindly call .flush() to
// flush *everything* that has come before). However it handles the common use
// case of flushing synchronously after ending a span or capturing an error:
// mySpan.end()
// await apm.flush()
// and it simplifies the implementation.
//
// # Dev Notes
//
// To support the implementation, agent code that creates an inflight event
// must do the following:
// - Take a reference to the current set of inflight events:
// const inflightEvents = agent._inflightEvents
// - Add a unique ID for the event to the set:
// inflightEvents.add(id)
// - Delete the ID from the set when sent to the transport (`.sendSpan(...)` et
// al) or when dropping the event (e.g. because of a filter):
// inflightEvents.delete(id)
Agent.prototype.flush = function (cb) {
// This 1s timeout is a subjective balance between "long enough for spans
// and errors to reasonably encode" and "short enough to not block data
// being reported to APM server".
const DEFAULT_INFLIGHT_FLUSH_TIMEOUT_MS = 1000;
// shared options for the private `._flush()` API.
const opts = { inflightTimeoutMs: DEFAULT_INFLIGHT_FLUSH_TIMEOUT_MS };
if (typeof cb !== 'function') {
return new Promise((resolve, reject) => {
this._flush(opts, (err) => {
if (err) {
reject(err);
}
resolve();
});
});
}
return this._flush(opts, cb);
};
// The internal-use `.flush()` that supports some options not exposed to the
// public API.
//
// @param {Number} opts.inflightTimeoutMs - Required. The number of ms to wait
// for inflight events (spans, errors) to finish being send to the transport
// before flushing.
// @param {Boolean} opts.lambdaEnd - Optional, default false. Set this to true
// to signal to the transport that this is the flush at the end of a Lambda
// function invocation.
// https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-aws-lambda.md#data-flushing
Agent.prototype._flush = function (opts, cb) {
const lambdaEnd = !!opts.lambdaEnd;
if (!this._apmClient) {
// Log an *err* to provide a stack for the user.
const err = new Error('cannot flush agent before it is started');
this.logger.warn({ err }, err.message);
if (cb) {
process.nextTick(cb);
}
return;
}
const boundCb = cb && this._instrumentation.bindFunction(cb);
// If there are no inflight events then avoid creating additional objects.
if (this._inflightEvents.size === 0) {
this._apmClient.flush({ lambdaEnd }, boundCb);
return;
}
// Otherwise, there are inflight events to wait for. Setup a handler to
// callback when the current set of inflight events complete.
const flushingInflightEvents = this._inflightEvents;
flushingInflightEvents.setDrainHandler((drainErr) => {
// The only possible drainErr is a timeout. This is best effort, so we only
// log this and move on.
this.logger.debug(
{
numRemainingInflightEvents: flushingInflightEvents.size,
err: drainErr,
},
'flush: drained inflight events',
);
// Then, flush the intake request to APM server.
this._apmClient.flush({ lambdaEnd }, boundCb);
}, opts.inflightTimeoutMs);
// Create a new empty set to collect subsequent inflight events.
this._inflightEvents = new InflightEventSet();
};
Agent.prototype.registerMetric = function (name, labelsOrCallback, callback) {
var labels;
if (typeof labelsOrCallback === 'function') {
callback = labelsOrCallback;
} else {
labels = labelsOrCallback;
}
if (typeof callback !== 'function') {
this.logger.error("Can't add callback of type %s", typeof callback);
return;
}
this._metrics.getOrCreateGauge(name, callback, labels);
};
/**
* Return true iff the given metric name is "disabled", according to the
* `disableMetrics` config var.
*
* @returns {boolean}
*/
Agent.prototype._isMetricNameDisabled = function (name) {
const regexps = this._conf.disableMetricsRegExp;
for (var i = 0; i < regexps.length; i++) {
if (regexps[i].test(name)) {
return true;
}
}
return false;
};