From 8e9513b8964b1be7f08db3eb899920b0787ae64b Mon Sep 17 00:00:00 2001 From: Raphael Manke Date: Wed, 26 Nov 2025 19:18:29 +0100 Subject: [PATCH 1/5] feat(instrumentation-aws-lambda): remove callback-based handler support BREAKING CHANGE: 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. Changes: - Removed Callback import and _wrapCallback method from instrumentation - Updated patchedHandler to only accept (event, context) parameters - Converted all test handlers from callback-based to Promise-based - Updated all tests to use Promise-based handlers directly - Added migration guide in README.md - Documented breaking change in CHANGELOG.md All 50 tests passing. --- .../instrumentation-aws-lambda/CHANGELOG.md | 6 + packages/instrumentation-aws-lambda/README.md | 19 ++ .../src/instrumentation.ts | 32 +--- .../lambda-handler.force-flush.test.ts | 61 +------ .../test/integrations/lambda-handler.test.ts | 169 +++--------------- .../test/lambda-test/sync.js | 20 +-- 6 files changed, 72 insertions(+), 235 deletions(-) diff --git a/packages/instrumentation-aws-lambda/CHANGELOG.md b/packages/instrumentation-aws-lambda/CHANGELOG.md index 14937a6336..34dab7c2cb 100644 --- a/packages/instrumentation-aws-lambda/CHANGELOG.md +++ b/packages/instrumentation-aws-lambda/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog +## [Unreleased] + +### Breaking Changes + +* **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. + ## [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..4fb421ba11 100644 --- a/packages/instrumentation-aws-lambda/README.md +++ b/packages/instrumentation-aws-lambda/README.md @@ -23,6 +23,25 @@ npm install --save @opentelemetry/instrumentation-aws-lambda - This package will instrument the lambda execution regardless of versions. +## Important Notes + +### Callback-Based Handlers Deprecated + +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. + +Example migration: +```js +// ❌ Deprecated callback-based handler +exports.handler = function(event, context, callback) { + callback(null, 'ok'); +}; + +// ✅ Promise-based handler +exports.handler = async function(event, context) { + return 'ok'; +}; +``` + ## 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..f680ae33e6 100644 --- a/packages/instrumentation-aws-lambda/src/instrumentation.ts +++ b/packages/instrumentation-aws-lambda/src/instrumentation.ts @@ -45,7 +45,6 @@ import { ATTR_FAAS_EXECUTION, ATTR_FAAS_ID } from './semconv-obsolete'; import { APIGatewayProxyEventHeaders, - Callback, Context, Handler, StreamifyHandler, @@ -277,8 +276,7 @@ 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); + // Type assertion: Handler type from aws-lambda still includes callback parameter, + // but we only support Promise-based handlers now (Node.js 24+) const maybePromise = safeExecuteInTheMiddle( - () => original.apply(this, [event, context, wrappedCallback]), + () => (original as (event: any, context: Context) => Promise | any).apply(this, [event, context]), error => { if (error != null) { - // Exception thrown synchronously before resolving callback / promise. + // Exception thrown synchronously before resolving promise. plugin._applyResponseHook(span, error); plugin._endSpan(span, error, () => {}); } @@ -440,19 +435,6 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { - diag.debug('executing original lookup callback function'); - return original.apply(this, [err, res]); - }); - }; - } - private _endSpan( span: Span, err: string | Error | null | undefined, @@ -482,14 +464,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..46ddf5362f 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,43 +368,19 @@ 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); }); }); - it('should record string error in callback', async () => { + it('should record string error in promise rejection', async () => { initializeHandler('lambda-test/sync.callbackstringerror'); let err: string; try { - await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').callbackstringerror( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } - } - ); - }); + await lambdaRequire('lambda-test/sync').callbackstringerror('arg', ctx); } catch (e: any) { err = e; } @@ -692,13 +584,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 +594,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 +602,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 +670,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 +693,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..51e71a75f3 100644 --- a/packages/instrumentation-aws-lambda/test/lambda-test/sync.js +++ b/packages/instrumentation-aws-lambda/test/lambda-test/sync.js @@ -15,26 +15,26 @@ */ const api = require('@opentelemetry/api'); -exports.handler = function (event, context, callback) { - callback(null, 'ok'); +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.callbackstringerror = async function (event, context) { + throw 'handler error'; }; -exports.context = function (event, context, callback) { - callback(null, api.trace.getSpan(api.context.active()).spanContext().traceId); +exports.context = async function (event, context) { + return api.trace.getSpan(api.context.active()).spanContext().traceId; }; From 3f03df1a783111b64e45609fd2fe3a0744d27390 Mon Sep 17 00:00:00 2001 From: Raphael Manke Date: Wed, 26 Nov 2025 20:27:25 +0100 Subject: [PATCH 2/5] 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. --- .../instrumentation-aws-lambda/CHANGELOG.md | 7 +- packages/instrumentation-aws-lambda/README.md | 17 +- .../instrumentation-aws-lambda/package.json | 2 +- .../src/instrumentation.ts | 159 ++++++++++++++---- .../test/integrations/lambda-handler.test.ts | 85 ++++++++++ .../test/lambda-test/sync.js | 14 ++ 6 files changed, 244 insertions(+), 40 deletions(-) diff --git a/packages/instrumentation-aws-lambda/CHANGELOG.md b/packages/instrumentation-aws-lambda/CHANGELOG.md index 34dab7c2cb..893375d061 100644 --- a/packages/instrumentation-aws-lambda/CHANGELOG.md +++ b/packages/instrumentation-aws-lambda/CHANGELOG.md @@ -3,9 +3,12 @@ ## [Unreleased] -### Breaking Changes +### Features -* **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. +* **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 deprecation 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 4fb421ba11..656f6f5a77 100644 --- a/packages/instrumentation-aws-lambda/README.md +++ b/packages/instrumentation-aws-lambda/README.md @@ -25,23 +25,30 @@ npm install --save @opentelemetry/instrumentation-aws-lambda ## Important Notes -### Callback-Based Handlers Deprecated +### Handler Types Supported -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. +This instrumentation automatically detects the Node.js runtime version and supports handlers accordingly: -Example migration: +- **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 -// ❌ Deprecated callback-based handler +// Callback-based handler (Node.js 22 and lower only) exports.handler = function(event, context, callback) { callback(null, 'ok'); }; -// ✅ Promise-based handler +// Promise-based handler (all Node.js versions) exports.handler = async function(event, context) { return 'ok'; }; ``` +**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+. + ## Usage Create a file to initialize the instrumentation, such as `lambda-wrapper.js`. diff --git a/packages/instrumentation-aws-lambda/package.json b/packages/instrumentation-aws-lambda/package.json index b429ce8a3a..918043ae7c 100644 --- a/packages/instrumentation-aws-lambda/package.json +++ b/packages/instrumentation-aws-lambda/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation-aws-lambda", - "version": "0.60.1", + "version": "0.61.0", "description": "OpenTelemetry instrumentation for AWS Lambda function invocations", "main": "build/src/index.js", "types": "build/src/index.d.ts", diff --git a/packages/instrumentation-aws-lambda/src/instrumentation.ts b/packages/instrumentation-aws-lambda/src/instrumentation.ts index f680ae33e6..5b38bbb258 100644 --- a/packages/instrumentation-aws-lambda/src/instrumentation.ts +++ b/packages/instrumentation-aws-lambda/src/instrumentation.ts @@ -45,6 +45,7 @@ import { ATTR_FAAS_EXECUTION, ATTR_FAAS_ID } from './semconv-obsolete'; import { APIGatewayProxyEventHeaders, + Callback, Context, Handler, StreamifyHandler, @@ -70,6 +71,25 @@ export const AWS_HANDLER_STREAMING_SYMBOL = Symbol.for( ); export const AWS_HANDLER_STREAMING_RESPONSE = 'response'; +/** + * Detects the Node.js runtime version from AWS_EXECUTION_ENV environment variable. + * Returns the major version number (e.g., 24, 22, 20) or null if not detected. + */ +function getNodeRuntimeVersion(): number | null { + const executionEnv = process.env.AWS_EXECUTION_ENV; + if (!executionEnv) { + return null; + } + + // 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); + } + + return null; +} + export class AwsLambdaInstrumentation extends InstrumentationBase { private declare _traceForceFlusher?: () => Promise; private declare _metricForceFlusher?: () => Promise; @@ -271,43 +291,105 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { - // Type assertion: Handler type from aws-lambda still includes callback parameter, - // but we only support Promise-based handlers now (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, () => {}); - } + 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( @@ -435,6 +517,19 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { + diag.debug('AWS Lambda instrumentation: Executing original callback function'); + return original.apply(this, [err, res]); + }); + }; + } + private _endSpan( span: Span, err: string | Error | null | undefined, 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 46ddf5362f..3f8fd698da 100644 --- a/packages/instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts +++ b/packages/instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts @@ -392,6 +392,91 @@ describe('lambda handler', () => { 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').callbackHandler( + 'arg', + ctx, + (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); + } + } + ); + }); + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assertSpanSuccess(span); + assert.strictEqual(span.parentSpanContext?.spanId, undefined); + }); + + it('should record error with callback handler', async () => { + initializeHandler('lambda-test/sync.callbackError'); + + 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) => { + if (err) { + reject(err); + } else { + resolve(res); + } + } + ); + }); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(span.spanContext().traceId, result); + }); + }); + describe('with remote parent', () => { beforeEach(() => { propagation.disable(); diff --git a/packages/instrumentation-aws-lambda/test/lambda-test/sync.js b/packages/instrumentation-aws-lambda/test/lambda-test/sync.js index 51e71a75f3..dec2477cf5 100644 --- a/packages/instrumentation-aws-lambda/test/lambda-test/sync.js +++ b/packages/instrumentation-aws-lambda/test/lambda-test/sync.js @@ -15,6 +15,7 @@ */ const api = require('@opentelemetry/api'); +// Promise-based handlers (all Node.js versions) exports.handler = async function (event, context) { return 'ok'; }; @@ -38,3 +39,16 @@ exports.callbackstringerror = async function (event, context) { 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.callbackContext = function (event, context, callback) { + callback(null, api.trace.getSpan(api.context.active()).spanContext().traceId); +}; From ae73486cfcd3713fe71a405225940f2ac62c8d4f Mon Sep 17 00:00:00 2001 From: Raphael Manke Date: Wed, 26 Nov 2025 21:23:20 +0100 Subject: [PATCH 3/5] refactor: simplify runtime detection and update README example - Refactor getNodeRuntimeVersion() to isSupportingCallbacks() returning boolean - Default to false (no callbacks) when AWS_EXECUTION_ENV is not set - Update README example to use ESM syntax for Promise-based handler --- packages/instrumentation-aws-lambda/README.md | 2 +- .../src/instrumentation.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/instrumentation-aws-lambda/README.md b/packages/instrumentation-aws-lambda/README.md index 656f6f5a77..edd1c60c01 100644 --- a/packages/instrumentation-aws-lambda/README.md +++ b/packages/instrumentation-aws-lambda/README.md @@ -42,7 +42,7 @@ exports.handler = function(event, context, callback) { }; // Promise-based handler (all Node.js versions) -exports.handler = async function(event, context) { +export const handler = async (event, context) => { return 'ok'; }; ``` diff --git a/packages/instrumentation-aws-lambda/src/instrumentation.ts b/packages/instrumentation-aws-lambda/src/instrumentation.ts index 5b38bbb258..b496b36431 100644 --- a/packages/instrumentation-aws-lambda/src/instrumentation.ts +++ b/packages/instrumentation-aws-lambda/src/instrumentation.ts @@ -72,22 +72,23 @@ export const AWS_HANDLER_STREAMING_SYMBOL = Symbol.for( export const AWS_HANDLER_STREAMING_RESPONSE = 'response'; /** - * Detects the Node.js runtime version from AWS_EXECUTION_ENV environment variable. - * Returns the major version number (e.g., 24, 22, 20) or null if not detected. + * 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 getNodeRuntimeVersion(): number | null { +function isSupportingCallbacks(): boolean { const executionEnv = process.env.AWS_EXECUTION_ENV; if (!executionEnv) { - return null; + 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); + return parseInt(match[1], 10) < 24; } - return null; + return false; } export class AwsLambdaInstrumentation extends InstrumentationBase { @@ -291,9 +292,8 @@ export class AwsLambdaInstrumentation extends InstrumentationBase Date: Wed, 26 Nov 2025 21:47:31 +0100 Subject: [PATCH 4/5] test: remove duplicate test handlers and invalid callback pattern - Remove duplicate callbackstringerror handler (same as stringerror) - Remove callbackStringError test (strings are not valid for Lambda callbacks) - Update test to use stringerror instead of callbackstringerror --- packages/instrumentation-aws-lambda/CHANGELOG.md | 2 +- packages/instrumentation-aws-lambda/README.md | 2 +- packages/instrumentation-aws-lambda/package.json | 2 +- packages/instrumentation-aws-lambda/src/instrumentation.ts | 4 ++-- .../test/integrations/lambda-handler.test.ts | 4 ++-- packages/instrumentation-aws-lambda/test/lambda-test/sync.js | 4 ---- 6 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/instrumentation-aws-lambda/CHANGELOG.md b/packages/instrumentation-aws-lambda/CHANGELOG.md index 893375d061..71c0b52306 100644 --- a/packages/instrumentation-aws-lambda/CHANGELOG.md +++ b/packages/instrumentation-aws-lambda/CHANGELOG.md @@ -8,7 +8,7 @@ * **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 deprecation of callbacks in Node.js 24+. + 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 edd1c60c01..6e74ab803c 100644 --- a/packages/instrumentation-aws-lambda/README.md +++ b/packages/instrumentation-aws-lambda/README.md @@ -47,7 +47,7 @@ export const handler = async (event, context) => { }; ``` -**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+. +**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 diff --git a/packages/instrumentation-aws-lambda/package.json b/packages/instrumentation-aws-lambda/package.json index 918043ae7c..b429ce8a3a 100644 --- a/packages/instrumentation-aws-lambda/package.json +++ b/packages/instrumentation-aws-lambda/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation-aws-lambda", - "version": "0.61.0", + "version": "0.60.1", "description": "OpenTelemetry instrumentation for AWS Lambda function invocations", "main": "build/src/index.js", "types": "build/src/index.d.ts", diff --git a/packages/instrumentation-aws-lambda/src/instrumentation.ts b/packages/instrumentation-aws-lambda/src/instrumentation.ts index b496b36431..75e3be494c 100644 --- a/packages/instrumentation-aws-lambda/src/instrumentation.ts +++ b/packages/instrumentation-aws-lambda/src/instrumentation.ts @@ -520,11 +520,11 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { - diag.debug('AWS Lambda instrumentation: Executing original callback function'); + plugin._diag.debug('executing original callback function'); return original.apply(this, [err, res]); }); }; 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 3f8fd698da..a9bf01e89c 100644 --- a/packages/instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts +++ b/packages/instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts @@ -376,11 +376,11 @@ describe('lambda handler', () => { }); it('should record string error in promise rejection', async () => { - initializeHandler('lambda-test/sync.callbackstringerror'); + initializeHandler('lambda-test/sync.stringerror'); let err: string; try { - await lambdaRequire('lambda-test/sync').callbackstringerror('arg', ctx); + await lambdaRequire('lambda-test/sync').stringerror('arg', ctx); } catch (e: any) { err = e; } diff --git a/packages/instrumentation-aws-lambda/test/lambda-test/sync.js b/packages/instrumentation-aws-lambda/test/lambda-test/sync.js index dec2477cf5..58aaf9bcda 100644 --- a/packages/instrumentation-aws-lambda/test/lambda-test/sync.js +++ b/packages/instrumentation-aws-lambda/test/lambda-test/sync.js @@ -32,10 +32,6 @@ exports.stringerror = async function (event, context) { throw 'handler error'; }; -exports.callbackstringerror = async function (event, context) { - throw 'handler error'; -}; - exports.context = async function (event, context) { return api.trace.getSpan(api.context.active()).spanContext().traceId; }; From e3a03564d346ac42d097eeabc91bc443d2edb2cd Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Wed, 26 Nov 2025 13:45:14 -0800 Subject: [PATCH 5/5] fix lint:markdown --- packages/instrumentation-aws-lambda/CHANGELOG.md | 4 ++-- packages/instrumentation-aws-lambda/README.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/instrumentation-aws-lambda/CHANGELOG.md b/packages/instrumentation-aws-lambda/CHANGELOG.md index 71c0b52306..0b52873cc7 100644 --- a/packages/instrumentation-aws-lambda/CHANGELOG.md +++ b/packages/instrumentation-aws-lambda/CHANGELOG.md @@ -6,8 +6,8 @@ ### 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 + * **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 6e74ab803c..f8c1ceeaa7 100644 --- a/packages/instrumentation-aws-lambda/README.md +++ b/packages/instrumentation-aws-lambda/README.md @@ -35,6 +35,7 @@ This instrumentation automatically detects the Node.js runtime version and suppo 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) {