diff --git a/.tav.yml b/.tav.yml index 5dfd76761c..100539167d 100644 --- a/.tav.yml +++ b/.tav.yml @@ -83,6 +83,7 @@ redis: node: '>=14.0.0' commands: - node test/instrumentation/modules/redis.test.js + - node test/instrumentation/modules/redis-disabled.test.js - node test/instrumentation/modules/redis4-legacy.test.js # We want these version ranges: diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index e193d05f08..120d9500bd 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -54,6 +54,10 @@ See the <> guide. * Add support for `@aws-sdk/client-sqs`, one of the AWS SDK v3 clients. ({issues}2957[#2957]) +* Fixes for some values of the <> config setting. + "redis" will now properly disable instrumentation for redis@4. + "next" will propertly disable all Next.js instrumentation. + ({pull}3658[#3658]) [float] ===== Bug fixes diff --git a/docs/agent-api.asciidoc b/docs/agent-api.asciidoc index 6d04a3e1fe..5fa592c905 100644 --- a/docs/agent-api.asciidoc +++ b/docs/agent-api.asciidoc @@ -810,6 +810,10 @@ apm.addPatch('timers', (exports, agent, { version, enabled }) => { apm.addPatch('timers', './timer-patch') ---- +This and the other "Patch"-related API methods should be called *before* +starting the APM agent. Changes after the agent has started and relevant +modules have been `require`d can have surprising caching behavior. + [[apm-remove-patch]] ==== `apm.removePatch(modules, handler)` diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 92718c60c1..6c5831296b 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -962,7 +962,7 @@ The `sanitizeFieldNames` will redact any matched _field names_. If you wish to * *Type:* Array of strings * *Env:* `ELASTIC_APM_DISABLE_INSTRUMENTATIONS` -Array or comma-separated string of modules to disable instrumentation for. +Array or comma-separated string of module names for which to disable instrumentation. When instrumentation is disabled for a module, no spans will be collected for that module. diff --git a/index.d.ts b/index.d.ts index d7f01c8d30..f9fae0aa0e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -381,6 +381,7 @@ declare namespace apm { type PatchHandler = (exports: any, agent: Agent, options: PatchOptions) => any; interface PatchOptions { + name: string; version: string | undefined; enabled: boolean; } diff --git a/lib/agent.js b/lib/agent.js index ac5846c7bc..31655dae4f 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -22,7 +22,7 @@ const connect = require('./middleware/connect'); const constants = require('./constants'); const errors = require('./errors'); const { InflightEventSet } = require('./InflightEventSet'); -const Instrumentation = require('./instrumentation'); +const { Instrumentation } = require('./instrumentation'); const { elasticApmAwsLambda } = require('./lambda'); const Metrics = require('./metrics'); const parsers = require('./parsers'); diff --git a/lib/instrumentation/index.js b/lib/instrumentation/index.js index b90152f7f7..518668efb3 100644 --- a/lib/instrumentation/index.js +++ b/lib/instrumentation/index.js @@ -18,7 +18,6 @@ const { CONTEXT_MANAGER_ASYNCLOCALSTORAGE, } = require('../config/schema'); var { Ids } = require('./ids'); -var NamedArray = require('./named-array'); var Transaction = require('./transaction'); var { NoopTransaction } = require('./noop-transaction'); const { @@ -37,55 +36,75 @@ const nodeSupportsAsyncLocalStorage = semver.satisfies( // v18.0.0) based on undici@5.0.0. We can instrument undici >=v4.7.1. const nodeHasInstrumentableFetch = typeof global.fetch === 'function'; -var MODULES = [ - '@apollo/server', - ['@smithy/smithy-client', '@aws-sdk/smithy-client'], // Instrument the base client which all AWS-SDK v3 clients extends. - ['@elastic/elasticsearch', '@elastic/elasticsearch-canary'], - '@opentelemetry/api', - '@opentelemetry/sdk-metrics', - '@node-redis/client/dist/lib/client', - '@node-redis/client/dist/lib/client/commands-queue', - '@redis/client/dist/lib/client', - '@redis/client/dist/lib/client/commands-queue', - 'apollo-server-core', - 'aws-sdk', - 'bluebird', - 'cassandra-driver', - 'elasticsearch', - 'express', - 'express-graphql', - 'express-queue', - 'fastify', - 'finalhandler', - 'generic-pool', - 'graphql', - 'handlebars', - '@hapi/hapi', - 'http', - 'https', - 'http2', - 'ioredis', - 'jade', - 'knex', - 'koa', - ['koa-router', '@koa/router'], - 'memcached', - 'mimic-response', - 'mongodb-core', - 'mongodb', - 'mysql', - 'mysql2', - 'next/dist/server/api-utils/node', - 'next/dist/server/dev/next-dev-server', - 'next/dist/server/next', - 'next/dist/server/next-server', - 'pg', - 'pug', - 'redis', - 'restify', - 'tedious', - 'undici', - 'ws', +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: '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: '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' }, ]; /** @@ -118,7 +137,158 @@ const IITM_MODULES = { pg: { instrumentImportMod: false }, }; -module.exports = Instrumentation; +/** + * 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]; + } +} + +/** + * 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} + */ + 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; @@ -128,36 +298,8 @@ function Instrumentation(agent) { this._started = false; this._runCtxMgr = null; this._log = agent.logger; - - // NOTE: we need to track module names for patches - // in a separate array rather than using Object.keys() - // because the array is given to the hook(...) call. - this._patches = new NamedArray(); - - for (let modName of MODULES) { - if (!Array.isArray(modName)) modName = [modName]; - const pathName = modName[0]; - - this.addPatch(modName, (...args) => { - // Lazy require so that we don't have to use `require.resolve` which - // would fail in combination with Webpack. For more info see: - // https://github.com/elastic/apm-agent-nodejs/pull/957 - return require(`./modules/${pathName}.js`)(...args); - }); - } - - // patch for lambda handler needs special handling since its - // module name will always be different than its handler name - this._lambdaHandlerInfo = getLambdaHandlerInfo( - process.env, - MODULES, - this._log, - ); - if (this._lambdaHandlerInfo) { - this.addPatch(this._lambdaHandlerInfo.filePath, (...args) => { - return require('./modules/_lambda-handler')(...args); - }); - } + this._patcherReg = new PatcherRegistry(); + this._cachedVerFromModBaseDir = new Map(); } Instrumentation.prototype.currTransaction = function () { @@ -190,41 +332,60 @@ Instrumentation.prototype.addPatch = function (modules, handler) { if (!Array.isArray(modules)) { modules = [modules]; } - - for (const modName of 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._patches.add(modName, handler); + this._patcherReg.add(modPath, handler); } - - this._startHook(); + this._restartHooks(); }; Instrumentation.prototype.removePatch = function (modules, handler) { if (!Array.isArray(modules)) modules = [modules]; - for (const modName of modules) { - this._patches.delete(modName, handler); + for (const modPath of modules) { + this._patcherReg.remove(modPath, handler); } - this._startHook(); + this._restartHooks(); }; Instrumentation.prototype.clearPatches = function (modules) { if (!Array.isArray(modules)) modules = [modules]; - for (const modName of modules) { - this._patches.clear(modName); + for (const modPath of modules) { + this._patcherReg.clear(modPath); } - this._startHook(); + this._restartHooks(); }; -Instrumentation.modules = Object.freeze(MODULES); +// If in a Lambda environment, find its handler and add a patcher for it. +Instrumentation.prototype._maybeLoadLambdaPatcher = function () { + let lambdaHandlerInfo = getLambdaHandlerInfo(process.env); + + 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. // @@ -264,15 +425,36 @@ Instrumentation.prototype.start = function (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 [modName, path] of patches) { - this.addPatch(modName, path); + for (const [modPath, patcher] of patches) { + this._patcherReg.add(modPath, patcher); } } this._runCtxMgr.enable(); - this._startHook(); + this._restartHooks(); if (nodeHasInstrumentableFetch && this._isModuleEnabled('undici')) { this._log.debug('instrumenting fetch'); @@ -308,7 +490,8 @@ Instrumentation.prototype.stop = function () { this._iitmHook.unhook(); this._iitmHook = null; } - + this._patcherReg.reset(); + this._lambdaHandlerInfo = null; if (nodeHasInstrumentableFetch) { undiciInstr.uninstrumentUndici(); } @@ -340,7 +523,7 @@ Instrumentation.prototype._isModuleEnabled = function (modName) { ); }; -Instrumentation.prototype._startHook = function () { +Instrumentation.prototype._restartHooks = function () { if (!this._started) { return; } @@ -356,39 +539,52 @@ Instrumentation.prototype._startHook = function () { var self = this; - this._agent.logger.debug('adding hooks to Node.js module loader'); + 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 || ''; + } - this._ritmHook = new RitmHook(this._patches.keys, function ( - exports, - name, - basedir, - ) { - const enabled = self._isModuleEnabled(name); - var pkg, version; - - const isHandlingLambda = - self._lambdaHandlerInfo && self._lambdaHandlerInfo.module === name; - - if (!isHandlingLambda && basedir) { - pkg = path.join(basedir, 'package.json'); - try { - version = JSON.parse(fs.readFileSync(pkg)).version; - } catch (e) { - self._agent.logger.debug( - 'could not shim %s module: %s', - name, - e.message, - ); + if (!self._patcherReg.has(modPath)) { + // Skip out if there are no patchers for this hooked module name. return exports; } - } else { - version = process.versions.node; - } - return self._patchModule(exports, name, version, enabled, false); - }); + // 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); @@ -412,22 +608,27 @@ Instrumentation.prototype._startHook = function () { }; 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') { - return version; - } else { - return undefined; + ver = version; } } catch (err) { this._agent.logger.debug( { modName, modBaseDir, err }, 'could not load package version', ); - return undefined; } + + this._cachedVerFromModBaseDir.set(modBaseDir, ver); + return ver; }; /** @@ -438,7 +639,7 @@ Instrumentation.prototype._getPackageVersion = function (modName, modBaseDir) { * 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} name + * @param {string} modPath * @param {string} version * @param {boolean} enabled Whether instrumentation is enabled for this module * depending on the `disableInstrumentations` config value. (Currently the @@ -451,48 +652,40 @@ Instrumentation.prototype._getPackageVersion = function (modName, modBaseDir) { */ Instrumentation.prototype._patchModule = function ( modExports, - name, + modPath, version, enabled, isImportMod, ) { - this._agent.logger.debug( + this._log.debug( 'instrumenting %s@%s module (enabled=%s, isImportMod=%s)', - name, + modPath, version, enabled, isImportMod, ); - const isHandlingLambda = - this._lambdaHandlerInfo && this._lambdaHandlerInfo.module === name; - let patches; - if (!isHandlingLambda) { - patches = this._patches.get(name); - } else if (name === this._lambdaHandlerInfo.module) { - patches = this._patches.get(this._lambdaHandlerInfo.filePath); - } - - if (patches) { - for (let patch of patches) { - if (typeof patch === 'string') { - if (patch[0] === '.') { - patch = path.resolve(process.cwd(), patch); + 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); } - patch = require(patch); + patcher = require(patcher); } - const type = typeof patch; + const type = typeof patcher; if (type !== 'function') { this._agent.logger.error( 'Invalid patch handler type "%s" for module "%s"', type, - name, + modPath, ); continue; } - modExports = patch(modExports, this._agent, { - name, + modExports = patcher(modExports, this._agent, { + name: modPath, version, enabled, isImportMod, @@ -915,3 +1108,7 @@ Instrumentation.prototype.withRunContext = function ( } return this._runCtxMgr.with(runContext, fn, thisArg, ...args); }; + +module.exports = { + Instrumentation, +}; diff --git a/lib/instrumentation/modules/@node-redis/client/dist/lib/client.js b/lib/instrumentation/modules/@node-redis/client/dist/lib/client.js deleted file mode 100644 index 165887b8d6..0000000000 --- a/lib/instrumentation/modules/@node-redis/client/dist/lib/client.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * 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. - */ - -// early versions of redis@4 were released under -// the @node-redis namespace. This ensures our -// instrumentation works whether it's required with -// the `@node-redis/client` name or the -// `@redis/client` name -module.exports = require('../../../../@redis/client/dist/lib/client'); diff --git a/lib/instrumentation/modules/@node-redis/client/dist/lib/client/commands-queue.js b/lib/instrumentation/modules/@node-redis/client/dist/lib/client/commands-queue.js deleted file mode 100644 index 20e50698fa..0000000000 --- a/lib/instrumentation/modules/@node-redis/client/dist/lib/client/commands-queue.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * 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. - */ - -// early versions of redis@4 were released under -// the @node-redis namespace. This ensures our -// instrumentation works whether it's required with -// the `@node-redis/client` name or the -// `@redis/client` name -module.exports = require('../../../../../@redis/client/dist/lib/client/commands-queue'); diff --git a/lib/instrumentation/modules/@redis/client/dist/lib/client.js b/lib/instrumentation/modules/@redis/client/dist/lib/client/index.js similarity index 92% rename from lib/instrumentation/modules/@redis/client/dist/lib/client.js rename to lib/instrumentation/modules/@redis/client/dist/lib/client/index.js index 0a6099dbce..ac79a4de83 100644 --- a/lib/instrumentation/modules/@redis/client/dist/lib/client.js +++ b/lib/instrumentation/modules/@redis/client/dist/lib/client/index.js @@ -7,8 +7,8 @@ // Shim @redis/client's `RedisClient.create` to pass client options down to // the `RedisCommandsQueue` instrumentation in ./client/commands-queue.js. const semver = require('semver'); -const shimmer = require('../../../../../shimmer'); -const { redisClientOptions } = require('../../../../../../../lib/symbols'); +const shimmer = require('../../../../../../shimmer'); +const { redisClientOptions } = require('../../../../../../../../lib/symbols'); module.exports = function (mod, agent, { version, enabled }) { if (!enabled) { diff --git a/lib/instrumentation/modules/_lambda-handler.js b/lib/instrumentation/modules/_lambda-handler.js index ab1443906d..2418617510 100644 --- a/lib/instrumentation/modules/_lambda-handler.js +++ b/lib/instrumentation/modules/_lambda-handler.js @@ -6,27 +6,35 @@ 'use strict'; -const Instrumentation = require('../index'); -const { getLambdaHandlerInfo } = require('../../lambda'); const propwrap = require('../../propwrap'); -module.exports = function (module, agent, { version, enabled }) { - if (!enabled) { - return module; - } +/** + * Return a patch handler, `function (module, agent, options)`, that will patch + * the Lambda handler function at the given property path. + * + * For example, a Lambda _HANDLER=index.handler indicates that a file "index.js" + * has a `handler` export that is the Lambda handler function. In this case + * `module` will be the imported "index.js" module and `propPath` will be + * "handler". + */ +function createLambdaPatcher(propPath) { + return function lambdaHandlerPatcher(module, agent, { enabled }) { + if (!enabled) { + return module; + } + + try { + const newMod = propwrap.wrap(module, propPath, (orig) => { + return agent.lambda(orig); + }); + return newMod; + } catch (wrapErr) { + agent.logger.warn('could not wrap lambda handler: %s', wrapErr); + return module; + } + }; +} - const { field } = getLambdaHandlerInfo( - process.env, - Instrumentation.modules, - agent.logger, - ); - try { - const newMod = propwrap.wrap(module, field, (orig) => { - return agent.lambda(orig); - }); - return newMod; - } catch (wrapErr) { - agent.logger.warn('could not wrap lambda handler: %s', wrapErr); - return module; - } +module.exports = { + createLambdaPatcher, }; diff --git a/lib/instrumentation/modules/next/dist/server/next.js b/lib/instrumentation/modules/next.js similarity index 100% rename from lib/instrumentation/modules/next/dist/server/next.js rename to lib/instrumentation/modules/next.js diff --git a/lib/instrumentation/modules/redis.js b/lib/instrumentation/modules/redis.js index 53488b319d..4a5d81b50f 100644 --- a/lib/instrumentation/modules/redis.js +++ b/lib/instrumentation/modules/redis.js @@ -23,7 +23,8 @@ module.exports = function (redis, agent, { version, enabled }) { return redis; } if (!semver.satisfies(version, '>=2.0.0 <4.0.0')) { - agent.logger.debug('redis version %s not supported - aborting...', version); + // Explicitly do not log.debug here, because the message is misleading for + // redis@4 and later that is being handled by @redis/client instrumentation. return redis; } diff --git a/lib/instrumentation/named-array.js b/lib/instrumentation/named-array.js deleted file mode 100644 index 9c6c7b3a76..0000000000 --- a/lib/instrumentation/named-array.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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'; - -// TODO: Move to a separate module -const arrays = Symbol('sets'); - -function ensureArray(arrays, key) { - const array = arrays[key]; - if (array) return array; - - arrays[key] = []; - return arrays[key]; -} - -class NamedArray { - constructor() { - this[arrays] = {}; - } - - get keys() { - return Object.keys(this[arrays]); - } - - add(key, value) { - return ensureArray(this[arrays], key).push(value); - } - - clear(key) { - if (this.has(key)) { - delete this[arrays][key]; - } - } - - delete(key, value) { - const array = this.get(key); - if (array) { - const index = array.indexOf(value); - if (index !== -1) { - array.splice(index, 1); - if (!array.length) { - this.clear(key); - } - } - } - } - - has(key) { - return key in this[arrays]; - } - - get(key) { - return this[arrays][key]; - } -} - -module.exports = NamedArray; diff --git a/lib/lambda.js b/lib/lambda.js index 4ec510c5c9..b7d4d9ada5 100644 --- a/lib/lambda.js +++ b/lib/lambda.js @@ -808,21 +808,6 @@ function isLambdaExecutionEnvironment() { return !!process.env.AWS_LAMBDA_FUNCTION_NAME; } -function isHandlerNameInModules(handlerModule, modules) { - for (let instrumentedModules of modules) { - // array.flat didn't come around until Node 11 - if (!Array.isArray(instrumentedModules)) { - instrumentedModules = [instrumentedModules]; - } - for (const instrumentedModule of instrumentedModules) { - if (handlerModule === instrumentedModule) { - return true; - } - } - } - return false; -} - // Returns the full file path to the user's handler handler module // // The Lambda Runtime allows a user's handler module to have either a .js or @@ -843,13 +828,13 @@ function getFilePath(taskRoot, handlerModule) { return filePath; } -function getLambdaHandlerInfo(env, modules, logger) { +function getLambdaHandlerInfo(env) { if ( !isLambdaExecutionEnvironment() || !env._HANDLER || !env.LAMBDA_TASK_ROOT ) { - return; + return null; } // extract module name and "path" from handler using the same regex as the runtime @@ -857,28 +842,16 @@ function getLambdaHandlerInfo(env, modules, logger) { const functionExpression = /^([^.]*)\.(.*)$/; const match = env._HANDLER.match(functionExpression); if (!match || match.length !== 3) { - return; + return null; } const handlerModule = match[1].split('/').pop(); const handlerFunctionPath = match[2]; - - // if there's a name conflict with an already instrumented module, skip the - // instrumentation of the lambda handle and log a message. - if (isHandlerNameInModules(handlerModule, modules)) { - logger.warn( - 'Unable to instrument Lambda handler "%s" due to name conflict with "%s", please choose a different Lambda handler name', - env._HANDLER, - handlerModule, - ); - return; - } - const handlerFilePath = getFilePath(env.LAMBDA_TASK_ROOT, match[1]); return { filePath: handlerFilePath, - module: handlerModule, - field: handlerFunctionPath, + modName: handlerModule, + propPath: handlerFunctionPath, }; } diff --git a/test/agent.test.js b/test/agent.test.js index e8c25e7bfb..d34fecf32b 100644 --- a/test/agent.test.js +++ b/test/agent.test.js @@ -2085,10 +2085,10 @@ test('#active: false', function (t) { test('patches', function (t) { t.test('#clearPatches(name)', function (t) { - const agent = new Agent(); - t.ok(agent._instrumentation._patches.has('express')); + const agent = new Agent().start(); + t.ok(agent._instrumentation._patcherReg.has('express')); t.doesNotThrow(() => agent.clearPatches('express')); - t.notOk(agent._instrumentation._patches.has('express')); + t.notOk(agent._instrumentation._patcherReg.has('express')); t.doesNotThrow(() => agent.clearPatches('does-not-exists')); agent.destroy(); t.end(); @@ -2138,18 +2138,18 @@ test('patches', function (t) { t.test('#removePatch(name, handler)', function (t) { const agent = new Agent().start(agentOptsNoopTransport); - t.notOk(agent._instrumentation._patches.has('does-not-exist')); + t.notOk(agent._instrumentation._patcherReg.has('does-not-exist')); agent.addPatch('does-not-exist', '/foo.js'); - t.ok(agent._instrumentation._patches.has('does-not-exist')); + t.ok(agent._instrumentation._patcherReg.has('does-not-exist')); agent.removePatch('does-not-exist', '/foo.js'); - t.notOk(agent._instrumentation._patches.has('does-not-exist')); + t.notOk(agent._instrumentation._patcherReg.has('does-not-exist')); const handler = (exports) => exports; agent.addPatch('does-not-exist', handler); - t.ok(agent._instrumentation._patches.has('does-not-exist')); + t.ok(agent._instrumentation._patcherReg.has('does-not-exist')); agent.removePatch('does-not-exist', handler); - t.notOk(agent._instrumentation._patches.has('does-not-exist')); + t.notOk(agent._instrumentation._patcherReg.has('does-not-exist')); agent.destroy(); t.end(); @@ -2159,7 +2159,7 @@ test('patches', function (t) { const agent = new Agent().start(agentOptsNoopTransport); const moduleName = 'removePatch-test-module'; - t.notOk(agent._instrumentation._patches.has(moduleName)); + t.notOk(agent._instrumentation._patcherReg.has(moduleName)); const handler1 = function (exports) { return exports; @@ -2169,7 +2169,8 @@ test('patches', function (t) { }; agent.addPatch(moduleName, handler1); agent.addPatch(moduleName, handler2); - const modulePatches = agent._instrumentation._patches.get(moduleName); + const modulePatches = + agent._instrumentation._patcherReg.getPatchers(moduleName); t.ok( modulePatches.length === 2 && modulePatches[0] === handler1 && @@ -2179,19 +2180,19 @@ test('patches', function (t) { agent.removePatch(moduleName); t.equal( - agent._instrumentation._patches.get(moduleName).length, + agent._instrumentation._patcherReg.getPatchers(moduleName).length, 2, 'still have 2 patches after removePatch(name)', ); agent.removePatch(moduleName, 'this is not one of the registered handlers'); t.equal( - agent._instrumentation._patches.get(moduleName).length, + agent._instrumentation._patcherReg.getPatchers(moduleName).length, 2, 'still have 2 patches after removePatch(name, oops)', ); agent.removePatch(moduleName, function oops() {}); t.equal( - agent._instrumentation._patches.get(moduleName).length, + agent._instrumentation._patcherReg.getPatchers(moduleName).length, 2, 'still have 2 patches after removePatch(name, function oops () {})', ); diff --git a/test/config.test.js b/test/config.test.js index 2208b6df7c..f35d5488f7 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -16,14 +16,13 @@ var util = require('util'); var isRegExp = require('core-util-is').isRegExp; var mkdirp = require('mkdirp'); var rimraf = require('rimraf'); -var semver = require('semver'); var test = require('tape'); const Agent = require('../lib/agent'); const { MockAPMServer } = require('./_mock_apm_server'); const { MockLogger } = require('./_mock_logger'); const { NoopApmClient } = require('../lib/apm-client/noop-apm-client'); -const { safeGetPackageVersion, findObjInArray } = require('./_utils'); +const { findObjInArray } = require('./_utils'); const { secondsFromDuration } = require('../lib/config/normalizers'); const { CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES, @@ -33,13 +32,8 @@ const { } = require('../lib/config/schema'); const config = require('../lib/config/config'); -var Instrumentation = require('../lib/instrumentation'); var apmVersion = require('../package').version; var apmName = require('../package').name; -var isHapiIncompat = require('./_is_hapi_incompat'); -const isMongodbIncompat = require('./_is_mongodb_incompat'); -const isFastifyIncompat = require('./_is_fastify_incompat'); -const isCassandraIncompat = require('./_is_cassandra_incompat'); // Options to pass to `agent.start()` to turn off some default agent behavior // that is unhelpful for these tests. @@ -1194,137 +1188,19 @@ usePathAsTransactionNameTests.forEach(function (usePathAsTransactionNameTest) { ); }); -test('disableInstrumentations', function (t) { - var esVersion = safeGetPackageVersion('@elastic/elasticsearch'); - - // require('apollo-server-core') is a hard crash on nodes < 12.0.0 - const apolloServerCoreVersion = - require('apollo-server-core/package.json').version; +test('disableInstrumentaions', function (t) { + var agent = new Agent(); - var flattenedModules = Instrumentation.modules.reduce( - (acc, val) => acc.concat(val), - [], + // Spot-check some cases. + agent.start( + Object.assign({}, agentOptsNoopTransport, { + disableInstrumentations: 'foo,express , bar', + }), ); - var modules = new Set(flattenedModules); - modules.delete('jade'); // Deprecated, we no longer test this instrumentation. - if (isHapiIncompat()) { - modules.delete('@hapi/hapi'); - } - modules.delete('express-graphql'); - if (semver.lt(process.version, '10.0.0') && semver.gte(esVersion, '7.12.0')) { - modules.delete('@elastic/elasticsearch'); // - Version 7.12.0 dropped support for node v8. - } - if (semver.lt(process.version, '12.0.0') && semver.gte(esVersion, '8.0.0')) { - modules.delete('@elastic/elasticsearch'); // - Version 8.0.0 dropped node v10 support. - } - if (semver.lt(process.version, '14.0.0') && semver.gte(esVersion, '8.2.0')) { - modules.delete('@elastic/elasticsearch'); // - Version 8.2.0 dropped node v12 support. - } - if (semver.lt(process.version, '14.0.0')) { - modules.delete('@elastic/elasticsearch-canary'); - } - if (isFastifyIncompat()) { - modules.delete('fastify'); - } - if (isMongodbIncompat()) { - modules.delete('mongodb'); - } - if (isCassandraIncompat()) { - modules.delete('cassandra-driver'); - } - if ( - semver.gte(apolloServerCoreVersion, '3.0.0') && - semver.lt(process.version, '12.0.0') - ) { - modules.delete('apollo-server-core'); - } - if (semver.satisfies(process.version, '>17.x', { includePrerelease: true })) { - // Restify (as of 8.6.0) is completely broken with latest node v18 nightly. - // https://github.com/restify/node-restify/issues/1888 - modules.delete('restify'); - } - if (semver.lt(process.version, '16.0.0')) { - modules.delete('tedious'); - } - if (semver.lt(process.version, '12.18.0')) { - modules.delete('undici'); // undici@5 supports node >=12.18 - } - if (semver.lt(process.version, '12.0.0')) { - modules.delete('koa-router'); // koa-router@11 supports node >=12 - modules.delete('@koa/router'); // koa-router@11 supports node >=12 - } - if (semver.lt(process.version, '14.8.0')) { - modules.delete('restify'); - } - if (semver.lt(process.version, '14.16.0')) { - modules.delete('@apollo/server'); - } - modules.delete('next/dist/server/api-utils/node'); - modules.delete('next/dist/server/dev/next-dev-server'); - modules.delete('next/dist/server/next'); - modules.delete('next/dist/server/next-server'); - if (semver.lt(process.version, '14.0.0')) { - modules.delete('redis'); // redis@4 supports node >=14 - modules.delete('@redis/client/dist/lib/client'); // redis@4 supports node >=14 - modules.delete('@redis/client/dist/lib/client/commands-queue'); // redis@4 supports node >=14 - } - // @node-redis only present for redis >4 <4.1 -- - modules.delete('@node-redis/client/dist/lib/client'); // redis@4 supports node >=14 - modules.delete('@node-redis/client/dist/lib/client/commands-queue'); // redis@4 supports node >=14 - modules.delete('mysql2'); - modules.delete('@aws-sdk/smithy-client'); - modules.delete('@smithy/smithy-client'); - - function testSlice(t, name, selector) { - var selection = selector(modules); - var selectionSet = new Set( - typeof selection === 'string' ? selection.split(',') : selection, - ); - - t.test(name + ' -> ' + Array.from(selectionSet).join(','), function (t) { - var agent = new Agent(); - agent.start( - Object.assign({}, agentOptsNoopTransport, { - disableInstrumentations: selection, - }), - ); - - var found = new Set(); - - agent._instrumentation._patchModule = function ( - exports, - name, - version, - enabled, - isImportMod, - ) { - if (!enabled) found.add(name); - return exports; - }; - - for (const mod of modules) { - require(mod); - } - - t.deepEqual(selectionSet, found, 'disabled all selected modules'); - - agent.destroy(); - t.end(); - }); - } - - for (const mod of modules) { - testSlice(t, 'individual modules', () => new Set([mod])); - } - - testSlice(t, 'multiple modules by array', (modules) => { - return Array.from(modules).filter((value, index) => index % 2); - }); - - testSlice(t, 'multiple modules by csv string', (modules) => { - return Array.from(modules).filter((value, index) => !(index % 2)); - }); + t.strictEqual(agent._instrumentation._isModuleEnabled('express'), false); + t.strictEqual(agent._instrumentation._isModuleEnabled('http'), true); + agent.destroy(); t.end(); }); diff --git a/test/instrumentation/modules/redis-disabled.test.js b/test/instrumentation/modules/redis-disabled.test.js new file mode 100644 index 0000000000..58f7f51195 --- /dev/null +++ b/test/instrumentation/modules/redis-disabled.test.js @@ -0,0 +1,90 @@ +/* + * 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. + */ + +// Test that `disableInstrumentations=redis` works. + +'use strict'; + +if (process.env.GITHUB_ACTIONS === 'true' && process.platform === 'win32') { + console.log('# SKIP: GH Actions do not support docker services on Windows'); + process.exit(0); +} + +const redisVersion = require('redis/package.json').version; +const semver = require('semver'); +if (semver.lt(redisVersion, '4.0.0')) { + console.log('# SKIP: skipping redis.test.js tests <4.0.0'); + process.exit(0); +} + +const test = require('tape'); + +const apm = require('../../../'); +const { MockAPMServer } = require('../../_mock_apm_server'); + +test('disableInstrumentations=redis works', function (suite) { + let server; + let serverUrl; + + suite.test('setup', function (t) { + server = new MockAPMServer({ mockLambdaExtension: true }); + server.start(function (serverUrl_) { + serverUrl = serverUrl_; + t.comment('mock APM serverUrl: ' + serverUrl); + apm.start({ + serverUrl, + serviceName: 'test-redis-disabled', + metricsInterval: '0s', + centralConfig: false, + logLevel: 'off', + disableInstrumentations: 'redis', + }); + t.comment('APM agent started'); + t.end(); + }); + }); + + suite.test('using redis client does not create spans', function (t) { + server.clear(); + + const redis = require('redis'); + + const client = redis.createClient({ + socket: { + port: '6379', + host: process.env.REDIS_HOST, + }, + }); + client.connect(); + + const t0 = apm.startTransaction('t0'); + + // Because of `disableInstrumentaions` above, this client usage should + // *not* result in a span. + client.set('foo', 'bar').finally(async () => { + t0.end(); + client.quit(); + await apm.flush(); + + t.equal(server.events.length, 2); + t.ok(server.events[0].metadata); + t.equal( + server.events[1].transaction.name, + 't0', + 'only got the manual transaction', + ); + t.end(); + }); + }); + + suite.test('teardown', function (t) { + server.close(); + t.end(); + apm.destroy(); + }); + + suite.end(); +}); diff --git a/test/instrumentation/modules/redis.test.js b/test/instrumentation/modules/redis.test.js index c2a81a5875..9e277c8a08 100644 --- a/test/instrumentation/modules/redis.test.js +++ b/test/instrumentation/modules/redis.test.js @@ -27,9 +27,10 @@ if (semver.lt(process.version, '14.0.0')) { const agent = require('../../..').start({ serviceName: 'test-redis', captureExceptions: false, - metricsInterval: 0, + metricsInterval: '0s', centralConfig: false, spanCompressionEnabled: false, + logLevel: 'off', }); const redis = require('redis'); diff --git a/test/lambda/fixtures/express.js b/test/lambda/fixtures/express.js new file mode 100644 index 0000000000..f30ce26b05 --- /dev/null +++ b/test/lambda/fixtures/express.js @@ -0,0 +1,13 @@ +/* + * 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. + */ + +// A Lambda handler module that has a module name conflict with another +// module that the APM agent instruments: "express". + +'use strict'; +module.exports.foo = function origHandlerFuncName(event, context) { + return 'fake handler'; +}; diff --git a/test/lambda/wrapper.test.js b/test/lambda/wrapper.test.js index 4f59687694..eca199ffa5 100644 --- a/test/lambda/wrapper.test.js +++ b/test/lambda/wrapper.test.js @@ -6,16 +6,14 @@ 'use strict'; -const tape = require('tape'); const path = require('path'); -const Instrumentation = require('../../lib/instrumentation'); -const logging = require('../../lib/logging'); + +const tape = require('tape'); + const { getLambdaHandlerInfo } = require('../../lib/lambda'); +const apm = require('../..'); -const MODULES = Instrumentation.modules; -tape.test('unit tests for getLambdaHandlerInfo', function (suite) { - const logger = logging.createLogger('off'); - // minimal mocked instrumentation object for unit tests +tape.test('getLambdaHandlerInfo', function (suite) { suite.test('returns false-ish in non-lambda places', function (t) { t.ok(!getLambdaHandlerInfo()); t.end(); @@ -24,121 +22,80 @@ tape.test('unit tests for getLambdaHandlerInfo', function (suite) { suite.test('extracts info with expected env variables', function (t) { process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; - const handler = getLambdaHandlerInfo( - { - _HANDLER: 'lambda.bar', - LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'), - }, - MODULES, - logger, - ); + const handler = getLambdaHandlerInfo({ + _HANDLER: 'lambda.bar', + LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'), + }); t.equals( handler.filePath, path.resolve(__dirname, 'fixtures', 'lambda.js'), 'extracted handler file path', ); - t.equals(handler.module, 'lambda', 'extracted handler module'); - t.equals(handler.field, 'bar', 'extracted handler field'); + t.equals(handler.modName, 'lambda', 'extracted handler module'); + t.equals(handler.propPath, 'bar', 'extracted handler propPath'); t.end(); }); suite.test('extracts info with extended path, cjs extension', function (t) { process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; - const handler = getLambdaHandlerInfo( - { - _HANDLER: 'handlermodule.lambda.bar', - LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'), - }, - MODULES, - logger, - ); + const handler = getLambdaHandlerInfo({ + _HANDLER: 'handlermodule.lambda.bar', + LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'), + }); t.equals( handler.filePath, path.resolve(__dirname, 'fixtures', 'handlermodule.cjs'), 'extracted handler file path', ); - t.equals(handler.module, 'handlermodule', 'extracted handler module'); - t.equals(handler.field, 'lambda.bar', 'extracted handler field'); + t.equals(handler.modName, 'handlermodule', 'extracted handler module'); + t.equals(handler.propPath, 'lambda.bar', 'extracted handler propPath'); t.end(); }); suite.test('extracts info with expected env variables', function (t) { process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; - const handler = getLambdaHandlerInfo( - { - _HANDLER: 'lambda.bar', - LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'), - }, - MODULES, - logger, - ); + const handler = getLambdaHandlerInfo({ + _HANDLER: 'lambda.bar', + LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'), + }); t.equals( handler.filePath, path.resolve(__dirname, 'fixtures', 'lambda.js'), 'extracted handler file path', ); - t.equals(handler.module, 'lambda', 'extracted handler module'); - t.equals(handler.field, 'bar', 'extracted handler field'); + t.equals(handler.modName, 'lambda', 'extracted handler module'); + t.equals(handler.propPath, 'bar', 'extracted handler propPath'); t.end(); }); - suite.test( - 'returns no value if module name conflicts with already instrumented module', - function (t) { - process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; - const handler = getLambdaHandlerInfo( - { - _HANDLER: 'express.bar', - LAMBDA_TASK_ROOT: '/var/task', - }, - MODULES, - logger, - ); - t.equals(handler, undefined, 'no handler extracted'); - t.end(); - }, - ); - suite.test('no task root', function (t) { process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; - const handler = getLambdaHandlerInfo( - { - _HANDLER: 'foo.bar', - }, - MODULES, - logger, - ); + const handler = getLambdaHandlerInfo({ + _HANDLER: 'foo.bar', + }); t.ok(!handler, 'no value when task root missing'); t.end(); }); suite.test('no handler', function (t) { process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; - const handler = getLambdaHandlerInfo( - { - LAMBDA_TASK_ROOT: '/var/task', - }, - MODULES, - logger, - ); + const handler = getLambdaHandlerInfo({ + LAMBDA_TASK_ROOT: '/var/task', + }); t.ok(!handler, 'no value when handler missing'); t.end(); }); suite.test('malformed handler: too few', function (t) { process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; - const handler = getLambdaHandlerInfo( - { - LAMBDA_TASK_ROOT: '/var/task', - _HANDLER: 'foo', - }, - MODULES, - logger, - ); + const handler = getLambdaHandlerInfo({ + LAMBDA_TASK_ROOT: '/var/task', + _HANDLER: 'foo', + }); t.ok(!handler, 'no value for malformed handler too few'); t.end(); @@ -146,49 +103,44 @@ tape.test('unit tests for getLambdaHandlerInfo', function (suite) { suite.test('longer handler', function (t) { process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; - const handler = getLambdaHandlerInfo( - { - LAMBDA_TASK_ROOT: '/var/task', - _HANDLER: 'foo.baz.bar', - }, - MODULES, - logger, - ); + const handler = getLambdaHandlerInfo({ + LAMBDA_TASK_ROOT: '/var/task', + _HANDLER: 'foo.baz.bar', + }); t.equals( handler.filePath, path.resolve('/var', 'task', 'foo.cjs'), 'extracted handler file path', ); - t.equals(handler.module, 'foo', 'extracted handler module'); - t.equals(handler.field, 'baz.bar', 'extracted handler field'); + t.equals(handler.modName, 'foo', 'extracted handler module name'); + t.equals(handler.propPath, 'baz.bar', 'extracted handler property path'); t.end(); }); suite.end(); }); -tape.test('integration test', function (t) { +tape.test('lambda handler wrapping', function (t) { if (process.platform === 'win32') { t.pass('skipping for windows'); t.end(); return; } // fake the enviornment - process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; + process.env.AWS_LAMBDA_FUNCTION_NAME = 'mylambdafnname'; process.env.LAMBDA_TASK_ROOT = path.join(__dirname, 'fixtures'); process.env._HANDLER = 'lambda.foo'; // load and start The Real agent - require('../..').start({ - serviceName: 'lambda test', + apm.start({ + serviceName: 'lambda-test', breakdownMetrics: false, captureExceptions: false, - metricsInterval: 0, + metricsInterval: '0s', centralConfig: false, cloudProvider: 'none', - spanStackTraceMinDuration: 0, // Always have span stacktraces. - transport: function () {}, + disableSend: true, }); // load express after agent (for wrapper checking) @@ -208,5 +160,46 @@ tape.test('integration test', function (t) { 'wrappedStatic', 'express module was instrumented correctly', ); + + apm.destroy(); + t.end(); +}); + +tape.test('not wrapped if _HANDLER module is a name conflict', function (t) { + if (process.platform === 'win32') { + t.pass('skipping for windows'); + t.end(); + return; + } + // fake the enviornment + process.env.AWS_LAMBDA_FUNCTION_NAME = 'mylambdafnname'; + process.env.LAMBDA_TASK_ROOT = path.join(__dirname, 'fixtures'); + process.env._HANDLER = 'express.foo'; + + apm.start({ + serviceName: 'lambda-test', + breakdownMetrics: false, + captureExceptions: false, + metricsInterval: '0s', + centralConfig: false, + cloudProvider: 'none', + disableSend: true, + }); + + const express = require('express'); + t.equals( + express.static.name, + 'wrappedStatic', + 'express module was instrumented correctly', + ); + + const handler = require(path.join(__dirname, '/fixtures/express')).foo; + t.equals( + handler.name, + 'origHandlerFuncName', + 'handler function was not wrapped', + ); + + apm.destroy(); t.end(); });