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:

LayerPurposeARN pattern
CollectorRuns 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
InstrumentationAuto-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

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 variablevalue
OPENTELEMETRY_COLLECTOR_CONFIG_URI/var/task/collector.yaml
OBSERVE_CUSTOMER_IDYour Observe customer ID, such as 123456789012.
OBSERVE_TOKENYour Observe Datastream token
📘

Note

You 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:

  1. Attaches the OTel Collector layer (runs as a sidecar extension, exports to Observe)
  2. Attaches the OTel Python instrumentation layer (wraps your handler to generate traces/metrics)
  3. Sets AWS_LAMBDA_EXEC_WRAPPER — tells Lambda to run the OTel wrapper script before your handler
  4. Sets OPENTELEMETRY_COLLECTOR_CONFIG_URI — tells the collector where to find its config
  5. Sets OTEL_SERVICE_NAME — identifies this function in Observe’s tracing UI
  6. 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

📘

Note

Cold 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.

SymptomCauseFix
No data in ObserveCollector config not foundVerify OPENTELEMETRY_COLLECTOR_CONFIG_URI points to the correct path.
No data in ObserveCredential issueConfirm OBSERVE_CUSTOMER_ID and OBSERVE_TOKEN are valid.
No data in ObserveLayer architecture mismatchEnsure collector layer architecture (arm64/amd64) matches your function.
Cold start timeoutJava agent overheadUse provisioned concurrency or limit instrumentation scope.
Missing downstream spansLibrary not auto-instrumentedCheck the supported libraries and frameworks for your language in the APM instrumentation documentation.
AWS_LAMBDA_EXEC_WRAPPER errorWrapper not in layerConfirm the instrumentation layer ARN is added correctly.

Layer version reference

The following table summarizes the layer and version compatibility:

LayerVersionARN
Collector (arm64)0.22.0arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-collector-<arch>-0_22_0:1
Collector (amd64)0.22.0arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-collector-<arch>-0_22_0:1
Python0.20.0arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-python-0_20_0:1
Node.js0.22.0arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-nodejs-0_22_0:1
Java (Agent)0.20.0arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-javaagent-0_20_0:1
Java (Wrapper)0.20.0arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-javawrapper-0_20_0:1
Ruby0.14.0arn:aws:lambda:<region>:<account-id>:layer:opentelemetry-ruby-0_14_0:1

Find the latest releases in the opentelemetry-lambda documentation.