Skip to content

Commit ce05f13

Browse files
feat(instrumentation-aws-lambda): support esm handlers and all other patterns
1 parent 80d0c74 commit ce05f13

File tree

17 files changed

+1014
-260
lines changed

17 files changed

+1014
-260
lines changed

plugins/node/opentelemetry-instrumentation-aws-lambda/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ const { registerInstrumentations } = require('@opentelemetry/instrumentation');
3535
const provider = new NodeTracerProvider();
3636
provider.register();
3737

38+
// Note that this needs to appear after the tracer provider is registered,
39+
// otherwise the instrumentation will not properly flush data after each
40+
// lambda invocation.
3841
registerInstrumentations({
3942
instrumentations: [
4043
new AwsLambdaInstrumentation({
@@ -46,7 +49,7 @@ registerInstrumentations({
4649

4750
In your Lambda function configuration, add or update the `NODE_OPTIONS` environment variable to require the wrapper, e.g.,
4851

49-
`NODE_OPTIONS=--require lambda-wrapper`
52+
`NODE_OPTIONS=--require lambda-wrapper --experimental-loader @opentelemetry/instrumentation/hook.mjs`
5053

5154
## AWS Lambda Instrumentation Options
5255

plugins/node/opentelemetry-instrumentation-aws-lambda/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
},
4444
"devDependencies": {
4545
"@opentelemetry/api": "^1.3.0",
46+
"@opentelemetry/contrib-test-utils": "^0.40.0",
4647
"@opentelemetry/core": "^1.8.0",
4748
"@opentelemetry/propagator-aws-xray": "^1.25.1",
4849
"@opentelemetry/propagator-aws-xray-lambda": "^0.52.1",

plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts

Lines changed: 93 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
import * as path from 'path';
18-
import * as fs from 'fs';
19-
2017
import {
2118
InstrumentationBase,
2219
InstrumentationNodeModuleDefinition,
@@ -55,7 +52,13 @@ import {
5552
import { AwsLambdaInstrumentationConfig, EventContextExtractor } from './types';
5653
/** @knipignore */
5754
import { PACKAGE_NAME, PACKAGE_VERSION } from './version';
58-
import { LambdaModule } from './internal-types';
55+
import {
56+
isInvalidHandler,
57+
moduleRootAndHandler,
58+
resolveHandler,
59+
splitHandlerString,
60+
tryPath,
61+
} from './user-function';
5962

6063
const headerGetter: TextMapGetter<APIGatewayProxyEventHeaders> = {
6164
keys(carrier): string[] {
@@ -88,69 +91,110 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
8891
);
8992
return [];
9093
}
94+
if (isInvalidHandler(handlerDef)) {
95+
this._diag.debug(
96+
'Skipping lambda instrumentation: _HANDLER/lambdaHandler is invalid.',
97+
{ taskRoot, handlerDef }
98+
);
99+
return [];
100+
}
91101

92-
const handler = path.basename(handlerDef);
93-
const moduleRoot = handlerDef.substr(0, handlerDef.length - handler.length);
94-
95-
const [module, functionName] = handler.split('.', 2);
96-
97-
// Lambda loads user function using an absolute path.
98-
let filename = path.resolve(taskRoot, moduleRoot, module);
99-
if (!filename.endsWith('.js')) {
100-
// its impossible to know in advance if the user has a cjs or js file.
101-
// check that the .js file exists otherwise fallback to next known possibility
102-
try {
103-
fs.statSync(`${filename}.js`);
104-
filename += '.js';
105-
} catch (e) {
106-
// fallback to .cjs
107-
filename += '.cjs';
108-
}
102+
const [moduleRoot, moduleAndHandler] = moduleRootAndHandler(handlerDef);
103+
const [module, handlerPath] = splitHandlerString(moduleAndHandler);
104+
105+
if (!module || !handlerPath) {
106+
this._diag.debug(
107+
'Skipping lambda instrumentation: _HANDLER/lambdaHandler is invalid.',
108+
{ taskRoot, handlerDef, moduleRoot, module, handlerPath }
109+
);
110+
return [];
111+
}
112+
113+
const filename = tryPath(taskRoot, moduleRoot, module);
114+
if (!filename) {
115+
this._diag.debug(
116+
'Skipping lambda instrumentation: _HANDLER/lambdaHandler and LAMBDA_TASK_ROOT did not resolve to a file.',
117+
{
118+
taskRoot,
119+
handlerDef,
120+
moduleRoot,
121+
module,
122+
handlerPath,
123+
}
124+
);
125+
return [];
109126
}
110127

111128
diag.debug('Instrumenting lambda handler', {
112129
taskRoot,
113130
handlerDef,
114-
handler,
131+
filename,
115132
moduleRoot,
116133
module,
117-
filename,
118-
functionName,
134+
handlerPath,
119135
});
120136

121137
const lambdaStartTime =
122138
this.getConfig().lambdaStartTime ||
123139
Date.now() - Math.floor(1000 * process.uptime());
124140

141+
const patch = (moduleExports: object) => {
142+
const [container, functionName] = resolveHandler(
143+
moduleExports,
144+
handlerPath
145+
);
146+
if (
147+
container == null ||
148+
functionName == null ||
149+
typeof container[functionName] !== 'function'
150+
) {
151+
this._diag.debug(
152+
'Skipping lambda instrumentation: _HANDLER/lambdaHandler did not resolve to a function.',
153+
{
154+
taskRoot,
155+
handlerDef,
156+
filename,
157+
moduleRoot,
158+
module,
159+
handlerPath,
160+
}
161+
);
162+
return moduleExports;
163+
}
164+
165+
if (isWrapped(container[functionName])) {
166+
this._unwrap(container, functionName);
167+
}
168+
this._wrap(container, functionName, this._getHandler(lambdaStartTime));
169+
return moduleExports;
170+
};
171+
const unpatch = (moduleExports?: object) => {
172+
if (moduleExports == null) return;
173+
const [container, functionName] = resolveHandler(
174+
moduleExports,
175+
handlerPath
176+
);
177+
if (
178+
container == null ||
179+
functionName == null ||
180+
typeof container[functionName] !== 'function'
181+
) {
182+
return;
183+
}
184+
185+
this._unwrap(container, functionName);
186+
};
187+
125188
return [
126189
new InstrumentationNodeModuleDefinition(
127-
// NB: The patching infrastructure seems to match names backwards, this must be the filename, while
128-
// InstrumentationNodeModuleFile must be the module name.
190+
// The patching infrastructure properly supports absolute paths when registering hooks but not when
191+
// actually matching against filenames when patching, so we need to provide a file instrumentation
192+
// that will actually match by using a relative path.
129193
filename,
130194
['*'],
131-
undefined,
132-
undefined,
133-
[
134-
new InstrumentationNodeModuleFile(
135-
module,
136-
['*'],
137-
(moduleExports: LambdaModule) => {
138-
if (isWrapped(moduleExports[functionName])) {
139-
this._unwrap(moduleExports, functionName);
140-
}
141-
this._wrap(
142-
moduleExports,
143-
functionName,
144-
this._getHandler(lambdaStartTime)
145-
);
146-
return moduleExports;
147-
},
148-
(moduleExports?: LambdaModule) => {
149-
if (moduleExports == null) return;
150-
this._unwrap(moduleExports, functionName);
151-
}
152-
),
153-
]
195+
patch,
196+
unpatch,
197+
[new InstrumentationNodeModuleFile(module, ['*'], patch, unpatch)]
154198
),
155199
];
156200
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* Adapted from https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.1.0/src/UserFunction.js
3+
*/
4+
5+
import * as path from 'path';
6+
import * as fs from 'fs';
7+
import { LambdaModule } from './internal-types';
8+
9+
const FUNCTION_EXPR = /^([^.]*)\.(.*)$/;
10+
const RELATIVE_PATH_SUBSTRING = '..';
11+
12+
/**
13+
* Break the full handler string into two pieces, the module root and the actual
14+
* handler string.
15+
* Given './somepath/something/module.nestedobj.handler' this returns
16+
* ['./somepath/something', 'module.nestedobj.handler']
17+
*/
18+
export function moduleRootAndHandler(
19+
fullHandlerString: string
20+
): [moduleRoot: string, handler: string] {
21+
const handlerString = path.basename(fullHandlerString);
22+
const moduleRoot = fullHandlerString.substring(
23+
0,
24+
fullHandlerString.indexOf(handlerString)
25+
);
26+
return [moduleRoot, handlerString];
27+
}
28+
29+
/**
30+
* Split the handler string into two pieces: the module name and the path to
31+
* the handler function.
32+
*/
33+
export function splitHandlerString(
34+
handler: string
35+
): [module: string | undefined, functionPath: string | undefined] {
36+
const match = handler.match(FUNCTION_EXPR);
37+
if (match && match.length === 3) {
38+
return [match[1], match[2]];
39+
} else {
40+
return [undefined, undefined];
41+
}
42+
}
43+
44+
/**
45+
* Resolve the user's handler function key and its containing object from the module.
46+
*/
47+
export function resolveHandler(
48+
object: object,
49+
nestedProperty: string
50+
): [container: LambdaModule | undefined, handlerKey: string | undefined] {
51+
const nestedPropertyKeys = nestedProperty.split('.');
52+
const handlerKey = nestedPropertyKeys.pop();
53+
54+
const container = nestedPropertyKeys.reduce<object | undefined>(
55+
(nested, key) => {
56+
return nested && (nested as Partial<Record<string, object>>)[key];
57+
},
58+
object
59+
);
60+
61+
if (container) {
62+
return [container as LambdaModule, handlerKey];
63+
} else {
64+
return [undefined, undefined];
65+
}
66+
}
67+
68+
/**
69+
* Attempt to determine the user's module path.
70+
*/
71+
export function tryPath(
72+
appRoot: string,
73+
moduleRoot: string,
74+
module: string
75+
): string | undefined {
76+
const lambdaStylePath = path.resolve(appRoot, moduleRoot, module);
77+
78+
const extensionless = fs.existsSync(lambdaStylePath);
79+
if (extensionless) {
80+
return lambdaStylePath;
81+
}
82+
83+
const extensioned =
84+
(fs.existsSync(lambdaStylePath + '.js') && lambdaStylePath + '.js') ||
85+
(fs.existsSync(lambdaStylePath + '.mjs') && lambdaStylePath + '.mjs') ||
86+
(fs.existsSync(lambdaStylePath + '.cjs') && lambdaStylePath + '.cjs');
87+
if (extensioned) {
88+
return extensioned;
89+
}
90+
91+
try {
92+
const nodeStylePath = require.resolve(module, {
93+
paths: [appRoot, moduleRoot],
94+
});
95+
return nodeStylePath;
96+
} catch {
97+
return undefined;
98+
}
99+
}
100+
101+
export function isInvalidHandler(fullHandlerString: string): boolean {
102+
if (fullHandlerString.includes(RELATIVE_PATH_SUBSTRING)) {
103+
return true;
104+
} else {
105+
return false;
106+
}
107+
}

0 commit comments

Comments
 (0)