diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index e1f46faef5..a829d9a86a 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -33,6 +33,24 @@ Notes: See the <> guide. +==== Unreleased + +[float] +===== Breaking changes + +[float] +===== Features + +[float] +===== Bug fixes + +* Fix AWS Lambda instrumentation to work with a "handler" string that includes + a period (`.`) in the module path. E.g. the leading `.` in `Handler: ./src/functions/myfunc/handler.main`. ({issues}4293[#4293]). + +[float] +===== Chores + + [[release-notes-4.8.0]] ==== 4.8.0 - 2024/10/08 diff --git a/lib/instrumentation/index.js b/lib/instrumentation/index.js index 8af059cff2..626bf5493d 100644 --- a/lib/instrumentation/index.js +++ b/lib/instrumentation/index.js @@ -374,7 +374,7 @@ Instrumentation.prototype.clearPatches = function (modules) { // If in a Lambda environment, find its handler and add a patcher for it. Instrumentation.prototype._maybeLoadLambdaPatcher = function () { - let lambdaHandlerInfo = getLambdaHandlerInfo(process.env); + let lambdaHandlerInfo = getLambdaHandlerInfo(process.env, this._log); if (lambdaHandlerInfo && this._patcherReg.has(lambdaHandlerInfo.modName)) { this._log.warn( diff --git a/lib/lambda.js b/lib/lambda.js index 587e50fbae..5255c09fba 100644 --- a/lib/lambda.js +++ b/lib/lambda.js @@ -817,18 +817,34 @@ function isLambdaExecutionEnvironment() { // .mjs file extension (which indicates an ECMAScript/import module, which the // agent does not support. // -// @param string taskRoot -// @param string handlerModule -// @return string -function getFilePath(taskRoot, handlerModule) { - let filePath = path.resolve(taskRoot, `${handlerModule}.js`); - if (!fs.existsSync(filePath)) { - filePath = path.resolve(taskRoot, `${handlerModule}.cjs`); +// TODO: support "extensionless"? per https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.2.1/src/UserFunction.js#L149 Is this for a dir/index.js? +// TODO: support ESM and .mjs +// +// @param {string} taskRoot +// @param {string} moduleRoot - The subdir under `taskRoot` holding the module. +// @param {string} module - The module name. +// @return {string | null} +function getFilePath(taskRoot, moduleRoot, module) { + const lambdaStylePath = path.resolve(taskRoot, moduleRoot, module); + if (fs.existsSync(lambdaStylePath + '.js')) { + return lambdaStylePath + '.js'; + } else if (fs.existsSync(lambdaStylePath + '.cjs')) { + return lambdaStylePath + '.cjs'; + } else { + return null; } - return filePath; } -function getLambdaHandlerInfo(env) { +/** + * Gather module and export info for the Lambda "handler" string. + * + * Compare to the Node.js Lambda runtime's equivalent processing here: + * https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.2.1/src/UserFunction.js#L288 + * + * @param {object} env - The process environment. + * @param {any} [logger] - Optional logger for trace/warn log output. + */ +function getLambdaHandlerInfo(env, logger) { if ( !isLambdaExecutionEnvironment() || !env._HANDLER || @@ -837,22 +853,48 @@ function getLambdaHandlerInfo(env) { return null; } - // extract module name and "path" from handler using the same regex as the runtime - // from https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/c31c41ffe5f2f03ae9e8589b96f3b005e2bb8a4a/src/utils/UserFunction.ts#L21 - const functionExpression = /^([^.]*)\.(.*)$/; - const match = env._HANDLER.match(functionExpression); + // Dev Note: This intentionally uses some of the same var names at + // https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.2.1/src/UserFunction.js#L288 + const fullHandlerString = env._HANDLER; + const moduleAndHandler = path.basename(fullHandlerString); + const moduleRoot = fullHandlerString.substring( + 0, + fullHandlerString.indexOf(moduleAndHandler), + ); + const FUNCTION_EXPR = /^([^.]*)\.(.*)$/; + const match = moduleAndHandler.match(FUNCTION_EXPR); if (!match || match.length !== 3) { + if (logger) { + logger.warn( + { fullHandlerString, moduleAndHandler }, + 'Lambda handler string did not match FUNCTION_EXPR', + ); + } + return null; + } + const module = match[1]; + const handlerPath = match[2]; + + const moduleAbsPath = getFilePath(env.LAMBDA_TASK_ROOT, moduleRoot, module); + if (!moduleAbsPath) { + if (logger) { + logger.warn( + { fullHandlerString, moduleRoot, module }, + 'could not find Lambda handler module file (ESM not yet supported)', + ); + } return null; } - const handlerModule = match[1].split('/').pop(); - const handlerFunctionPath = match[2]; - const handlerFilePath = getFilePath(env.LAMBDA_TASK_ROOT, match[1]); - return { - filePath: handlerFilePath, - modName: handlerModule, - propPath: handlerFunctionPath, + const lambdaHandlerInfo = { + filePath: moduleAbsPath, + modName: module, + propPath: handlerPath, }; + if (logger) { + logger.trace({ fullHandlerString, lambdaHandlerInfo }, 'lambdaHandlerInfo'); + } + return lambdaHandlerInfo; } function lowerCaseObjectKeys(obj) { diff --git a/test/lambda/fixtures/foo.js b/test/lambda/fixtures/foo.js new file mode 100644 index 0000000000..d678e19bdb --- /dev/null +++ b/test/lambda/fixtures/foo.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. + */ + +'use strict'; + +module.exports = { + bar: function (event, context) { + return 'fake handler'; + }, +}; diff --git a/test/lambda/fixtures/handlermodule.cjs b/test/lambda/fixtures/handlermodule.cjs new file mode 100644 index 0000000000..97aca045dd --- /dev/null +++ b/test/lambda/fixtures/handlermodule.cjs @@ -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. + */ + +module.exports = { + lambda: { + foo: function myHandler(event, context) { + return 'hi'; + }, + }, +}; diff --git a/test/lambda/wrapper.test.js b/test/lambda/wrapper.test.js index eca199ffa5..d901e83ab6 100644 --- a/test/lambda/wrapper.test.js +++ b/test/lambda/wrapper.test.js @@ -37,6 +37,24 @@ tape.test('getLambdaHandlerInfo', function (suite) { t.end(); }); + suite.test('extracts info with leading "./" on _HANDLER path', function (t) { + process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; + + const info = getLambdaHandlerInfo({ + _HANDLER: './lambda.bar', + LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'), + }); + + t.equals( + info.filePath, + path.resolve(__dirname, 'fixtures', 'lambda.js'), + 'extracted handler file path', + ); + t.equals(info.modName, 'lambda', 'extracted handler module'); + t.equals(info.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'; @@ -93,7 +111,7 @@ tape.test('getLambdaHandlerInfo', function (suite) { suite.test('malformed handler: too few', function (t) { process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; const handler = getLambdaHandlerInfo({ - LAMBDA_TASK_ROOT: '/var/task', + LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'), _HANDLER: 'foo', }); @@ -104,13 +122,13 @@ tape.test('getLambdaHandlerInfo', function (suite) { suite.test('longer handler', function (t) { process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo'; const handler = getLambdaHandlerInfo({ - LAMBDA_TASK_ROOT: '/var/task', + LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'), _HANDLER: 'foo.baz.bar', }); t.equals( handler.filePath, - path.resolve('/var', 'task', 'foo.cjs'), + path.resolve(__dirname, 'fixtures', 'foo.js'), 'extracted handler file path', ); t.equals(handler.modName, 'foo', 'extracted handler module name');