Skip to content

Commit 4676273

Browse files
feat(instrumentation-aws-lambda): support esm handlers and all other patterns
1 parent 2b117bb commit 4676273

File tree

17 files changed

+1021
-277
lines changed

17 files changed

+1021
-277
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
@@ -44,6 +44,7 @@
4444
},
4545
"devDependencies": {
4646
"@opentelemetry/api": "^1.3.0",
47+
"@opentelemetry/contrib-test-utils": "^0.40.0",
4748
"@opentelemetry/core": "^1.8.0",
4849
"@opentelemetry/sdk-metrics": "^1.8.0",
4950
"@opentelemetry/sdk-trace-base": "^1.8.0",

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

Lines changed: 95 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import * as path from 'path';
18-
import * as fs from 'fs';
17+
import { env } from 'process';
1918

2019
import {
2120
InstrumentationBase,
@@ -58,8 +57,13 @@ import {
5857

5958
import { AwsLambdaInstrumentationConfig, EventContextExtractor } from './types';
6059
import { PACKAGE_NAME, PACKAGE_VERSION } from './version';
61-
import { env } from 'process';
62-
import { LambdaModule } from './internal-types';
60+
import {
61+
isInvalidHandler,
62+
moduleRootAndHandler,
63+
resolveHandler,
64+
splitHandlerString,
65+
tryPath,
66+
} from './user-function';
6367

6468
const awsPropagator = new AWSXRayPropagator();
6569
const headerGetter: TextMapGetter<APIGatewayProxyEventHeaders> = {
@@ -92,6 +96,7 @@ export class AwsLambdaInstrumentation extends InstrumentationBase {
9296
this._config.disableAwsContextPropagation = true;
9397
}
9498
}
99+
this._traceForceFlusher = this._traceForceFlush(trace.getTracerProvider());
95100
}
96101

97102
override setConfig(config: AwsLambdaInstrumentationConfig = {}) {
@@ -110,61 +115,106 @@ export class AwsLambdaInstrumentation extends InstrumentationBase {
110115
);
111116
return [];
112117
}
118+
if (isInvalidHandler(handlerDef)) {
119+
this._diag.debug(
120+
'Skipping lambda instrumentation: _HANDLER/lambdaHandler is invalid.',
121+
{ taskRoot, handlerDef }
122+
);
123+
return [];
124+
}
113125

114-
const handler = path.basename(handlerDef);
115-
const moduleRoot = handlerDef.substr(0, handlerDef.length - handler.length);
116-
117-
const [module, functionName] = handler.split('.', 2);
118-
119-
// Lambda loads user function using an absolute path.
120-
let filename = path.resolve(taskRoot, moduleRoot, module);
121-
if (!filename.endsWith('.js')) {
122-
// its impossible to know in advance if the user has a cjs or js file.
123-
// check that the .js file exists otherwise fallback to next known possibility
124-
try {
125-
fs.statSync(`${filename}.js`);
126-
filename += '.js';
127-
} catch (e) {
128-
// fallback to .cjs
129-
filename += '.cjs';
130-
}
126+
const [moduleRoot, moduleAndHandler] = moduleRootAndHandler(handlerDef);
127+
const [module, handlerPath] = splitHandlerString(moduleAndHandler);
128+
129+
if (!module || !handlerPath) {
130+
this._diag.debug(
131+
'Skipping lambda instrumentation: _HANDLER/lambdaHandler is invalid.',
132+
{ taskRoot, handlerDef, moduleRoot, module, handlerPath }
133+
);
134+
return [];
135+
}
136+
137+
const filename = tryPath(taskRoot, moduleRoot, module);
138+
if (!filename) {
139+
this._diag.debug(
140+
'Skipping lambda instrumentation: _HANDLER/lambdaHandler and LAMBDA_TASK_ROOT did not resolve to a file.',
141+
{
142+
taskRoot,
143+
handlerDef,
144+
moduleRoot,
145+
module,
146+
handlerPath,
147+
}
148+
);
149+
return [];
131150
}
132151

133152
diag.debug('Instrumenting lambda handler', {
134153
taskRoot,
135154
handlerDef,
136-
handler,
155+
filename,
137156
moduleRoot,
138157
module,
139-
filename,
140-
functionName,
158+
handlerPath,
141159
});
142160

161+
const patch = (moduleExports: object) => {
162+
const [container, functionName] = resolveHandler(
163+
moduleExports,
164+
handlerPath
165+
);
166+
if (
167+
container == null ||
168+
functionName == null ||
169+
typeof container[functionName] !== 'function'
170+
) {
171+
this._diag.debug(
172+
'Skipping lambda instrumentation: _HANDLER/lambdaHandler did not resolve to a function.',
173+
{
174+
taskRoot,
175+
handlerDef,
176+
filename,
177+
moduleRoot,
178+
module,
179+
handlerPath,
180+
}
181+
);
182+
return moduleExports;
183+
}
184+
185+
if (isWrapped(container[functionName])) {
186+
this._unwrap(container, functionName);
187+
}
188+
this._wrap(container, functionName, this._getHandler());
189+
return moduleExports;
190+
};
191+
const unpatch = (moduleExports?: object) => {
192+
if (moduleExports == null) return;
193+
const [container, functionName] = resolveHandler(
194+
moduleExports,
195+
handlerPath
196+
);
197+
if (
198+
container == null ||
199+
functionName == null ||
200+
typeof container[functionName] !== 'function'
201+
) {
202+
return;
203+
}
204+
205+
this._unwrap(container, functionName);
206+
};
207+
143208
return [
144209
new InstrumentationNodeModuleDefinition(
145-
// NB: The patching infrastructure seems to match names backwards, this must be the filename, while
146-
// InstrumentationNodeModuleFile must be the module name.
210+
// The patching infrastructure properly supports absolute paths when registering hooks but not when
211+
// actually matching against filenames when patching, so we need to provide a file instrumentation
212+
// that will actually match by using a relative path.
147213
filename,
148214
['*'],
149-
undefined,
150-
undefined,
151-
[
152-
new InstrumentationNodeModuleFile(
153-
module,
154-
['*'],
155-
(moduleExports: LambdaModule) => {
156-
if (isWrapped(moduleExports[functionName])) {
157-
this._unwrap(moduleExports, functionName);
158-
}
159-
this._wrap(moduleExports, functionName, this._getHandler());
160-
return moduleExports;
161-
},
162-
(moduleExports?: LambdaModule) => {
163-
if (moduleExports == null) return;
164-
this._unwrap(moduleExports, functionName);
165-
}
166-
),
167-
]
215+
patch,
216+
unpatch,
217+
[new InstrumentationNodeModuleFile(module, ['*'], patch, unpatch)]
168218
),
169219
];
170220
}
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)