elastic-apm-node
Advanced tools
Comparing version 4.0.0 to 4.1.0
@@ -384,2 +384,3 @@ /* | ||
interface PatchOptions { | ||
name: string; | ||
version: string | undefined; | ||
@@ -386,0 +387,0 @@ enabled: boolean; |
@@ -25,3 +25,3 @@ /* | ||
const { InflightEventSet } = require('./InflightEventSet'); | ||
const Instrumentation = require('./instrumentation'); | ||
const { Instrumentation } = require('./instrumentation'); | ||
const { elasticApmAwsLambda } = require('./lambda'); | ||
@@ -28,0 +28,0 @@ const Metrics = require('./metrics'); |
@@ -8,5 +8,9 @@ /* | ||
'use strict'; | ||
const URL = require('url').URL; | ||
const JSONBigInt = require('json-bigint'); | ||
const { httpRequest } = require('../http-request'); | ||
const DEFAULT_BASE_URL = new URL('/', 'http://metadata.google.internal:80'); | ||
/** | ||
@@ -48,2 +52,16 @@ * Checks for metadata server then fetches data | ||
res.on('end', function (data) { | ||
if (res.statusCode !== 200) { | ||
logger.debug('gcp metadata: unexpected statusCode: %s', res.statusCode); | ||
cb( | ||
new Error( | ||
'error fetching gcp metadata: unexpected statusCode: ' + | ||
res.statusCode, | ||
), | ||
); | ||
return; | ||
} | ||
// Note: We could also guard on the response having the | ||
// 'Metadata-Flavor: Google' header as done by: | ||
// https://github.com/googleapis/gcp-metadata/blob/v6.0.0/src/index.ts#L109-L112 | ||
let result; | ||
@@ -53,3 +71,3 @@ try { | ||
} catch (err) { | ||
logger.trace( | ||
logger.debug( | ||
'gcp metadata server responded, but there was an ' + | ||
@@ -84,72 +102,33 @@ 'error parsing the result: %o', | ||
* | ||
* Takes the response from a /computeMetadata/v1/?recursive=true | ||
* service request and formats it into the cloud metadata object | ||
* Convert a GCP Cloud Engine VM metadata response | ||
* (https://cloud.google.com/compute/docs/metadata/default-metadata-values) | ||
* to the APM intake cloud metadata object | ||
* (https://github.com/elastic/apm/blob/main/specs/agents/metadata.md#gcp-metadata). | ||
* | ||
* See discussion about big int values here: | ||
* https://github.com/googleapis/gcp-metadata#take-care-with-large-number-valued-properties | ||
* This implementation is using the same 'json-bigint' library as 'gcp-metadata'. | ||
*/ | ||
function formatMetadataStringIntoObject(string) { | ||
const data = JSON.parse(string); | ||
// cast string manipulation fields as strings "just in case" | ||
if (data.instance) { | ||
data.instance.machineType = String(data.instance.machineType); | ||
data.instance.zone = String(data.instance.zone); | ||
} | ||
const data = JSONBigInt.parse(string); | ||
// E.g., 'projects/513326162531/zones/us-west1-b' -> 'us-west1-b' | ||
const az = data.instance.zone.split('/').pop(); | ||
const metadata = { | ||
availability_zone: null, | ||
region: null, | ||
provider: 'gcp', | ||
instance: { | ||
id: null, | ||
id: data.instance.id.toString(), // We expect this to be a BigInt. | ||
name: data.instance.name, | ||
}, | ||
project: { | ||
id: data.project.projectId, | ||
}, | ||
availability_zone: az, | ||
region: az.slice(0, az.lastIndexOf('-')), // 'us-west1-b' -> 'us-west1' | ||
machine: { | ||
type: null, | ||
type: data.instance.machineType.split('/').pop(), | ||
}, | ||
provider: null, | ||
project: { | ||
id: null, | ||
name: null, | ||
}, | ||
}; | ||
metadata.availability_zone = null; | ||
metadata.region = null; | ||
if (data.instance && data.instance.zone) { | ||
// `projects/513326162531/zones/us-west1-b` manipuated into | ||
// `us-west1-b`, and then `us-west1` | ||
const regionWithZone = data.instance.zone.split('/').pop(); | ||
const parts = regionWithZone.split('-'); | ||
parts.pop(); | ||
metadata.region = parts.join('-'); | ||
metadata.availability_zone = regionWithZone; | ||
} | ||
if (data.instance) { | ||
metadata.instance = { | ||
id: String(data.instance.id), | ||
}; | ||
metadata.machine = { | ||
type: String(data.instance.machineType.split('/').pop()), | ||
}; | ||
} else { | ||
metadata.instance = { | ||
id: null, | ||
}; | ||
metadata.machine = { | ||
type: null, | ||
}; | ||
} | ||
metadata.provider = 'gcp'; | ||
if (data.project) { | ||
metadata.project = { | ||
id: String(data.project.numericProjectId), | ||
name: String(data.project.projectId), | ||
}; | ||
} else { | ||
metadata.project = { | ||
id: null, | ||
name: null, | ||
}; | ||
} | ||
return metadata; | ||
@@ -156,0 +135,0 @@ } |
@@ -21,3 +21,2 @@ /* | ||
var { Ids } = require('./ids'); | ||
var NamedArray = require('./named-array'); | ||
var Transaction = require('./transaction'); | ||
@@ -41,51 +40,71 @@ var { NoopTransaction } = require('./noop-transaction'); | ||
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' }, | ||
]; | ||
@@ -113,2 +132,3 @@ | ||
// '@aws-sdk/smithy-client': { instrumentImportMod: false }, | ||
'cassandra-driver': { instrumentImportMod: false }, | ||
express: { instrumentImportMod: false }, | ||
@@ -123,4 +143,155 @@ fastify: { instrumentImportMod: true }, | ||
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<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) { | ||
@@ -134,32 +305,4 @@ this._agent = agent; | ||
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(); | ||
} | ||
@@ -197,4 +340,3 @@ | ||
} | ||
for (const modName of modules) { | ||
for (const modPath of modules) { | ||
const type = typeof handler; | ||
@@ -205,7 +347,5 @@ if (type !== 'function' && type !== 'string') { | ||
} | ||
this._patches.add(modName, handler); | ||
this._patcherReg.add(modPath, handler); | ||
} | ||
this._startHook(); | ||
this._restartHooks(); | ||
}; | ||
@@ -216,7 +356,7 @@ | ||
for (const modName of modules) { | ||
this._patches.delete(modName, handler); | ||
for (const modPath of modules) { | ||
this._patcherReg.remove(modPath, handler); | ||
} | ||
this._startHook(); | ||
this._restartHooks(); | ||
}; | ||
@@ -227,11 +367,33 @@ | ||
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. | ||
@@ -275,6 +437,27 @@ // | ||
// 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); | ||
} | ||
@@ -284,3 +467,3 @@ } | ||
this._runCtxMgr.enable(); | ||
this._startHook(); | ||
this._restartHooks(); | ||
@@ -321,3 +504,4 @@ if (nodeHasInstrumentableFetch && this._isModuleEnabled('undici')) { | ||
} | ||
this._patcherReg.reset(); | ||
this._lambdaHandlerInfo = null; | ||
if (nodeHasInstrumentableFetch) { | ||
@@ -354,3 +538,3 @@ undiciInstr.uninstrumentUndici(); | ||
Instrumentation.prototype._startHook = function () { | ||
Instrumentation.prototype._restartHooks = function () { | ||
if (!this._started) { | ||
@@ -371,35 +555,48 @@ return; | ||
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._patches.keys, function ( | ||
exports, | ||
name, | ||
basedir, | ||
) { | ||
const enabled = self._isModuleEnabled(name); | ||
var pkg, version; | ||
this._ritmHook = new RitmHook( | ||
this._patcherReg.ritmModulesArg(), | ||
{ internals: true }, | ||
function (exports, modPath, basedir) { | ||
let version = undefined; | ||
const isHandlingLambda = | ||
self._lambdaHandlerInfo && self._lambdaHandlerInfo.module === name; | ||
// 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 || ''; | ||
} | ||
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), | ||
@@ -428,2 +625,7 @@ function (modExports, modName, modBaseDir) { | ||
Instrumentation.prototype._getPackageVersion = function (modName, modBaseDir) { | ||
if (this._cachedVerFromModBaseDir.has(modBaseDir)) { | ||
return this._cachedVerFromModBaseDir.get(modBaseDir); | ||
} | ||
let ver = undefined; | ||
try { | ||
@@ -434,5 +636,3 @@ const version = JSON.parse( | ||
if (typeof version === 'string') { | ||
return version; | ||
} else { | ||
return undefined; | ||
ver = version; | ||
} | ||
@@ -444,4 +644,6 @@ } catch (err) { | ||
); | ||
return undefined; | ||
} | ||
this._cachedVerFromModBaseDir.set(modBaseDir, ver); | ||
return ver; | ||
}; | ||
@@ -457,3 +659,3 @@ | ||
* `module.exports`) if `isImportMod=false`. | ||
* @param {string} name | ||
* @param {string} modPath | ||
* @param {string} version | ||
@@ -471,3 +673,3 @@ * @param {boolean} enabled Whether instrumentation is enabled for this module | ||
modExports, | ||
name, | ||
modPath, | ||
version, | ||
@@ -477,5 +679,5 @@ enabled, | ||
) { | ||
this._agent.logger.debug( | ||
this._log.debug( | ||
'instrumenting %s@%s module (enabled=%s, isImportMod=%s)', | ||
name, | ||
modPath, | ||
version, | ||
@@ -485,21 +687,13 @@ enabled, | ||
); | ||
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') { | ||
@@ -509,3 +703,3 @@ this._agent.logger.error( | ||
type, | ||
name, | ||
modPath, | ||
); | ||
@@ -515,4 +709,4 @@ continue; | ||
modExports = patch(modExports, this._agent, { | ||
name, | ||
modExports = patcher(modExports, this._agent, { | ||
name: modPath, | ||
version, | ||
@@ -940,1 +1134,5 @@ enabled, | ||
}; | ||
module.exports = { | ||
Instrumentation, | ||
}; |
@@ -9,25 +9,33 @@ /* | ||
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; | ||
} | ||
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; | ||
} | ||
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; | ||
} | ||
}; | ||
} | ||
module.exports = { | ||
createLambdaPatcher, | ||
}; |
@@ -214,2 +214,12 @@ /* | ||
/** | ||
* Tells if the command needs to be ingored | ||
* @param {import('@aws-sdk/types').Command} command the command sent by the SNS client | ||
* @param {any} config the agent configuration | ||
* @returns {boolean} false if the command should create a span | ||
*/ | ||
function s3ShouldIgnoreCommand(command, config) { | ||
return false; | ||
} | ||
module.exports = { | ||
@@ -220,2 +230,3 @@ S3_NAME: NAME, | ||
s3MiddlewareFactory, | ||
s3ShouldIgnoreCommand, | ||
}; |
@@ -13,2 +13,9 @@ /* | ||
const { | ||
DYNAMODB_NAME, | ||
DYNAMODB_TYPE, | ||
DYNAMODB_SUBTYPE, | ||
dynamoDBMiddlewareFactory, | ||
dynamoDBShouldIgnoreCommand, | ||
} = require('../@aws-sdk/client-dynamodb'); | ||
const { | ||
S3_NAME, | ||
@@ -18,4 +25,20 @@ S3_TYPE, | ||
s3MiddlewareFactory, | ||
s3ShouldIgnoreCommand, | ||
} = require('../@aws-sdk/client-s3'); | ||
const { | ||
SNS_NAME, | ||
SNS_TYPE, | ||
SNS_SUBTYPE, | ||
snsMiddlewareFactory, | ||
snsShouldIgnoreCommand, | ||
} = require('../@aws-sdk/client-sns'); | ||
const { | ||
SQS_NAME, | ||
SQS_TYPE, | ||
SQS_SUBTYPE, | ||
sqsMiddlewareFactory, | ||
sqsShouldIgnoreCommand, | ||
} = require('../@aws-sdk/client-sqs'); | ||
/** | ||
@@ -55,2 +78,9 @@ * We do alias them to a local type | ||
const clientsConfig = { | ||
DynamoDBClient: { | ||
NAME: DYNAMODB_NAME, | ||
TYPE: DYNAMODB_TYPE, | ||
SUBTYPE: DYNAMODB_SUBTYPE, | ||
factory: dynamoDBMiddlewareFactory, | ||
shouldIgnoreCommand: dynamoDBShouldIgnoreCommand, | ||
}, | ||
S3Client: { | ||
@@ -61,3 +91,18 @@ NAME: S3_NAME, | ||
factory: s3MiddlewareFactory, | ||
shouldIgnoreCommand: s3ShouldIgnoreCommand, | ||
}, | ||
SNSClient: { | ||
NAME: SNS_NAME, | ||
TYPE: SNS_TYPE, | ||
SUBTYPE: SNS_SUBTYPE, | ||
factory: snsMiddlewareFactory, | ||
shouldIgnoreCommand: snsShouldIgnoreCommand, | ||
}, | ||
SQSClient: { | ||
NAME: SQS_NAME, | ||
TYPE: SQS_TYPE, | ||
SUBTYPE: SQS_SUBTYPE, | ||
factory: sqsMiddlewareFactory, | ||
shouldIgnoreCommand: sqsShouldIgnoreCommand, | ||
}, | ||
}; | ||
@@ -115,2 +160,7 @@ | ||
const command = arguments[0]; | ||
if (clientConfig.shouldIgnoreCommand(command, agent._conf)) { | ||
return orig.apply(this, arguments); | ||
} | ||
const opName = opNameFromCommandName(command.constructor.name); | ||
@@ -117,0 +167,0 @@ const name = clientConfig.NAME + ' ' + opName; |
@@ -26,3 +26,4 @@ /* | ||
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; | ||
@@ -29,0 +30,0 @@ } |
@@ -811,17 +811,2 @@ /* | ||
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 | ||
@@ -847,3 +832,3 @@ // | ||
function getLambdaHandlerInfo(env, modules, logger) { | ||
function getLambdaHandlerInfo(env) { | ||
if ( | ||
@@ -854,3 +839,3 @@ !isLambdaExecutionEnvironment() || | ||
) { | ||
return; | ||
return null; | ||
} | ||
@@ -863,18 +848,6 @@ | ||
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]); | ||
@@ -884,4 +857,4 @@ | ||
filePath: handlerFilePath, | ||
module: handlerModule, | ||
field: handlerFunctionPath, | ||
modName: handlerModule, | ||
propPath: handlerFunctionPath, | ||
}; | ||
@@ -888,0 +861,0 @@ } |
@@ -116,4 +116,4 @@ /* | ||
* | ||
* @param {Object} res - Typically `res` is a Node.js `http.OutgoingMessage` | ||
* (https://nodejs.org/api/http.html#class-httpoutgoingmessage). | ||
* @param {Object} res - Typically `res` is a Node.js `http.ServerResponse` | ||
* (https://nodejs.org/api/http.html#class-httpserverresponse). | ||
* However, some cases (e.g. Lambda and Azure Functions instrumentation) | ||
@@ -120,0 +120,0 @@ * create a pseudo-res object that matches well enough for this function. |
{ | ||
"name": "elastic-apm-node", | ||
"version": "4.0.0", | ||
"version": "4.1.0", | ||
"description": "The official Elastic APM agent for Node.js", | ||
@@ -8,3 +8,2 @@ "main": "index.js", | ||
"scripts": { | ||
"backport": "backport", | ||
"docs:open": "PREVIEW=1 npm run docs:build", | ||
@@ -110,2 +109,3 @@ "docs:build": "./docs/scripts/build_docs.sh apm-agent-nodejs ./docs ./build", | ||
"import-in-the-middle": "1.4.2", | ||
"json-bigint": "^1.0.0", | ||
"lru-cache": "^10.0.1", | ||
@@ -131,3 +131,6 @@ "measured-reporting": "^1.51.1", | ||
"@apollo/server": "^4.2.2", | ||
"@aws-sdk/client-dynamodb": "^3.379.1", | ||
"@aws-sdk/client-s3": "^3.379.1", | ||
"@aws-sdk/client-sns": "^3.379.1", | ||
"@aws-sdk/client-sqs": "^3.379.1", | ||
"@aws-sdk/s3-request-presigner": "^3.379.1", | ||
@@ -147,3 +150,2 @@ "@babel/cli": "^7.8.4", | ||
"aws-sdk": "^2.1400.0", | ||
"backport": "^5.1.2", | ||
"benchmark": "^2.1.4", | ||
@@ -150,0 +152,0 @@ "bluebird": "^3.7.2", |
@@ -87,2 +87,3 @@ # Elastic APM Node.js Agent | ||
- [Release notes](https://www.elastic.co/guide/en/apm/agent/nodejs/current/release-notes.html) | ||
- [Supported Technologies](https://www.elastic.co/guide/en/apm/agent/nodejs/current/supported-technologies.html) describes the supported Node.js versions, which modules (and version ranges) are automatically traced, and other technologies. | ||
@@ -102,5 +103,4 @@ - [Configuring the agent](https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuring-the-agent.html) describes the different ways to configure the APM agent (via options to `apm.start(...)`, environment variables, or other mechanisms). | ||
- The ["main" branch](https://github.com/elastic/apm-agent-nodejs/tree/main) is being used for **4.x releases**. | ||
- The ["3.x" branch](https://github.com/elastic/apm-agent-nodejs/tree/3.x) is being used for **3.x maintenance releases**. The 3.x line will be [supported for 6 months](https://www.elastic.co/support/eol) after the release of v4.0.0. | ||
- The ["3.x" branch](https://github.com/elastic/apm-agent-nodejs/tree/3.x) is being used for **3.x maintenance releases**. The 3.x line will be [supported until 2024-03-07](https://www.elastic.co/support/eol) -- for 6 months after the release of v4.0.0. | ||
## Contributing | ||
@@ -107,0 +107,0 @@ |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
887100
23160
37
83
+ Addedjson-bigint@^1.0.0
+ Addedbignumber.js@9.1.2(transitive)
+ Addedjson-bigint@1.0.0(transitive)