Instrument AWS Lambda with OpenTelemetry
This guide walks you through sending traces, metrics, and logs from your AWS Lambda functions to Observe using the OpenTelemetry Lambda layers.
Overview
The OpenTelemetry Lambda instrumentation consists of the following layers:
| Layer | Purpose | ARN pattern |
|---|---|---|
| Collector | Runs the OTel Collector as a Lambda extension; receives telemetry and exports to Observe. | arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-collector-<arch>-0_22_0:1 |
| Instrumentation | Auto-instruments your function code. | Language-specific. |
Both layers are required. The instrumentation layer generates telemetry and the collector layer exports it to Observe.
Instructions
Instrumenting AWS Lambda with OpenTelemetry requires the following tasks:
- Step 1. Add the collector layer
- Step 2. Configure the collector to export to Observe
- Step 3. Add the instrumentation layer and configure your function
- Step 4. Verify in Observe
Step 1. Add the collector layer
The collector layer runs as a Lambda extension alongside your function. It receives OTLP data from the instrumentation layer over localhost and exports it to Observe.
Latest collector layer ARN:
arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-collector-<arch>-0_22_0:1
Replace the following placeholders in the ARN:
- Replace
<region>with your Lambda’s region, such as us-west-2 - Replace
<account-id>with your AWS account ID - Replace
<arch>with either arm64 or amd64 to match your function’s architecture
You can add the collector layer using the AWS CLI, CloudFormation/SAM, or Terraform.
Add the collector layer using the AWS CLI:
aws lambda update-function-configuration \
--function-name my-function \
--layers "arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-collector-<arch>-
0_22_0:1"
Add the collector layer using CloudFormation/SAM:
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
Layers:
arm64-0_22_0:1"
- !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:opentelemetry-collector-${arch}-0_22_0:1"
Add the collector layer using Terraform:
resource "aws_lambda_function" "my_function" {
# ...
layers = [
"arn:aws:lambda:${var.region}:${data.aws_caller_identity.current.account_id}:layer:opentelemetry-collector-${var.arch}-0_22_0:1"
]
}
Step 2. Configure the collector to export to Observe
Create the following collector.yaml file in the root of your deployment package:
receivers:
otlp:
protocols:
grpc:
endpoint: "localhost:4317"
http:
endpoint: "localhost:4318"
exporters:
otlphttp:
endpoint: "https://${OBSERVE_CUSTOMER_ID}.collect.observeinc.com/v2/otel"
headers:
Authorization: "Bearer ${OBSERVE_TOKEN}"
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlphttp]
metrics:
receivers: [otlp]
exporters: [otlphttp]
logs:
receivers: [otlp]
exporters: [otlphttp]Then set the following environment variables on your Lambda:
| Environment variable | value |
|---|---|
| OPENTELEMETRY_COLLECTOR_CONFIG_URI | /var/task/collector.yaml |
| OBSERVE_CUSTOMER_ID | Your Observe customer ID, such as 123456789012. |
| OBSERVE_TOKEN | Your Observe Datastream token |
NoteYou can also load the config from S3 using
s3://<bucket>.s3. <region>.amazonaws.com/collector.yaml. Make sure the Lambda execution role has s3:GetObject permission on the bucket.
Step 3. Add the instrumentation layer and configure your function
Choose the environment that matches your Lambda runtime. Each example shows the complete AWS CLI command that attaches both layers (collector + instrumentation) and sets all required environment variables in a single operation.
Python
Supported runtimes: python3.9, python3.10, python3.11, python3.12, python3.13
Complete setup command:
aws lambda update-function-configuration \
--function-name <your-function-name> \
--layers \
"arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-collector-<arch>-0_22_0:1" \
"arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-python-0_20_0:1" \
--environment "Variables={ \
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-handler, \
OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/collector.yaml, \
OTEL_SERVICE_NAME=<your-function-name>, \
OBSERVE_CUSTOMER_ID=<your-observe-customer-id>, \
OBSERVE_TOKEN=<your-datastream-token> \
}"
What this does:
- Attaches the OTel Collector layer (runs as a sidecar extension, exports to Observe)
- Attaches the OTel Python instrumentation layer (wraps your handler to generate traces/metrics)
- Sets AWS_LAMBDA_EXEC_WRAPPER — tells Lambda to run the OTel wrapper script before your handler
- Sets OPENTELEMETRY_COLLECTOR_CONFIG_URI — tells the collector where to find its config
- Sets OTEL_SERVICE_NAME — identifies this function in Observe’s tracing UI
- Sets OBSERVE_CUSTOMER_ID and OBSERVE_TOKEN — credentials used by collector.yaml
Your function code requires no changes:
import json
import boto3
def handler(event, context):
client = boto3.client("dynamodb")
response = client.scan(TableName="my-table")
return {
"statusCode": 200,
"body": json.dumps({"count": response["Count"]})
}
The layer automatically instruments boto3, requests, urllib3, httpx, psycopg2, pymysql, and other common libraries. Each AWS SDK call, HTTP request, and database query becomes a span.
Node.js
Supported runtimes: nodejs18.x, nodejs20.x, nodejs22.x
Complete setup command:
aws lambda update-function-configuration \
--function-name <your-function-name> \
--layers \
"arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-collector-<arch>-0_22_0:1" \
"arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-nodejs-0_22_0:1" \
--environment "Variables={ \
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-handler, \
OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/collector.yaml, \
OTEL_SERVICE_NAME=<your-function-name>, \
OBSERVE_CUSTOMER_ID=<your-observe-customer-id>, \
OBSERVE_TOKEN=<your-datastream-token> \
}"
Your function code requires no changes:
const { DynamoDBClient, ScanCommand } = require("@aws-sdk/client-dynamodb");
const client = new DynamoDBClient({});
exports.handler = async (event) => {
const result = await client.send(new ScanCommand({ TableName: "my-table" }));
return {
statusCode: 200,
body: JSON.stringify({ count: result.Count }),
};
};
The layer automatically instruments @aws-sdk, http/https, express, mysql2, pg, ioredis, mongodb, and more.
Java
Supported runtimes: java11, java17, java21
NoteCold start warning: The Java auto-instrumentation agent adds 5-15 seconds to cold starts. For production, use provisioned concurrency or limit instrumentation scope (shown below).
Complete setup command:
aws lambda update-function-configuration \
--function-name <your-function-name> \
--layers \
"arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-collector-<arch>-0_22_0:1" \
"arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-javaagent-0_20_0:1" \
--environment "Variables={ \
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-handler, \
OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/collector.yaml, \
OTEL_SERVICE_NAME=<your-function-name>, \
OBSERVE_CUSTOMER_ID=<your-observe-customer-id>, \
OBSERVE_TOKEN=<your-datastream-token>, \
OTEL_INSTRUMENTATION_COMMON_DEFAULT_ENABLED=false, \
OTEL_INSTRUMENTATION_AWS_LAMBDA_ENABLED=true, \
OTEL_INSTRUMENTATION_AWS_SDK_ENABLED=true \
}"
The last three OTEL_INSTRUMENTATION_* variables limit auto-instrumentation to only Lambda and AWS SDK, thus significantly reducing cold start overhead. Remove them to instrument all libraries, but at the cost of slower cold starts.
Your function code requires no changes:
package com.example;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
public class MyHandler implements RequestHandler<Object, String> {
private final DynamoDbClient ddb = DynamoDbClient.create();
@Override
public String handleRequest(Object event, Context context) {
var result = ddb.scan(ScanRequest.builder().tableName("my-table").build());
return String.format("{\"count\": %d}", result.count());
}
}
An an alternative, you can use the lighter-weight wrapper layer (opentelemetry-javawrapper-0_20_0) if you want manual instrumentation with lower overhead:
arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-javawrapper-0_20_0:1
Ruby
Supported runtimes: ruby3.3, ruby3.4
Complete setup command:
aws lambda update-function-configuration \
--function-name <your-function-name> \
--layers \
"arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-collector-<arch>-0_22_0:1" \
"arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-ruby-0_14_0:1" \
--environment "Variables={ \
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-handler, \
OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/collector.yaml, \
OTEL_SERVICE_NAME=<your-function-name>, \
OBSERVE_CUSTOMER_ID=<your-observe-customer-id>, \
OBSERVE_TOKEN=<your-datastream-token> \
}"
Your function code requires no changes:
require 'aws-sdk-dynamodb'
def handler(event:, context:)
client = Aws::DynamoDB::Client.new
result = client.scan(table_name: 'my-table')
{ statusCode: 200, body: { count: result.count }.to_json }
end
The layer automatically instruments aws-sdk, net/http, faraday, rack, and other common gems.
Step 4. Verify in Observe
After deploying, invoke your Lambda function. Within seconds, telemetry should appear in Observe:
- View traces in the Trace Explorer, showing Lambda invocation spans with downstream calls (DynamoDB, S3, HTTP, etc.)
- View runtime metrics like invocation duration and cold starts in the Metrics Explorer.
- If your collector configuration includes a logs pipeline, view logs in the Log Explorer.
Filter for your function in Observe using the following:
service.name = "my-function-name"
Troubleshooting
Check the following table for guidance if you encounter any problems.
| Symptom | Cause | Fix |
|---|---|---|
| No data in Observe | Collector config not found | Verify OPENTELEMETRY_COLLECTOR_CONFIG_URI points to the correct path. |
| No data in Observe | Credential issue | Confirm OBSERVE_CUSTOMER_ID and OBSERVE_TOKEN are valid. |
| No data in Observe | Layer architecture mismatch | Ensure collector layer architecture (arm64/amd64) matches your function. |
| Cold start timeout | Java agent overhead | Use provisioned concurrency or limit instrumentation scope. |
| Missing downstream spans | Library not auto-instrumented | Check the supported libraries and frameworks for your language in the APM instrumentation documentation. |
AWS_LAMBDA_EXEC_WRAPPER error | Wrapper not in layer | Confirm the instrumentation layer ARN is added correctly. |
Layer version reference
The following table summarizes the layer and version compatibility:
| Layer | Version | ARN |
|---|---|---|
| Collector (arm64) | 0.22.0 | arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-collector-<arch>-0_22_0:1 |
| Collector (amd64) | 0.22.0 | arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-collector-<arch>-0_22_0:1 |
| Python | 0.20.0 | arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-python-0_20_0:1 |
| Node.js | 0.22.0 | arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-nodejs-0_22_0:1 |
| Java (Agent) | 0.20.0 | arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-javaagent-0_20_0:1 |
| Java (Wrapper) | 0.20.0 | arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-javawrapper-0_20_0:1 |
| Ruby | 0.14.0 | arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-ruby-0_14_0:1 |
Find the latest releases in the opentelemetry-lambda documentation.
Updated about 1 hour ago