Skip to content

Commit 41add4d

Browse files
authored
fix(lambda): fix lambda instr to work with a '.' in the handler module path (#4294)
Before this if the _HANDLER string had a '.' in the module path (the part before the 'moduleName.functionExport'), then the parsing of that handler string would silently produce bogus 'lambdaHandlerInfo' that would result in a RITM path that would never actually get loaded, hence no Lambda instrumentation. Fixes: #4293
1 parent d321a91 commit 41add4d

File tree

6 files changed

+128
-24
lines changed

6 files changed

+128
-24
lines changed

CHANGELOG.asciidoc

+18
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,24 @@ Notes:
3333
3434
See the <<upgrade-to-v4>> guide.
3535
36+
==== Unreleased
37+
38+
[float]
39+
===== Breaking changes
40+
41+
[float]
42+
===== Features
43+
44+
[float]
45+
===== Bug fixes
46+
47+
* Fix AWS Lambda instrumentation to work with a "handler" string that includes
48+
a period (`.`) in the module path. E.g. the leading `.` in `Handler: ./src/functions/myfunc/handler.main`. ({issues}4293[#4293]).
49+
50+
[float]
51+
===== Chores
52+
53+
3654
[[release-notes-4.8.0]]
3755
==== 4.8.0 - 2024/10/08
3856

lib/instrumentation/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ Instrumentation.prototype.clearPatches = function (modules) {
374374

375375
// If in a Lambda environment, find its handler and add a patcher for it.
376376
Instrumentation.prototype._maybeLoadLambdaPatcher = function () {
377-
let lambdaHandlerInfo = getLambdaHandlerInfo(process.env);
377+
let lambdaHandlerInfo = getLambdaHandlerInfo(process.env, this._log);
378378

379379
if (lambdaHandlerInfo && this._patcherReg.has(lambdaHandlerInfo.modName)) {
380380
this._log.warn(

lib/lambda.js

+62-20
Original file line numberDiff line numberDiff line change
@@ -817,18 +817,34 @@ function isLambdaExecutionEnvironment() {
817817
// .mjs file extension (which indicates an ECMAScript/import module, which the
818818
// agent does not support.
819819
//
820-
// @param string taskRoot
821-
// @param string handlerModule
822-
// @return string
823-
function getFilePath(taskRoot, handlerModule) {
824-
let filePath = path.resolve(taskRoot, `${handlerModule}.js`);
825-
if (!fs.existsSync(filePath)) {
826-
filePath = path.resolve(taskRoot, `${handlerModule}.cjs`);
820+
// 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?
821+
// TODO: support ESM and .mjs
822+
//
823+
// @param {string} taskRoot
824+
// @param {string} moduleRoot - The subdir under `taskRoot` holding the module.
825+
// @param {string} module - The module name.
826+
// @return {string | null}
827+
function getFilePath(taskRoot, moduleRoot, module) {
828+
const lambdaStylePath = path.resolve(taskRoot, moduleRoot, module);
829+
if (fs.existsSync(lambdaStylePath + '.js')) {
830+
return lambdaStylePath + '.js';
831+
} else if (fs.existsSync(lambdaStylePath + '.cjs')) {
832+
return lambdaStylePath + '.cjs';
833+
} else {
834+
return null;
827835
}
828-
return filePath;
829836
}
830837

831-
function getLambdaHandlerInfo(env) {
838+
/**
839+
* Gather module and export info for the Lambda "handler" string.
840+
*
841+
* Compare to the Node.js Lambda runtime's equivalent processing here:
842+
* https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.2.1/src/UserFunction.js#L288
843+
*
844+
* @param {object} env - The process environment.
845+
* @param {any} [logger] - Optional logger for trace/warn log output.
846+
*/
847+
function getLambdaHandlerInfo(env, logger) {
832848
if (
833849
!isLambdaExecutionEnvironment() ||
834850
!env._HANDLER ||
@@ -837,22 +853,48 @@ function getLambdaHandlerInfo(env) {
837853
return null;
838854
}
839855

840-
// extract module name and "path" from handler using the same regex as the runtime
841-
// from https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/c31c41ffe5f2f03ae9e8589b96f3b005e2bb8a4a/src/utils/UserFunction.ts#L21
842-
const functionExpression = /^([^.]*)\.(.*)$/;
843-
const match = env._HANDLER.match(functionExpression);
856+
// Dev Note: This intentionally uses some of the same var names at
857+
// https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.2.1/src/UserFunction.js#L288
858+
const fullHandlerString = env._HANDLER;
859+
const moduleAndHandler = path.basename(fullHandlerString);
860+
const moduleRoot = fullHandlerString.substring(
861+
0,
862+
fullHandlerString.indexOf(moduleAndHandler),
863+
);
864+
const FUNCTION_EXPR = /^([^.]*)\.(.*)$/;
865+
const match = moduleAndHandler.match(FUNCTION_EXPR);
844866
if (!match || match.length !== 3) {
867+
if (logger) {
868+
logger.warn(
869+
{ fullHandlerString, moduleAndHandler },
870+
'Lambda handler string did not match FUNCTION_EXPR',
871+
);
872+
}
873+
return null;
874+
}
875+
const module = match[1];
876+
const handlerPath = match[2];
877+
878+
const moduleAbsPath = getFilePath(env.LAMBDA_TASK_ROOT, moduleRoot, module);
879+
if (!moduleAbsPath) {
880+
if (logger) {
881+
logger.warn(
882+
{ fullHandlerString, moduleRoot, module },
883+
'could not find Lambda handler module file (ESM not yet supported)',
884+
);
885+
}
845886
return null;
846887
}
847-
const handlerModule = match[1].split('/').pop();
848-
const handlerFunctionPath = match[2];
849-
const handlerFilePath = getFilePath(env.LAMBDA_TASK_ROOT, match[1]);
850888

851-
return {
852-
filePath: handlerFilePath,
853-
modName: handlerModule,
854-
propPath: handlerFunctionPath,
889+
const lambdaHandlerInfo = {
890+
filePath: moduleAbsPath,
891+
modName: module,
892+
propPath: handlerPath,
855893
};
894+
if (logger) {
895+
logger.trace({ fullHandlerString, lambdaHandlerInfo }, 'lambdaHandlerInfo');
896+
}
897+
return lambdaHandlerInfo;
856898
}
857899

858900
function lowerCaseObjectKeys(obj) {

test/lambda/fixtures/foo.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and other contributors where applicable.
3+
* Licensed under the BSD 2-Clause License; you may not use this file except in
4+
* compliance with the BSD 2-Clause License.
5+
*/
6+
7+
'use strict';
8+
9+
module.exports = {
10+
bar: function (event, context) {
11+
return 'fake handler';
12+
},
13+
};
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and other contributors where applicable.
3+
* Licensed under the BSD 2-Clause License; you may not use this file except in
4+
* compliance with the BSD 2-Clause License.
5+
*/
6+
7+
module.exports = {
8+
lambda: {
9+
foo: function myHandler(event, context) {
10+
return 'hi';
11+
},
12+
},
13+
};

test/lambda/wrapper.test.js

+21-3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ tape.test('getLambdaHandlerInfo', function (suite) {
3737
t.end();
3838
});
3939

40+
suite.test('extracts info with leading "./" on _HANDLER path', function (t) {
41+
process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo';
42+
43+
const info = getLambdaHandlerInfo({
44+
_HANDLER: './lambda.bar',
45+
LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'),
46+
});
47+
48+
t.equals(
49+
info.filePath,
50+
path.resolve(__dirname, 'fixtures', 'lambda.js'),
51+
'extracted handler file path',
52+
);
53+
t.equals(info.modName, 'lambda', 'extracted handler module');
54+
t.equals(info.propPath, 'bar', 'extracted handler propPath');
55+
t.end();
56+
});
57+
4058
suite.test('extracts info with extended path, cjs extension', function (t) {
4159
process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo';
4260

@@ -93,7 +111,7 @@ tape.test('getLambdaHandlerInfo', function (suite) {
93111
suite.test('malformed handler: too few', function (t) {
94112
process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo';
95113
const handler = getLambdaHandlerInfo({
96-
LAMBDA_TASK_ROOT: '/var/task',
114+
LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'),
97115
_HANDLER: 'foo',
98116
});
99117

@@ -104,13 +122,13 @@ tape.test('getLambdaHandlerInfo', function (suite) {
104122
suite.test('longer handler', function (t) {
105123
process.env.AWS_LAMBDA_FUNCTION_NAME = 'foo';
106124
const handler = getLambdaHandlerInfo({
107-
LAMBDA_TASK_ROOT: '/var/task',
125+
LAMBDA_TASK_ROOT: path.resolve(__dirname, 'fixtures'),
108126
_HANDLER: 'foo.baz.bar',
109127
});
110128

111129
t.equals(
112130
handler.filePath,
113-
path.resolve('/var', 'task', 'foo.cjs'),
131+
path.resolve(__dirname, 'fixtures', 'foo.js'),
114132
'extracted handler file path',
115133
);
116134
t.equals(handler.modName, 'foo', 'extracted handler module name');

0 commit comments

Comments
 (0)