Skip to content

AWS Lambda: trace Node.js and Python functions

Shield360 traces each Lambda invocation as one span and flushes everything the invocation recorded before it returns. Telemetry reaches Shield360 even though Lambda freezes the function as soon as the handler returns.

You can add it in two ways:

  • The Shield360 layer needs no code change. Add the layer and two environment variables, and Shield360 wraps your handler.
  • The handler wrapper is one line in your code: wrapHandler in Node.js, @wrap_handler in Python.

Both record the same span and flush in the same way.

AttributeValue
faas.triggerhttp (API Gateway, function URLs, ALB), pubsub (SQS, SNS, Kinesis, MSK), datasource (S3, DynamoDB), timer (EventBridge schedules), or other
faas.coldstarttrue for the first invocation in a new execution environment. Provisioned concurrency counts as warm.
faas.invocation_idThe request ID
faas.name, faas.version, faas.max_memoryThe function’s name, version and memory limit in bytes
cloud.provider, cloud.platform, cloud.region, cloud.account.idaws, aws_lambda, the region and the account ID
cloud.resource_id, aws.lambda.invoked_arnThe function’s ARN, and the ARN it was called through (with an alias or version)

HTTP invocations are named by route (POST /orders) and also record http.request.method, url.path, http.route and http.response.status_code. A 5xx response or an exception marks the span as an error. When the caller sends a W3C traceparent header, the invocation joins the caller’s trace.

Everything the handler does appears inside that span: LLM calls, outgoing HTTP, AWS SDK calls and database queries.

Set these environment variables on the function:

Terminal window
SHIELD360_URL=https://app.shield360.ai
SHIELD360_API_KEY=<your API key> # Settings → Developer → API keys
OTEL_SERVICE_NAME=checkout # optional: defaults to the function name

You can use an OTLP collector of your own instead, with OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS. The other SDK settings work too.

Add the layer

Add the Shield360 layer that matches your runtime: shield360-nodejs for every Node.js runtime, or shield360-python<version>-<arch> for Python (for example shield360-python312-arm64). Ask Shield360 support for the layer ARN in your region, or host the layer in your own account.

Turn on the wrapper

Set one more environment variable:

Terminal window
AWS_LAMBDA_EXEC_WRAPPER=/opt/shield360-wrapper

Leave the function’s Handler setting as it is. The wrapper saves it in SHIELD360_LAMBDA_HANDLER and points Lambda at Shield360’s handler. Shield360’s handler starts Shield360, loads your handler, and wraps it.

Invoke the function

The first invocation appears in Shield360 under the service name, marked as a cold start.

Shield360 starts before your function’s code is loaded, so the provider SDKs your code imports (OpenAI, Anthropic, Bedrock and the others) are instrumented.

If you can’t use an exec wrapper, set the handler yourself:

RuntimeHandlerSHIELD360_LAMBDA_HANDLER
Node.js/opt/nodejs/node_modules/shield360/dist/lambda/handler.handlerYour handler, for example index.handler
Pythonshield360.aws_lambda.handlerYour handler, for example app.handler
Resources:
Checkout:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs22.x
Handler: index.handler
Layers:
- !Ref Shield360LayerArn
Environment:
Variables:
AWS_LAMBDA_EXEC_WRAPPER: /opt/shield360-wrapper
SHIELD360_URL: https://app.shield360.ai
SHIELD360_API_KEY: '{{resolve:secretsmanager:shield360:SecretString:apiKey}}'

Install the SDK with the function’s code, then wrap the handler.

Terminal window
npm install shield360
import { wrapHandler } from 'shield360/lambda';
export const handler = wrapHandler(async (event, context) => {
return { statusCode: 200, body: 'ok' };
});

Async and callback-style handlers both work. The wrapped handler is always async.

If Shield360 hasn’t started yet, wrapHandler starts it from the environment. Modules imported before that point aren’t instrumented, so also set NODE_OPTIONS=--require shield360/register on the function. Shield360 then starts before your code loads. If your code calls Shield360.init() itself, pass { autoInit: false }.

If you bundle the function with esbuild or webpack, keep shield360 and your LLM client (openai, @anthropic-ai/sdk) out of the bundle so their instrumentation can attach.

After each invocation, Shield360 flushes traces, metrics and logs before the handler returns. The flush waits at most SHIELD360_LAMBDA_FLUSH_TIMEOUT_MS milliseconds (2000 by default). It never runs past the invocation’s own deadline, so a collector that can’t be reached can’t make the function time out. Spans that don’t fit in that window stay queued and go out with a later invocation’s flush. If Lambda shuts the execution environment down first, they are lost.

The flush adds latency to each invocation: usually a few milliseconds when the collector is in the same region, and up to the timeout when it doesn’t answer.

VariableDefaultMeaning
SHIELD360_LAMBDA_FLUSH_TIMEOUT_MS2000The longest the flush waits after each invocation
SHIELD360_LAMBDA_HANDLERset by the wrapperYour own handler, for the layer’s handler to load
SHIELD360_LAMBDA_EAGERtruefalse loads your handler on the first invocation instead of during the init phase

Shield360 publishes the layers; ask support@shield360.ai for the ARN in your region. To keep a copy in your own AWS account (for example for a region without a published layer, or to pin a version), ask support for the layer zip, then publish it:

Terminal window
aws lambda publish-layer-version --layer-name shield360-nodejs \
--zip-file fileb://shield360-nodejs.zip \
--compatible-runtimes nodejs20.x nodejs22.x nodejs24.x \
--compatible-architectures x86_64 arm64

The Python layers leave out boto3 and botocore, because every Lambda Python runtime already includes them.

  • Node.js and Python only. Lambda wrappers for .NET and Go aren’t available yet. In those runtimes, call the SDK’s init and flush its providers at the end of the handler.
  • Lambda response streaming (awslambda.streamifyResponse) is not wrapped. The span would end before the stream does.
  • Lambda extensions aren’t used. The flush runs inside the invocation, which keeps the layer small, but adds the flush time to each invocation’s duration.