diff --git a/packages/instrumentation-aws-lambda/CHANGELOG.md b/packages/instrumentation-aws-lambda/CHANGELOG.md index 14937a6336..0b52873cc7 100644 --- a/packages/instrumentation-aws-lambda/CHANGELOG.md +++ b/packages/instrumentation-aws-lambda/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog +## [Unreleased] + +### Features + +* **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: + * **Node.js 24+**: Only Promise-based handlers are supported (callbacks deprecated by AWS Lambda) + * **Node.js 22 and lower**: Both callback-based and Promise-based handlers are supported for backward compatibility + This ensures seamless operation across different Node.js runtime versions while respecting AWS Lambda's removal of callbacks in Node.js 24+. + ## [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) diff --git a/packages/instrumentation-aws-lambda/README.md b/packages/instrumentation-aws-lambda/README.md index 0ff8480705..f8c1ceeaa7 100644 --- a/packages/instrumentation-aws-lambda/README.md +++ b/packages/instrumentation-aws-lambda/README.md @@ -23,6 +23,33 @@ npm install --save @opentelemetry/instrumentation-aws-lambda - This package will instrument the lambda execution regardless of versions. +## Important Notes + +### Handler Types Supported + +This instrumentation automatically detects the Node.js runtime version and supports handlers accordingly: + +- **Node.js 24+**: Only Promise-based handlers are supported (callbacks are deprecated by AWS Lambda) +- **Node.js 22 and lower**: Both callback-based and Promise-based handlers are supported for backward compatibility + +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)`. + +Example handlers: + +```js +// Callback-based handler (Node.js 22 and lower only) +exports.handler = function(event, context, callback) { + callback(null, 'ok'); +}; + +// Promise-based handler (all Node.js versions) +export const handler = async (event, context) => { + return 'ok'; +}; +``` + +**Note**: AWS Lambda has removed callback-based handlers in Node.js 24 runtime. It's required to migrate to Promise-based handlers when upgrading to Node.js 24+. + ## Usage Create a file to initialize the instrumentation, such as `lambda-wrapper.js`. diff --git a/packages/instrumentation-aws-lambda/src/instrumentation.ts b/packages/instrumentation-aws-lambda/src/instrumentation.ts index a865b8575d..75e3be494c 100644 --- a/packages/instrumentation-aws-lambda/src/instrumentation.ts +++ b/packages/instrumentation-aws-lambda/src/instrumentation.ts @@ -71,6 +71,26 @@ export const AWS_HANDLER_STREAMING_SYMBOL = Symbol.for( ); export const AWS_HANDLER_STREAMING_RESPONSE = 'response'; +/** + * Determines if callback-based handlers are supported based on the Node.js runtime version. + * Returns true if callbacks are supported (Node.js < 24). + * Returns false if AWS_EXECUTION_ENV is not set or doesn't match the expected format. + */ +function isSupportingCallbacks(): boolean { + const executionEnv = process.env.AWS_EXECUTION_ENV; + if (!executionEnv) { + return false; + } + + // AWS_EXECUTION_ENV format: AWS_Lambda_nodejs24.x, AWS_Lambda_nodejs22.x, etc. + const match = executionEnv.match(/AWS_Lambda_nodejs(\d+)\./); + if (match && match[1]) { + return parseInt(match[1], 10) < 24; + } + + return false; +} + export class AwsLambdaInstrumentation extends InstrumentationBase { private declare _traceForceFlusher?: () => Promise; private declare _metricForceFlusher?: () => Promise; @@ -272,47 +292,104 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { - // Lambda seems to pass a callback even if handler is of Promise form, so we wrap all the time before calling - // the handler and see if the result is a Promise or not. In such a case, the callback is usually ignored. If - // the handler happened to both call the callback and complete a returned Promise, whichever happens first will - // win and the latter will be ignored. - const wrappedCallback = plugin._wrapCallback(callback, span); - const maybePromise = safeExecuteInTheMiddle( - () => original.apply(this, [event, context, wrappedCallback]), - error => { - if (error != null) { - // Exception thrown synchronously before resolving callback / promise. - plugin._applyResponseHook(span, error); - plugin._endSpan(span, error, () => {}); - } + if (supportsCallbacks) { + // Node.js 22 and lower: Support callback-based handlers for backward compatibility + return function patchedHandlerWithCallback( + this: never, + // The event can be a user type, it truly is any. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + event: any, + context: Context, + callback?: Callback + ) { + _onRequest(); + + const parent = plugin._determineParent(event, context); + + const span = plugin._createSpanForRequest( + event, + context, + requestIsColdStart, + parent + ); + + plugin._applyRequestHook(span, event, context); + + return otelContext.with(trace.setSpan(parent, span), () => { + // Support both callback-based and Promise-based handlers for backward compatibility + if (callback) { + const wrappedCallback = plugin._wrapCallback(callback, span); + const maybePromise = safeExecuteInTheMiddle( + () => original.apply(this, [event, context, wrappedCallback]), + error => { + if (error != null) { + // Exception thrown synchronously before resolving callback / promise. + plugin._applyResponseHook(span, error); + plugin._endSpan(span, error, () => {}); + } + } + ) as Promise<{}> | undefined; + + return plugin._handlePromiseResult(span, maybePromise); + } else { + // Promise-based handler + const maybePromise = safeExecuteInTheMiddle( + () => (original as (event: any, context: Context) => Promise | any).apply(this, [event, context]), + error => { + if (error != null) { + // Exception thrown synchronously before resolving promise. + plugin._applyResponseHook(span, error); + plugin._endSpan(span, error, () => {}); + } + } + ) as Promise<{}> | undefined; + + return plugin._handlePromiseResult(span, maybePromise); } - ) as Promise<{}> | undefined; + }); + }; + } else { + // Node.js 24+: Only Promise-based handlers (callbacks deprecated) + return function patchedHandler( + this: never, + // The event can be a user type, it truly is any. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + event: any, + context: Context + ) { + _onRequest(); - return plugin._handlePromiseResult(span, maybePromise); - }); - }; + const parent = plugin._determineParent(event, context); + + const span = plugin._createSpanForRequest( + event, + context, + requestIsColdStart, + parent + ); + + plugin._applyRequestHook(span, event, context); + + return otelContext.with(trace.setSpan(parent, span), () => { + // Promise-based handler only (Node.js 24+) + const maybePromise = safeExecuteInTheMiddle( + () => (original as (event: any, context: Context) => Promise | any).apply(this, [event, context]), + error => { + if (error != null) { + // Exception thrown synchronously before resolving promise. + plugin._applyResponseHook(span, error); + plugin._endSpan(span, error, () => {}); + } + } + ) as Promise<{}> | undefined; + + return plugin._handlePromiseResult(span, maybePromise); + }); + }; + } } private _createSpanForRequest( @@ -443,11 +520,11 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { - diag.debug('executing original lookup callback function'); + plugin._diag.debug('executing original callback function'); return original.apply(this, [err, res]); }); }; @@ -482,14 +559,14 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { provider.forceFlush = forceFlush; initializeHandlerTracing('lambda-test/sync.handler', provider); - await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } - } - ); - }); + await lambdaRequire('lambda-test/sync').handler('arg', ctx); assert.strictEqual(forceFlushed, true); }); @@ -133,19 +121,7 @@ describe('force flush', () => { nodeTracerProvider.forceFlush = forceFlush; initializeHandlerTracing('lambda-test/sync.handler', provider); - await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } - } - ); - }); + await lambdaRequire('lambda-test/sync').handler('arg', ctx); assert.strictEqual(forceFlushed, true); }); @@ -165,24 +141,12 @@ describe('force flush', () => { provider.forceFlush = forceFlush; initializeHandlerMetrics('lambda-test/sync.handler', provider); - await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } - } - ); - }); + await lambdaRequire('lambda-test/sync').handler('arg', ctx); assert.strictEqual(forceFlushed, true); }); - it('should callback once after force flush providers', async () => { + it('should complete handler after force flush providers', async () => { const nodeTracerProvider = new NodeTracerProvider({ spanProcessors: [new BatchSpanProcessor(traceMemoryExporter)], }); @@ -216,24 +180,9 @@ describe('force flush', () => { instrumentation.setTracerProvider(tracerProvider); instrumentation.setMeterProvider(meterProvider); - let callbackCount = 0; - await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - callbackCount++; - if (err) { - reject(err); - } else { - resolve(res); - } - } - ); - }); + await lambdaRequire('lambda-test/sync').handler('arg', ctx); assert.strictEqual(tracerForceFlushed, true); assert.strictEqual(meterForceFlushed, true); - assert.strictEqual(callbackCount, 1); }); }); diff --git a/packages/instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts b/packages/instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts index 067a94e394..a9bf01e89c 100644 --- a/packages/instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts +++ b/packages/instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts @@ -242,19 +242,7 @@ describe('lambda handler', () => { it('should export a valid span', async () => { initializeHandler('lambda-test/sync.handler'); - const result = await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } - } - ); - }); + const result = await lambdaRequire('lambda-test/sync').handler('arg', ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -268,25 +256,9 @@ describe('lambda handler', () => { const handlerModule = lambdaRequire('lambda-test/sync'); - const result1 = await new Promise((resolve, reject) => { - handlerModule.handler('arg', ctx, (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }); + const result1 = await handlerModule.handler('arg', ctx); - const result2 = await new Promise((resolve, reject) => { - handlerModule.handler('arg', ctx, (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }); + const result2 = await handlerModule.handler('arg', ctx); const spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 2); @@ -308,19 +280,7 @@ describe('lambda handler', () => { initializeHandler('lambda-test/sync.handler'); - const result = await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } - } - ); - }); + const result = await lambdaRequire('lambda-test/sync').handler('arg', ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -335,19 +295,7 @@ describe('lambda handler', () => { lambdaStartTime: Date.now() - 2 * lambdaMaxInitInMilliseconds, }); - const result = await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } - } - ); - }); + const result = await lambdaRequire('lambda-test/sync').handler('arg', ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -362,11 +310,7 @@ describe('lambda handler', () => { let err: Error; try { - lambdaRequire('lambda-test/sync').error( - 'arg', - ctx, - (err: Error, res: any) => {} - ); + await lambdaRequire('lambda-test/sync').error('arg', ctx); } catch (e: any) { err = e; } @@ -378,24 +322,12 @@ describe('lambda handler', () => { assert.strictEqual(span.parentSpanContext?.spanId, undefined); }); - it('should record error in callback', async () => { + it('should record error in promise rejection', async () => { initializeHandler('lambda-test/sync.callbackerror'); let err: Error; try { - await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').callbackerror( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } - } - ); - }); + await lambdaRequire('lambda-test/sync').callbackerror('arg', ctx); } catch (e: any) { err = e; } @@ -412,11 +344,7 @@ describe('lambda handler', () => { let err: string; try { - lambdaRequire('lambda-test/sync').stringerror( - 'arg', - ctx, - (err: Error, res: any) => {} - ); + await lambdaRequire('lambda-test/sync').stringerror('arg', ctx); } catch (e: any) { err = e; } @@ -431,19 +359,7 @@ describe('lambda handler', () => { it('context should have parent trace', async () => { initializeHandler('lambda-test/sync.context'); - const result = await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').context( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } - } - ); - }); + const result = await lambdaRequire('lambda-test/sync').context('arg', ctx); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; assert.strictEqual(span.spanContext().traceId, result); @@ -452,8 +368,45 @@ describe('lambda handler', () => { it('context should have parent trace', async () => { initializeHandler('lambda-test/sync.context'); + const result = await lambdaRequire('lambda-test/sync').context('arg', ctx); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(span.spanContext().traceId, result); + }); + }); + + it('should record string error in promise rejection', async () => { + initializeHandler('lambda-test/sync.stringerror'); + + let err: string; + try { + await lambdaRequire('lambda-test/sync').stringerror('arg', ctx); + } catch (e: any) { + err = e; + } + assert.strictEqual(err!, 'handler error'); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assertSpanFailure(span); + assert.strictEqual(span.parentSpanContext?.spanId, undefined); + }); + + describe('callback-based handler (Node.js 22 and lower)', () => { + beforeEach(() => { + // Simulate Node.js 22 runtime for backward compatibility testing + process.env.AWS_EXECUTION_ENV = 'AWS_Lambda_nodejs22.x'; + }); + + afterEach(() => { + delete process.env.AWS_EXECUTION_ENV; + }); + + it('should export a valid span with callback handler', async () => { + initializeHandler('lambda-test/sync.callbackHandler'); + const result = await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').context( + lambdaRequire('lambda-test/sync').callbackHandler( 'arg', ctx, (err: Error, res: any) => { @@ -465,19 +418,48 @@ describe('lambda handler', () => { } ); }); + assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; - assert.strictEqual(span.spanContext().traceId, result); + assert.strictEqual(spans.length, 1); + assertSpanSuccess(span); + assert.strictEqual(span.parentSpanContext?.spanId, undefined); }); - }); - it('should record string error in callback', async () => { - initializeHandler('lambda-test/sync.callbackstringerror'); + it('should record error with callback handler', async () => { + initializeHandler('lambda-test/sync.callbackError'); - let err: string; - try { - await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').callbackstringerror( + let err: Error; + try { + await new Promise((resolve, reject) => { + lambdaRequire('lambda-test/sync').callbackError( + 'arg', + ctx, + (error: Error, _res: any) => { + if (error) { + reject(error); + } else { + resolve({}); + } + } + ); + }); + } catch (e: any) { + err = e; + } + assert.strictEqual(err!.message, 'handler error'); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assertSpanFailure(span); + assert.strictEqual(span.parentSpanContext?.spanId, undefined); + }); + + it('context should have parent trace with callback handler', async () => { + initializeHandler('lambda-test/sync.callbackContext'); + + const result = await new Promise((resolve, reject) => { + lambdaRequire('lambda-test/sync').callbackContext( 'arg', ctx, (err: Error, res: any) => { @@ -489,15 +471,10 @@ describe('lambda handler', () => { } ); }); - } catch (e: any) { - err = e; - } - assert.strictEqual(err!, 'handler error'); - const spans = memoryExporter.getFinishedSpans(); - const [span] = spans; - assert.strictEqual(spans.length, 1); - assertSpanFailure(span); - assert.strictEqual(span.parentSpanContext?.spanId, undefined); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(span.spanContext().traceId, result); + }); }); describe('with remote parent', () => { @@ -692,13 +669,7 @@ describe('lambda handler', () => { it('sync - success', async () => { initializeHandler('lambda-test/sync.handler', config); - const result = await new Promise((resolve, _reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (_err: Error, res: any) => resolve(res) - ); - }); + const result = await lambdaRequire('lambda-test/sync').handler('arg', ctx); const [span] = memoryExporter.getFinishedSpans(); assert.strictEqual(span.attributes[RES_ATTR], result); }); @@ -708,7 +679,7 @@ describe('lambda handler', () => { let err: Error; try { - lambdaRequire('lambda-test/sync').error('arg', ctx, () => {}); + await lambdaRequire('lambda-test/sync').error('arg', ctx); } catch (e: any) { err = e; } @@ -716,20 +687,15 @@ describe('lambda handler', () => { assert.strictEqual(span.attributes[ERR_ATTR], err!.message); }); - it('sync - error with callback', async () => { + it('sync - error with promise rejection', async () => { initializeHandler('lambda-test/sync.callbackerror', config); let error: Error; - await new Promise((resolve, _reject) => { - lambdaRequire('lambda-test/sync').callbackerror( - 'arg', - ctx, - (err: Error, _res: any) => { - error = err; - resolve({}); - } - ); - }); + try { + await lambdaRequire('lambda-test/sync').callbackerror('arg', ctx); + } catch (e: any) { + error = e; + } const [span] = memoryExporter.getFinishedSpans(); assert.strictEqual(span.attributes[ERR_ATTR], error!.message); }); @@ -789,13 +755,13 @@ describe('lambda handler', () => { }, }; - await lambdaRequire('lambda-test/sync').handler(event, ctx, () => {}); + await lambdaRequire('lambda-test/sync').handler(event, ctx); const [span] = memoryExporter.getFinishedSpans(); assert.ok( span.attributes[ATTR_URL_FULL] === 'http://www.example.com:1234/lambda/test/path?key=value&key2=value2' || - span.attributes[ATTR_URL_FULL] === - 'http://www.example.com:1234/lambda/test/path?key2=value2&key=value' + span.attributes[ATTR_URL_FULL] === + 'http://www.example.com:1234/lambda/test/path?key2=value2&key=value' ); }); it('pulls url from api gateway http events', async () => { @@ -812,7 +778,7 @@ describe('lambda handler', () => { }, }; - await lambdaRequire('lambda-test/sync').handler(event, ctx, () => {}); + await lambdaRequire('lambda-test/sync').handler(event, ctx); const [span] = memoryExporter.getFinishedSpans(); assert.strictEqual( span.attributes[ATTR_URL_FULL], diff --git a/packages/instrumentation-aws-lambda/test/lambda-test/sync.js b/packages/instrumentation-aws-lambda/test/lambda-test/sync.js index 521c5f6ea4..58aaf9bcda 100644 --- a/packages/instrumentation-aws-lambda/test/lambda-test/sync.js +++ b/packages/instrumentation-aws-lambda/test/lambda-test/sync.js @@ -15,26 +15,36 @@ */ const api = require('@opentelemetry/api'); -exports.handler = function (event, context, callback) { - callback(null, 'ok'); +// Promise-based handlers (all Node.js versions) +exports.handler = async function (event, context) { + return 'ok'; }; -exports.error = function (event, context, callback) { +exports.error = async function (event, context) { throw new Error('handler error'); }; -exports.callbackerror = function (event, context, callback) { - callback(new Error('handler error')); +exports.callbackerror = async function (event, context) { + throw new Error('handler error'); }; -exports.stringerror = function (event, context, callback) { +exports.stringerror = async function (event, context) { throw 'handler error'; }; -exports.callbackstringerror = function (event, context, callback) { - callback('handler error'); +exports.context = async function (event, context) { + return api.trace.getSpan(api.context.active()).spanContext().traceId; +}; + +// Callback-based handlers (Node.js 22 and lower only - for backward compatibility testing) +exports.callbackHandler = function (event, context, callback) { + callback(null, 'ok'); +}; + +exports.callbackError = function (event, context, callback) { + callback(new Error('handler error')); }; -exports.context = function (event, context, callback) { +exports.callbackContext = function (event, context, callback) { callback(null, api.trace.getSpan(api.context.active()).spanContext().traceId); };