Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/instrumentation-aws-lambda/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
<!-- markdownlint-disable MD007 MD034 -->
# 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 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)


Expand Down
26 changes: 26 additions & 0 deletions packages/instrumentation-aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,32 @@ 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 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`.
Expand Down
2 changes: 1 addition & 1 deletion packages/instrumentation-aws-lambda/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
161 changes: 119 additions & 42 deletions packages/instrumentation-aws-lambda/src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AwsLambdaInstrumentationConfig> {
private declare _traceForceFlusher?: () => Promise<void>;
private declare _metricForceFlusher?: () => Promise<void>;
Expand Down Expand Up @@ -272,47 +292,104 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
};
}

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,
callback: Callback
) {
_onRequest();

const parent = plugin._determineParent(event, context);

const span = plugin._createSpanForRequest(
event,
context,
requestIsColdStart,
parent
);
// Determine whether to support callbacks based on runtime version
const supportsCallbacks = isSupportingCallbacks();

plugin._applyRequestHook(span, event, context);

return otelContext.with(trace.setSpan(parent, span), () => {
// 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> | 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> | 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(
Expand Down Expand Up @@ -443,11 +520,11 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
private _wrapCallback(original: Callback, span: Span): Callback {
const plugin = this;
return function wrappedCallback(this: never, err, res) {
diag.debug('executing wrapped lookup callback function');
diag.debug('AWS Lambda instrumentation: Executing wrapped callback function');
plugin._applyResponseHook(span, err, res);

plugin._endSpan(span, err, () => {
diag.debug('executing original lookup callback function');
diag.debug('AWS Lambda instrumentation: Executing original callback function');
return original.apply(this, [err, res]);
});
};
Expand Down Expand Up @@ -482,14 +559,14 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
flushers.push(this._traceForceFlusher());
} else {
diag.debug(
'Spans may not be exported for the lambda function because we are not force flushing before callback.'
'Spans may not be exported for the lambda function because we are not force flushing before handler completion.'
);
}
if (this._metricForceFlusher) {
flushers.push(this._metricForceFlusher());
} else {
diag.debug(
'Metrics may not be exported for the lambda function because we are not force flushing before callback.'
'Metrics may not be exported for the lambda function because we are not force flushing before handler completion.'
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,19 +100,7 @@ describe('force flush', () => {
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);
});
Expand All @@ -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);
});
Expand All @@ -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)],
});
Expand Down Expand Up @@ -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);
});
});
Loading
Loading