Skip to content

Commit 3f03df1

Browse files
committed
feat(instrumentation-aws-lambda): add runtime-aware handler support with backward compatibility
Added runtime-aware handler support that automatically detects Node.js version from AWS_EXECUTION_ENV and adapts handler signatures accordingly. Node.js 24+ only supports Promise-based handlers, while Node.js 22 and lower support both callback and Promise-based handlers for backward compatibility.
1 parent 8e9513b commit 3f03df1

File tree

6 files changed

+244
-40
lines changed

6 files changed

+244
-40
lines changed

packages/instrumentation-aws-lambda/CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33

44
## [Unreleased]
55

6-
### Breaking Changes
6+
### Features
77

8-
* **instrumentation-aws-lambda:** Removed support for callback-based Lambda handlers. Only Promise-based handlers (async/await or functions returning Promises) are now supported. This aligns with AWS Lambda's deprecation of callbacks in Node.js 24 runtime. Users must migrate their handlers to Promise-based format before upgrading.
8+
* **instrumentation-aws-lambda:** Added runtime-aware handler support. The instrumentation automatically detects the Node.js runtime version from `AWS_EXECUTION_ENV` and adapts handler signatures accordingly:
9+
- **Node.js 24+**: Only Promise-based handlers are supported (callbacks deprecated by AWS Lambda)
10+
- **Node.js 22 and lower**: Both callback-based and Promise-based handlers are supported for backward compatibility
11+
This ensures seamless operation across different Node.js runtime versions while respecting AWS Lambda's deprecation of callbacks in Node.js 24+.
912

1013
## [0.60.1](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/instrumentation-aws-lambda-v0.60.0...instrumentation-aws-lambda-v0.60.1) (2025-11-24)
1114

packages/instrumentation-aws-lambda/README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,30 @@ npm install --save @opentelemetry/instrumentation-aws-lambda
2525

2626
## Important Notes
2727

28-
### Callback-Based Handlers Deprecated
28+
### Handler Types Supported
2929

30-
As of AWS Lambda Node.js 24 runtime, callback-based handlers are deprecated. This instrumentation now only supports Promise-based handlers (async/await or functions returning Promises). If you're using callback-based handlers, you must migrate to Promise-based handlers before upgrading to Node.js 24 runtime.
30+
This instrumentation automatically detects the Node.js runtime version and supports handlers accordingly:
3131

32-
Example migration:
32+
- **Node.js 24+**: Only Promise-based handlers are supported (callbacks are deprecated by AWS Lambda)
33+
- **Node.js 22 and lower**: Both callback-based and Promise-based handlers are supported for backward compatibility
34+
35+
The instrumentation detects the runtime version from the `AWS_EXECUTION_ENV` environment variable and adapts the handler signature accordingly. For Node.js 24+, the handler signature is `(event, context)`, while for Node.js 22 and lower, it supports both `(event, context, callback)` and `(event, context)`.
36+
37+
Example handlers:
3338
```js
34-
// ❌ Deprecated callback-based handler
39+
// Callback-based handler (Node.js 22 and lower only)
3540
exports.handler = function(event, context, callback) {
3641
callback(null, 'ok');
3742
};
3843

39-
// Promise-based handler
44+
// Promise-based handler (all Node.js versions)
4045
exports.handler = async function(event, context) {
4146
return 'ok';
4247
};
4348
```
4449

50+
**Note**: AWS Lambda has deprecated callback-based handlers in Node.js 24 runtime. It's recommended to migrate to Promise-based handlers when upgrading to Node.js 24+.
51+
4552
## Usage
4653

4754
Create a file to initialize the instrumentation, such as `lambda-wrapper.js`.

packages/instrumentation-aws-lambda/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opentelemetry/instrumentation-aws-lambda",
3-
"version": "0.60.1",
3+
"version": "0.61.0",
44
"description": "OpenTelemetry instrumentation for AWS Lambda function invocations",
55
"main": "build/src/index.js",
66
"types": "build/src/index.d.ts",

packages/instrumentation-aws-lambda/src/instrumentation.ts

Lines changed: 127 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { ATTR_FAAS_EXECUTION, ATTR_FAAS_ID } from './semconv-obsolete';
4545

4646
import {
4747
APIGatewayProxyEventHeaders,
48+
Callback,
4849
Context,
4950
Handler,
5051
StreamifyHandler,
@@ -70,6 +71,25 @@ export const AWS_HANDLER_STREAMING_SYMBOL = Symbol.for(
7071
);
7172
export const AWS_HANDLER_STREAMING_RESPONSE = 'response';
7273

74+
/**
75+
* Detects the Node.js runtime version from AWS_EXECUTION_ENV environment variable.
76+
* Returns the major version number (e.g., 24, 22, 20) or null if not detected.
77+
*/
78+
function getNodeRuntimeVersion(): number | null {
79+
const executionEnv = process.env.AWS_EXECUTION_ENV;
80+
if (!executionEnv) {
81+
return null;
82+
}
83+
84+
// AWS_EXECUTION_ENV format: AWS_Lambda_nodejs24.x, AWS_Lambda_nodejs22.x, etc.
85+
const match = executionEnv.match(/AWS_Lambda_nodejs(\d+)\./);
86+
if (match && match[1]) {
87+
return parseInt(match[1], 10);
88+
}
89+
90+
return null;
91+
}
92+
7393
export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstrumentationConfig> {
7494
private declare _traceForceFlusher?: () => Promise<void>;
7595
private declare _metricForceFlusher?: () => Promise<void>;
@@ -271,43 +291,105 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
271291
};
272292
}
273293

