UNPKG

elastic-apm-node

Version:

The official Elastic APM agent for Node.js

1,128 lines (1,025 loc) 34.5 kB
/* * 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'; var fs = require('fs'); var path = require('path'); const { Hook: RitmHook } = require('require-in-the-middle'); const IitmHook = require('import-in-the-middle'); const semver = require('semver'); const { CONTEXT_MANAGER_ASYNCHOOKS, CONTEXT_MANAGER_ASYNCLOCALSTORAGE, } = require('../constants'); var { Ids } = require('./ids'); var Transaction = require('./transaction'); var { NoopTransaction } = require('./noop-transaction'); const { AsyncHooksRunContextManager, AsyncLocalStorageRunContextManager, } = require('./run-context'); const { getLambdaHandlerInfo } = require('../lambda'); const undiciInstr = require('./modules/undici'); const azureFunctionsInstr = require('./azure-functions'); const nodeSupportsAsyncLocalStorage = semver.satisfies( process.versions.node, '>=14.5 || ^12.19.0', ); // Node v16.5.0 added fetch support (behind `--experimental-fetch` until // v18.0.0) based on undici@5.0.0. We can instrument undici >=v4.7.1. const nodeHasInstrumentableFetch = typeof global.fetch === 'function'; var MODULE_PATCHERS = [ { modPath: '@apollo/server' }, { modPath: '@smithy/smithy-client' }, // Instrument the base client which all AWS-SDK v3 clients extend. { modPath: '@aws-sdk/smithy-client', patcher: './modules/@smithy/smithy-client.js', }, { modPath: '@elastic/elasticsearch' }, { modPath: '@elastic/elasticsearch-canary', patcher: './modules/@elastic/elasticsearch.js', }, { modPath: '@opentelemetry/api' }, { modPath: '@opentelemetry/sdk-metrics' }, { modPath: '@redis/client/dist/lib/client/index.js', diKey: 'redis' }, { modPath: '@redis/client/dist/lib/client/commands-queue.js', diKey: 'redis', }, { modPath: '@node-redis/client/dist/lib/client/index.js', patcher: './modules/@redis/client/dist/lib/client/index.js', diKey: 'redis', }, { modPath: '@node-redis/client/dist/lib/client/commands-queue.js', patcher: './modules/@redis/client/dist/lib/client/commands-queue.js', diKey: 'redis', }, { modPath: 'apollo-server-core' }, { modPath: 'aws-sdk' }, { modPath: 'bluebird' }, { modPath: 'cassandra-driver' }, { modPath: 'elasticsearch' }, { modPath: 'express' }, { modPath: 'express-graphql' }, { modPath: 'express-queue' }, { modPath: 'fastify' }, { modPath: 'finalhandler' }, { modPath: 'generic-pool' }, { modPath: 'graphql' }, { modPath: 'handlebars' }, { modPath: '@hapi/hapi' }, { modPath: 'http' }, { modPath: 'https' }, { modPath: 'http2' }, { modPath: 'ioredis' }, { modPath: 'jade' }, { modPath: 'kafkajs' }, { modPath: 'knex' }, { modPath: 'koa' }, { modPath: 'koa-router' }, { modPath: '@koa/router', patcher: './modules/koa-router.js' }, { modPath: 'memcached' }, { modPath: 'mimic-response' }, { modPath: 'mongodb-core' }, { modPath: 'mongodb' }, { modPath: 'mongodb/lib/cmap/connection_pool.js', patcher: './modules/mongodb/lib/cmap/connection_pool.js', }, { modPath: 'mysql' }, { modPath: 'mysql2' }, { modPath: 'next' }, { modPath: 'next/dist/server/api-utils/node.js' }, { modPath: 'next/dist/server/dev/next-dev-server.js' }, { modPath: 'next/dist/server/next-server.js' }, { modPath: 'pg' }, { modPath: 'pug' }, { modPath: 'redis' }, { modPath: 'restify' }, { modPath: 'tedious' }, { modPath: 'undici' }, { modPath: 'ws' }, ]; /** * This is a subset of `MODULES` until ESM support for all is tested. * * @typedef {Object} IitmModuleInfo * @property {boolean} [instrumentImportMod] If false, this indicates that * the instrumentation for this module should be passed the * `modExports.default` property instead of the `modExports`. For * instrumentation of CommonJS modules that do not modify top-level * exports, this generally means the instrumentation can remain unchanged. * See the handling of the `default` property at * https://nodejs.org/api/esm.html#commonjs-namespaces * * @type {Map<string, IitmModuleInfo>} */ const IITM_MODULES = { // This smithy-client entry isn't used for `@aws-sdk/client-*` ESM support // because the smithy-client is transitively `require`d by CommonJS aws-sdk // code. If a future aws-sdk v3 version switches to ESM imports internally, // then this will be relevant. // '@aws-sdk/smithy-client': { instrumentImportMod: false }, 'cassandra-driver': { instrumentImportMod: false }, express: { instrumentImportMod: false }, fastify: { instrumentImportMod: true }, http: { instrumentImportMod: true }, https: { instrumentImportMod: true }, ioredis: { instrumentImportMod: false }, knex: { instrumentImportMod: false }, pg: { instrumentImportMod: false }, }; /** * modPath modName * ------- --------- * mongodb mongodb * mongodb/lib/foo.js mongodb * @elastic/elasticsearch @elastic/elasticsearch * @redis/client/dist/lib/client.js @redis/client * /var/task/index.js /var/task/index.js */ function modNameFromModPath(modPath) { if (modPath.startsWith('/')) { return modPath; } else if (modPath.startsWith('@')) { return modPath.split('/', 2).join('/'); } else { return modPath.split('/', 1)[0]; } } function normPathSeps(s) { return path.sep !== '/' ? s.split(path.sep).join('/') : s; } /** * Holds the registered set of "patchers" (functions that monkey patch imported * modules) for a module path (`modPath`). */ class PatcherRegistry { constructor() { this.reset(); } reset() { this._infoFromModPath = {}; } /** * Add a patcher for the given module path. * * @param {string} modPath - Identifies a module that RITM can hook: a * module name (http, @smithy/client), a module-relative path * (mongodb/lib/cmap/connection_pool.js), an absolute path * (/var/task/index.js; Windows paths are not supported), a sub-module * (react-dom/server). * @param {import('../..').PatchHandler | string} patcher - A patcher function * or a path to a CommonJS module that exports one as the default export. * @param {string} [diKey] - An optional key in the `disableInstrumentations` * config var that is used to determine if this patcher is * disabled. All patchers for the same modPath must share the same `diKey`. * This throws if a conflicting `diKey` is given. * It defaults to the `modName` (derived from the `modPath`). */ add(modPath, patcher, diKey = null) { if (!(modPath in this._infoFromModPath)) { this._infoFromModPath[modPath] = { patchers: [patcher], diKey: diKey || modNameFromModPath(modPath), }; } else { const entry = this._infoFromModPath[modPath]; // The `diKey`, if provided, must be the same for all patchers for a modPath. if (diKey && diKey !== entry.diKey) { throw new Error( `invalid "diKey", ${diKey}, for module "${modPath}" patcher: it conflicts with existing diKey=${entry.diKey}`, ); } entry.patchers.push(patcher); } } /** * Remove the given patcher for the given module path. */ remove(modPath, patcher) { const entry = this._infoFromModPath[modPath]; if (!entry) { return; } const idx = entry.patchers.indexOf(patcher); if (idx !== -1) { entry.patchers.splice(idx, 1); } if (entry.patchers.length === 0) { delete this._infoFromModPath[modPath]; } } /** * Remove all patchers for the given module path. */ clear(modPath) { delete this._infoFromModPath[modPath]; } has(modPath) { return modPath in this._infoFromModPath; } getPatchers(modPath) { return this._infoFromModPath[modPath]?.patchers; } /** * Returns the appropriate RITM `modules` argument so that all registered * `modPath`s will be hooked. This assumes `{internals: true}` RITM options * are used. * * @returns {Array<string>} */ ritmModulesArg() { // RITM hooks: // 1. `require('mongodb')` if 'mongodb' is in the modules arg; // 2. `require('mongodb/lib/foo.js')`, a module-relative path, if 'mongodb' // is in the modules arg and `{internals: true}` option is given; // 3. `require('/var/task/index.js')` if the exact resolved absolute path // is in the modules arg; and // 4. `require('react-dom/server')`, a "sub-module", if 'react-dom/server' // is in the modules arg. // // The wrinkle is that the modPath "mongodb/lib/foo.js" need not be in the // `modules` argument to RITM, but the similar-looking "react-dom/server" // must be. const modules = new Set(); const hasModExt = /\.(js|cjs|mjs|json)$/; Object.keys(this._infoFromModPath).forEach((modPath) => { const modName = modNameFromModPath(modPath); if (modPath === modName) { modules.add(modPath); } else { if (hasModExt.test(modPath)) { modules.add(modName); // case 2 } else { // Beware the RITM bug: passing both 'foo' and 'foo/subpath' results // in 'foo/subpath' not being hooked. // TODO: link to issue for this modules.add(modPath); // case 4 } } }); return Array.from(modules); } /** * Get the string on the `disableInstrumentations` config var that indicates * if this module path should be disabled. * * Typically this is the module name -- e.g. "@redis/client" -- but might be * a custom value -- e.g. "lambda" for a Lambda handler path. * * @returns {string | undefined} */ diKey(modPath) { return this._infoFromModPath[modPath]?.diKey; } } function Instrumentation(agent) { this._agent = agent; this._disableInstrumentationsSet = null; this._ritmHook = null; this._iitmHook = null; this._started = false; this._runCtxMgr = null; this._log = agent.logger; this._patcherReg = new PatcherRegistry(); this._cachedVerFromModBaseDir = new Map(); } Instrumentation.prototype.currTransaction = function () { if (!this._started) { return null; } return this._runCtxMgr.active().currTransaction(); }; Instrumentation.prototype.currSpan = function () { if (!this._started) { return null; } return this._runCtxMgr.active().currSpan(); }; Instrumentation.prototype.ids = function () { if (!this._started) { return new Ids(); } const runContext = this._runCtxMgr.active(); const currSpanOrTrans = runContext.currSpan() || runContext.currTransaction(); if (currSpanOrTrans) { return currSpanOrTrans.ids; } return new Ids(); }; Instrumentation.prototype.addPatch = function (modules, handler) { if (!Array.isArray(modules)) { modules = [modules]; } for (const modPath of modules) { const type = typeof handler; if (type !== 'function' && type !== 'string') { this._agent.logger.error('Invalid patch handler type: %s', type); return; } this._patcherReg.add(modPath, handler); } this._restartHooks(); }; Instrumentation.prototype.removePatch = function (modules, handler) { if (!Array.isArray(modules)) modules = [modules]; for (const modPath of modules) { this._patcherReg.remove(modPath, handler); } this._restartHooks(); }; Instrumentation.prototype.clearPatches = function (modules) { if (!Array.isArray(modules)) modules = [modules]; for (const modPath of modules) { this._patcherReg.clear(modPath); } this._restartHooks(); }; // If in a Lambda environment, find its handler and add a patcher for it. Instrumentation.prototype._maybeLoadLambdaPatcher = function () { let lambdaHandlerInfo = getLambdaHandlerInfo(process.env, this._log); if (lambdaHandlerInfo && this._patcherReg.has(lambdaHandlerInfo.modName)) { this._log.warn( 'Unable to instrument Lambda handler "%s" due to name conflict with "%s", please choose a different Lambda handler name', process.env._HANDLER, lambdaHandlerInfo.modName, ); lambdaHandlerInfo = null; } if (lambdaHandlerInfo) { const { createLambdaPatcher } = require('./modules/_lambda-handler'); this._lambdaHandlerInfo = lambdaHandlerInfo; this._patcherReg.add( this._lambdaHandlerInfo.filePath, createLambdaPatcher(lambdaHandlerInfo.propPath), 'lambda', // diKey ); } }; // Start the instrumentation system. // // @param {RunContext} [runContextClass] - A class to use for the core object // that is used to track run context. It defaults to `RunContext`. If given, // it must be `RunContext` (the typical case) or a subclass of it. The OTel // Bridge uses this to provide a subclass that bridges to OpenTelemetry // `Context` usage. Instrumentation.prototype.start = function (runContextClass) { if (this._started) return; this._started = true; // Could have changed in Agent.start(). this._log = this._agent.logger; // Select the appropriate run-context manager. const confContextManager = this._agent._conf.contextManager; if (confContextManager === CONTEXT_MANAGER_ASYNCHOOKS) { this._runCtxMgr = new AsyncHooksRunContextManager( this._log, runContextClass, ); } else if (nodeSupportsAsyncLocalStorage) { this._runCtxMgr = new AsyncLocalStorageRunContextManager( this._log, runContextClass, ); } else { if (confContextManager === CONTEXT_MANAGER_ASYNCLOCALSTORAGE) { this._log.warn( `config includes 'contextManager="${confContextManager}"', but node ${process.version} does not support AsyncLocalStorage for run-context management: falling back to using async_hooks`, ); } this._runCtxMgr = new AsyncHooksRunContextManager( this._log, runContextClass, ); } // Load module patchers: from MODULE_PATCHERS, for Lambda, and from // config.addPatch. for (let info of MODULE_PATCHERS) { let patcher; if (info.patcher) { patcher = path.resolve(__dirname, info.patcher); } else { // Typically the patcher module for the APM agent's included // instrumentations is "./modules/${modPath}[.js]". patcher = path.resolve( __dirname, 'modules', info.modPath + (info.modPath.endsWith('.js') ? '' : '.js'), ); } this._patcherReg.add(info.modPath, patcher, info.diKey); } this._maybeLoadLambdaPatcher(); const patches = this._agent._conf.addPatch; if (Array.isArray(patches)) { for (const [modPath, patcher] of patches) { this._patcherReg.add(modPath, patcher); } } this._runCtxMgr.enable(); this._restartHooks(); if (nodeHasInstrumentableFetch && this._isModuleEnabled('undici')) { this._log.debug('instrumenting fetch'); undiciInstr.instrumentUndici(this._agent); } if (azureFunctionsInstr.isAzureFunctionsEnvironment) { this._log.debug('instrumenting azure-functions'); azureFunctionsInstr.instrument(this._agent); } }; // Stop active instrumentation and reset global state *as much as possible*. // // Limitations: Removing and re-applying 'require-in-the-middle'-based patches // has no way to update existing references to patched or unpatched exports from // those modules. Instrumentation.prototype.stop = function () { this._started = false; // Reset run context tracking. if (this._runCtxMgr) { this._runCtxMgr.disable(); this._runCtxMgr = null; } // Reset patching. if (this._ritmHook) { this._ritmHook.unhook(); this._ritmHook = null; } if (this._iitmHook) { this._iitmHook.unhook(); this._iitmHook = null; } this._patcherReg.reset(); this._lambdaHandlerInfo = null; if (nodeHasInstrumentableFetch) { undiciInstr.uninstrumentUndici(); } if (azureFunctionsInstr.isAzureFunctionsEnvironment) { azureFunctionsInstr.uninstrument(); } }; // Reset internal state for (relatively) clean re-use of this Instrumentation. // Used for testing, while `resetAgent()` + "test/_agent.js" usage still exists. // // This does *not* include redoing monkey patching. It resets context tracking, // so a subsequent test case can re-use the Instrumentation in the same process. Instrumentation.prototype.testReset = function () { if (this._runCtxMgr) { this._runCtxMgr.testReset(); } }; Instrumentation.prototype._isModuleEnabled = function (modName) { if (!this._disableInstrumentationsSet) { this._disableInstrumentationsSet = new Set( this._agent._conf.disableInstrumentations, ); } return ( this._agent._conf.instrument && !this._disableInstrumentationsSet.has(modName) ); }; Instrumentation.prototype._restartHooks = function () { if (!this._started) { return; } if (this._ritmHook || this._iitmHook) { this._agent.logger.debug('removing hooks to Node.js module loader'); if (this._ritmHook) { this._ritmHook.unhook(); } if (this._iitmHook) { this._iitmHook.unhook(); } } var self = this; this._log.debug('adding Node.js module loader hooks'); this._ritmHook = new RitmHook( this._patcherReg.ritmModulesArg(), { internals: true }, function (exports, modPath, basedir) { let version = undefined; // An *absolute path* given to RITM results in the file *basename* being // used as `modPath` in this callback. We need the absolute path back to // look up the patcher in our registry. We know the only absolute path // we use is for our Lambda handler. if (self._lambdaHandlerInfo?.modName === modPath) { modPath = self._lambdaHandlerInfo.filePath; version = process.env.AWS_LAMBDA_FUNCTION_VERSION || ''; } else { // RITM returns `modPath` using native path separators. However, // _patcherReg is keyed with '/' separators, so we need to normalize. modPath = normPathSeps(modPath); } if (!self._patcherReg.has(modPath)) { // Skip out if there are no patchers for this hooked module name. return exports; } // Find an appropriate version for this modPath. if (version !== undefined) { // Lambda version already handled above. } else if (!basedir) { // This is a core module. version = process.versions.node; } else { // This is a module (e.g. 'mongodb') or a module internal path // ('mongodb/lib/cmap/connection_pool.js'). version = self._getPackageVersion(modPath, basedir); if (version === undefined) { self._log.debug('could not patch %s module', modPath); return exports; } } const diKey = self._patcherReg.diKey(modPath); const enabled = self._isModuleEnabled(diKey); return self._patchModule(exports, modPath, version, enabled, false); }, ); this._iitmHook = IitmHook( // TODO: Eventually derive this from `_patcherRegistry`. Object.keys(IITM_MODULES), function (modExports, modName, modBaseDir) { const enabled = self._isModuleEnabled(modName); const version = modBaseDir ? self._getPackageVersion(modName, modBaseDir) : process.versions.node; if (IITM_MODULES[modName].instrumentImportMod) { return self._patchModule(modExports, modName, version, enabled, true); } else { modExports.default = self._patchModule( modExports.default, modName, version, enabled, false, ); return modExports; } }, ); }; Instrumentation.prototype._getPackageVersion = function (modName, modBaseDir) { if (this._cachedVerFromModBaseDir.has(modBaseDir)) { return this._cachedVerFromModBaseDir.get(modBaseDir); } let ver = undefined; try { const version = JSON.parse( fs.readFileSync(path.join(modBaseDir, 'package.json')), ).version; if (typeof version === 'string') { ver = version; } } catch (err) { this._agent.logger.debug( { modName, modBaseDir, err }, 'could not load package version', ); } this._cachedVerFromModBaseDir.set(modBaseDir, ver); return ver; }; /** * Patch/instrument the given module. * * @param {Module | any} modExports The object made available by the RITM or * IITM hook. For a `require` this is the `module.exports` value, which can * by any type. For an `import` this is a `Module` object if * `isImportMod=true`, or the default export (the equivalent of * `module.exports`) if `isImportMod=false`. * @param {string} modPath * @param {string} version * @param {boolean} enabled Whether instrumentation is enabled for this module * depending on the `disableInstrumentations` config value. (Currently the * http, https, and http2 instrumentations, at least, do *some* work even if * enabled=false.) * @param {boolean} isImportMod When false, the `modExports` param is the * `module.exports` object (typically from a `require`). When true, * `modExports` is the `Module` instance from an `import`. This depends on * the `instrumentImportMod` flag that is set per module. */ Instrumentation.prototype._patchModule = function ( modExports, modPath, version, enabled, isImportMod, ) { this._log.debug( 'instrumenting %s@%s module (enabled=%s, isImportMod=%s)', modPath, version, enabled, isImportMod, ); const patchers = this._patcherReg.getPatchers(modPath); if (patchers) { for (let patcher of patchers) { if (typeof patcher === 'string') { if (patcher[0] === '.') { patcher = path.resolve(process.cwd(), patcher); } patcher = require(patcher); } const type = typeof patcher; if (type !== 'function') { this._agent.logger.error( 'Invalid patch handler type "%s" for module "%s"', type, modPath, ); continue; } modExports = patcher(modExports, this._agent, { name: modPath, version, enabled, isImportMod, }); } } return modExports; }; Instrumentation.prototype.addEndedTransaction = function (transaction) { var agent = this._agent; if (!this._started) { agent.logger.debug('ignoring transaction %o', { trans: transaction.id, trace: transaction.traceId, }); return; } const rc = this._runCtxMgr.active(); if (rc.currTransaction() === transaction) { // Replace the active run context with an empty one. I.e. there is now // no active transaction or span (at least in this async task). this._runCtxMgr.supersedeRunContext(this._runCtxMgr.root()); this._log.debug( { ctxmgr: this._runCtxMgr.toString() }, 'addEndedTransaction(%s)', transaction.name, ); } // Avoid transaction filtering time if only propagating trace-context. if (agent._conf.contextPropagationOnly) { // This one log.trace related to contextPropagationOnly is included as a // possible log hint to future debugging for why events are not being sent // to APM server. agent.logger.trace('contextPropagationOnly: skip sendTransaction'); return; } // https://github.com/elastic/apm/blob/main/specs/agents/tracing-sampling.md#non-sampled-transactions if ( !transaction.sampled && !agent._apmClient.supportsKeepingUnsampledTransaction() ) { return; } // if I have ended and I have something buffered, send that buffered thing if (transaction.getBufferedSpan()) { this._encodeAndSendSpan(transaction.getBufferedSpan()); } var payload = agent._transactionFilters.process(transaction._encode()); if (!payload) { agent.logger.debug('transaction ignored by filter %o', { trans: transaction.id, trace: transaction.traceId, }); return; } agent.logger.debug('sending transaction %o', { trans: transaction.id, trace: transaction.traceId, }); agent._apmClient.sendTransaction(payload); }; Instrumentation.prototype.addEndedSpan = function (span) { var agent = this._agent; if (!this._started) { agent.logger.debug('ignoring span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type, }); return; } // Replace the active run context with this span removed. Typically this // span is the top of stack (i.e. is the current span). However, it is // possible to have out-of-order span.end(), in which case the ended span // might not. const newRc = this._runCtxMgr.active().leaveSpan(span); if (newRc) { this._runCtxMgr.supersedeRunContext(newRc); } this._log.debug( { ctxmgr: this._runCtxMgr.toString() }, 'addEndedSpan(%s)', span.name, ); // Avoid span encoding time if only propagating trace-context. if (agent._conf.contextPropagationOnly) { return; } if (!span.isRecorded()) { span.transaction.captureDroppedSpan(span); return; } if (!this._agent._conf.spanCompressionEnabled) { this._encodeAndSendSpan(span); } else { // if I have ended and I have something buffered, send that buffered thing if (span.getBufferedSpan()) { this._encodeAndSendSpan(span.getBufferedSpan()); span.setBufferedSpan(null); } const parentSpan = span.getParentSpan(); if ((parentSpan && parentSpan.ended) || !span.isCompressionEligible()) { const buffered = parentSpan && parentSpan.getBufferedSpan(); if (buffered) { this._encodeAndSendSpan(buffered); parentSpan.setBufferedSpan(null); } this._encodeAndSendSpan(span); } else if (!parentSpan.getBufferedSpan()) { // span is compressible and there's nothing buffered // add to buffer, move on parentSpan.setBufferedSpan(span); } else if (!parentSpan.getBufferedSpan().tryToCompress(span)) { // we could not compress span so SEND bufferend span // and buffer the span we could not compress this._encodeAndSendSpan(parentSpan.getBufferedSpan()); parentSpan.setBufferedSpan(span); } } }; Instrumentation.prototype._encodeAndSendSpan = function (span) { const duration = span.isComposite() ? span.getCompositeSum() : span.duration(); if ( span.discardable && duration / 1000 < this._agent._conf.exitSpanMinDuration ) { span.transaction.captureDroppedSpan(span); return; } const agent = this._agent; // Note this error as an "inflight" event. See Agent#flush(). const inflightEvents = agent._inflightEvents; inflightEvents.add(span.id); agent.logger.debug('encoding span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type, }); span._encode(function (err, payload) { if (err) { agent.logger.error('error encoding span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type, error: err.message, }); } else { payload = agent._spanFilters.process(payload); if (!payload) { agent.logger.debug('span ignored by filter %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type, }); } else { agent.logger.debug('sending span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type, }); if (agent._apmClient) { agent._apmClient.sendSpan(payload); } } } inflightEvents.delete(span.id); }); }; // Replace the current run context with one where the given transaction is // current. Instrumentation.prototype.supersedeWithTransRunContext = function (trans) { if (this._started) { const rc = this._runCtxMgr.root().enterTrans(trans); this._runCtxMgr.supersedeRunContext(rc); this._log.debug( { ctxmgr: this._runCtxMgr.toString() }, 'supersedeWithTransRunContext(<Trans %s>)', trans.id, ); } }; // Replace the current run context with one where the given span is current. Instrumentation.prototype.supersedeWithSpanRunContext = function (span) { if (this._started) { const rc = this._runCtxMgr.active().enterSpan(span); this._runCtxMgr.supersedeRunContext(rc); this._log.debug( { ctxmgr: this._runCtxMgr.toString() }, 'supersedeWithSpanRunContext(<Span %s>)', span.id, ); } }; // Set the current run context to have *no* transaction. No spans will be // created in this run context until a subsequent `startTransaction()`. Instrumentation.prototype.supersedeWithEmptyRunContext = function () { if (this._started) { this._runCtxMgr.supersedeRunContext(this._runCtxMgr.root()); this._log.debug( { ctxmgr: this._runCtxMgr.toString() }, 'supersedeWithEmptyRunContext()', ); } }; // Create a new transaction, but do *not* replace the current run context to // make this the "current" transaction. Compare to `startTransaction`. Instrumentation.prototype.createTransaction = function (name, ...args) { return new Transaction(this._agent, name, ...args); }; Instrumentation.prototype.startTransaction = function (name, ...args) { if (!this._agent.isStarted()) { return new NoopTransaction(); } const trans = new Transaction(this._agent, name, ...args); this.supersedeWithTransRunContext(trans); return trans; }; Instrumentation.prototype.endTransaction = function (result, endTime) { const trans = this.currTransaction(); if (!trans) { this._agent.logger.debug( 'cannot end transaction - no active transaction found', ); return; } trans.end(result, endTime); }; Instrumentation.prototype.setDefaultTransactionName = function (name) { const trans = this.currTransaction(); if (!trans) { this._agent.logger.debug( 'no active transaction found - cannot set default transaction name', ); return; } trans.setDefaultName(name); }; Instrumentation.prototype.setTransactionName = function (name) { const trans = this.currTransaction(); if (!trans) { this._agent.logger.debug( 'no active transaction found - cannot set transaction name', ); return; } trans.name = name; }; Instrumentation.prototype.setTransactionOutcome = function (outcome) { const trans = this.currTransaction(); if (!trans) { this._agent.logger.debug( 'no active transaction found - cannot set transaction outcome', ); return; } trans.setOutcome(outcome); }; // Create a new span in the current transaction, if any, and make it the // current span. The started span is returned. This will return null if a span // could not be created -- which could happen for a number of reasons. Instrumentation.prototype.startSpan = function ( name, type, subtype, action, opts, ) { const trans = this.currTransaction(); if (!trans) { this._agent.logger.debug( 'no active transaction found - cannot build new span', ); return null; } return trans.startSpan.apply(trans, arguments); }; // Create a new span in the current transaction, if any. The created span is // returned, or null if the span could not be created. // // This does *not* replace the current run context to make this span the // "current" one. This allows instrumentations to avoid impacting the run // context of the calling code. Compare to `startSpan`. Instrumentation.prototype.createSpan = function ( name, type, subtype, action, opts, ) { const trans = this.currTransaction(); if (!trans) { this._agent.logger.debug( 'no active transaction found - cannot build new span', ); return null; } return trans.createSpan.apply(trans, arguments); }; Instrumentation.prototype.setSpanOutcome = function (outcome) { const span = this.currSpan(); if (!span) { this._agent.logger.debug('no active span found - cannot set span outcome'); return null; } span.setOutcome(outcome); }; Instrumentation.prototype.currRunContext = function () { if (!this._started) { return null; } return this._runCtxMgr.active(); }; // Bind the given function to the current run context. Instrumentation.prototype.bindFunction = function (fn) { if (!this._started) { return fn; } return this._runCtxMgr.bindFn(this._runCtxMgr.active(), fn); }; // Bind the given function to a given run context. Instrumentation.prototype.bindFunctionToRunContext = function (runContext, fn) { if (!this._started) { return fn; } return this._runCtxMgr.bindFn(runContext, fn); }; // Bind the given function to an *empty* run context. // This can be used to ensure `fn` does *not* run in the context of the current // transaction or span. Instrumentation.prototype.bindFunctionToEmptyRunContext = function (fn) { if (!this._started) { return fn; } return this._runCtxMgr.bindFn(this._runCtxMgr.root(), fn); }; // Bind the given EventEmitter to the current run context. // // This wraps the emitter so that any added event handler function is bound // as if `bindFunction` had been called on it. Note that `ee` need not // inherit from EventEmitter -- it uses duck typing. Instrumentation.prototype.bindEmitter = function (ee) { if (!this._started) { return ee; } return this._runCtxMgr.bindEE(this._runCtxMgr.active(), ee); }; // Bind the given EventEmitter to a given run context. Instrumentation.prototype.bindEmitterToRunContext = function (runContext, ee) { if (!this._started) { return ee; } return this._runCtxMgr.bindEE(runContext, ee); }; // Return true iff the given EventEmitter is bound to a run context. Instrumentation.prototype.isEventEmitterBound = function (ee) { if (!this._started) { return false; } return this._runCtxMgr.isEEBound(ee); }; // Invoke the given function in the context of `runContext`. Instrumentation.prototype.withRunContext = function ( runContext, fn, thisArg, ...args ) { if (!this._started) { return fn.call(thisArg, ...args); } return this._runCtxMgr.with(runContext, fn, thisArg, ...args); }; module.exports = { Instrumentation, };