274-
return function patchedHandler(
275-
this: never,
276-
// The event can be a user type, it truly is any.
277-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
278-
event: any,
279-
context: Context
280-
) {
281-
_onRequest();
294+
// Determine runtime version to decide whether to support callbacks
295+
const nodeVersion = getNodeRuntimeVersion();
296+
const supportsCallbacks = nodeVersion === null || nodeVersion < 24;
282297

283-
const parent = plugin._determineParent(event, context);
298+
if (supportsCallbacks) {
299+
// Node.js 22 and lower: Support callback-based handlers for backward compatibility
300+
return function patchedHandlerWithCallback(
301+
this: never,
302+
// The event can be a user type, it truly is any.
303+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
304+
event: any,
305+
context: Context,
306+
callback?: Callback
307+
) {
308+
_onRequest();
284309

285-
const span = plugin._createSpanForRequest(
286-
event,
287-
context,
288-
requestIsColdStart,
289-
parent
290-
);
310+
const parent = plugin._determineParent(event, context);
291311

292-
plugin._applyRequestHook(span, event, context);
293-
294-
return otelContext.with(trace.setSpan(parent, span), () => {
295-
// Type assertion: Handler type from aws-lambda still includes callback parameter,
296-
// but we only support Promise-based handlers now (Node.js 24+)
297-
const maybePromise = safeExecuteInTheMiddle(
298-
() => (original as (event: any, context: Context) => Promise<any> | any).apply(this, [event, context]),
299-
error => {
300-
if (error != null) {
301-
// Exception thrown synchronously before resolving promise.
302-
plugin._applyResponseHook(span, error);
303-
plugin._endSpan(span, error, () => {});
304-
}
312+
const span = plugin._createSpanForRequest(
313+
event,
314+
context,
315+
requestIsColdStart,
316+
parent
317+
);
318+
319+
plugin._applyRequestHook(span, event, context);
320+
321+
return otelContext.with(trace.setSpan(parent, span), () => {
322+
// Support both callback-based and Promise-based handlers for backward compatibility
323+
if (callback) {
324+
const wrappedCallback = plugin._wrapCallback(callback, span);
325+
const maybePromise = safeExecuteInTheMiddle(
326+
() => original.apply(this, [event, context, wrappedCallback]),
327+
error => {
328+
if (error != null) {
329+
// Exception thrown synchronously before resolving callback / promise.
330+
plugin._applyResponseHook(span, error);
331+
plugin._endSpan(span, error, () => {});
332+
}
333+
}
334+
) as Promise<{}> | undefined;
335+
336+
return plugin._handlePromiseResult(span, maybePromise);
337+
} else {
338+
// Promise-based handler
339+
const maybePromise = safeExecuteInTheMiddle(
340+
() => (original as (event: any, context: Context) => Promise<any> | any).apply(this, [event, context]),
341+
error => {
342+
if (error != null) {
343+
// Exception thrown synchronously before resolving promise.
344+
plugin._applyResponseHook(span, error);
345+
plugin._endSpan(span, error, () => {});
346+
}
347+
}
348+
) as Promise<{}> | undefined;
349+
350+
return plugin._handlePromiseResult(span, maybePromise);
305351
}
306-
) as Promise<{}> | undefined;
352+
});
353+
};
354+
} else {
355+
// Node.js 24+: Only Promise-based handlers (callbacks deprecated)
356+
return function patchedHandler(
357+
this: never,
358+
// The event can be a user type, it truly is any.
359+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
360+
event: any,
361+
context: Context
362+
) {
363+
_onRequest();
307364

308-
return plugin._handlePromiseResult(span, maybePromise);
309-
});
310-
};
365+
const parent = plugin._determineParent(event, context);
366+
367+
const span = plugin._createSpanForRequest(
368+
event,
369+
context,
370+
requestIsColdStart,
371+
parent
372+
);
373+
374+
plugin._applyRequestHook(span, event, context);
375+
376+
return otelContext.with(trace.setSpan(parent, span), () => {
377+
// Promise-based handler only (Node.js 24+)
378+
const maybePromise = safeExecuteInTheMiddle(
379+
() => (original as (event: any, context: Context) => Promise<any> | any).apply(this, [event, context]),
380+
error => {
381+
if (error != null) {
382+
// Exception thrown synchronously before resolving promise.
383+
plugin._applyResponseHook(span, error);
384+
plugin._endSpan(span, error, () => {});
385+
}
386+
}
387+
) as Promise<{}> | undefined;
388+
389+
return plugin._handlePromiseResult(span, maybePromise);
390+
});
391+
};
392+
}
311393
}
312394

313395
private _createSpanForRequest(
@@ -435,6 +517,19 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
435517
return undefined;
436518
}
437519

520+
private _wrapCallback(original: Callback, span: Span): Callback {
521+
const plugin = this;
522+
return function wrappedCallback(this: never, err, res) {
523+
diag.debug('AWS Lambda instrumentation: Executing wrapped callback function');
524+
plugin._applyResponseHook(span, err, res);
525+
526+
plugin._endSpan(span, err, () => {
527+
diag.debug('AWS Lambda instrumentation: Executing original callback function');
528+
return original.apply(this, [err, res]);
529+
});
530+
};
531+
}
532+
438533
private _endSpan(
439534
span: Span,
440535
err: string | Error | null | undefined,

packages/instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,91 @@ describe('lambda handler', () => {
392392
assert.strictEqual(span.parentSpanContext?.spanId, undefined);
393393
});
394394

395+
describe('callback-based handler (Node.js 22 and lower)', () => {
396+
beforeEach(() => {
397+
// Simulate Node.js 22 runtime for backward compatibility testing
398+
process.env.AWS_EXECUTION_ENV = 'AWS_Lambda_nodejs22.x';
399+
});
400+
401+
afterEach(() => {
402+
delete process.env.AWS_EXECUTION_ENV;
403+
});
404+
405+
it('should export a valid span with callback handler', async () => {
406+
initializeHandler('lambda-test/sync.callbackHandler');
407+
408+
const result = await new Promise((resolve, reject) => {
409+
lambdaRequire('lambda-test/sync').callbackHandler(
410+
'arg',
411+
ctx,
412+
(err: Error, res: any) => {
413+
if (err) {
414+
reject(err);
415+
} else {
416+
resolve(res);
417+
}
418+
}
419+
);
420+
});
421+
assert.strictEqual(result, 'ok');
422+
const spans = memoryExporter.getFinishedSpans();
423+
const [span] = spans;
424+
assert.strictEqual(spans.length, 1);
425+
assertSpanSuccess(span);
426+
assert.strictEqual(span.parentSpanContext?.spanId, undefined);
427+
});
428+
429+
it('should record error with callback handler', async () => {
430+
initializeHandler('lambda-test/sync.callbackError');
431+
432+
let err: Error;
433+
try {
434+
await new Promise((resolve, reject) => {
435+
lambdaRequire('lambda-test/sync').callbackError(
436+
'arg',
437+
ctx,
438+
(error: Error, _res: any) => {
439+
if (error) {
440+
reject(error);
441+
} else {
442+
resolve({});
443+
}
444+
}
445+
);
446+
});
447+
} catch (e: any) {
448+
err = e;
449+
}
450+
assert.strictEqual(err!.message, 'handler error');
451+
const spans = memoryExporter.getFinishedSpans();
452+
const [span] = spans;
453+
assert.strictEqual(spans.length, 1);
454+
assertSpanFailure(span);
455+
assert.strictEqual(span.parentSpanContext?.spanId, undefined);
456+
});
457+
458+
it('context should have parent trace with callback handler', async () => {
459+
initializeHandler('lambda-test/sync.callbackContext');
460+
461+
const result = await new Promise((resolve, reject) => {
462+
lambdaRequire('lambda-test/sync').callbackContext(
463+
'arg',
464+
ctx,
465+
(err: Error, res: any) => {
466+
if (err) {
467+
reject(err);
468+
} else {
469+
resolve(res);
470+
}
471+
}
472+
);
473+
});
474+
const spans = memoryExporter.getFinishedSpans();
475+
const [span] = spans;
476+
assert.strictEqual(span.spanContext().traceId, result);
477+
});
478+
});
479+
395480
describe('with remote parent', () => {
396481
beforeEach(() => {
397482
propagation.disable();

packages/instrumentation-aws-lambda/test/lambda-test/sync.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
const api = require('@opentelemetry/api');
1717

18+
// Promise-based handlers (all Node.js versions)
1819
exports.handler = async function (event, context) {
1920
return 'ok';
2021
};
@@ -38,3 +39,16 @@ exports.callbackstringerror = async function (event, context) {
3839
exports.context = async function (event, context) {
3940
return api.trace.getSpan(api.context.active()).spanContext().traceId;
4041
};
42+
43+
// Callback-based handlers (Node.js 22 and lower only - for backward compatibility testing)
44+
exports.callbackHandler = function (event, context, callback) {
45+
callback(null, 'ok');
46+
};
47+
48+
exports.callbackError = function (event, context, callback) {
49+
callback(new Error('handler error'));
50+
};
51+
52+
exports.callbackContext = function (event, context, callback) {
53+
callback(null, api.trace.getSpan(api.context.active()).spanContext().traceId);
54+
};

0 commit comments

Comments
 (0)