For the complete documentation index, see llms.txt. Markdown versions are available by appending .md to documentation URLs.

Instrument AWS Lambda Container Images with OpenTelemetry

SigNoz Cloud - This page applies to SigNoz Cloud editions.
Self-Host - This page applies to self-hosted SigNoz editions.

Overview

A Lambda function packaged as a container image cannot use Lambda layers. To instrument such a function, unpack the OpenTelemetry layer into the image when you build it.

This guide covers Python, Node.js, and Java. Each setup has two parts:

  • The instrumentation goes in the image.
  • The endpoint and the ingestion key go in the function configuration.

Prerequisites

Send traces to SigNoz

Step 1: Add the OpenTelemetry layer to your image

The OpenTelemetry Lambda project publishes each layer as a zip file on GitHub. Unpack that zip into /opt in a build stage, then copy /opt into your function image.

Dockerfile
# Stage 1: unpack the OpenTelemetry Lambda layer
FROM public.ecr.aws/docker/library/alpine:3 AS otel
RUN apk add --no-cache curl unzip \
    && curl -fsSL -o /tmp/layer.zip \
       https://github.com/open-telemetry/opentelemetry-lambda/releases/download/layer-python%2F0.21.0/opentelemetry-python-layer.zip \
    && unzip /tmp/layer.zip -d /opt \
    && chmod -R 755 /opt
 
# Stage 2: your function image
FROM public.ecr.aws/lambda/python:3.13
 
COPY --from=otel /opt /opt
ENV AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument
ENV OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
 
COPY requirements.txt ${LAMBDA_TASK_ROOT}/
RUN pip install --no-cache-dir -r ${LAMBDA_TASK_ROOT}/requirements.txt
COPY app.py ${LAMBDA_TASK_ROOT}/
 
CMD [ "app.lambda_handler" ]

Two ENV lines control the instrumentation:

  • AWS_LAMBDA_EXEC_WRAPPER points at the wrapper script from the layer. The AWS base image entrypoint runs this script before the runtime starts, so OpenTelemetry loads before your handler does.
  • OTEL_EXPORTER_OTLP_PROTOCOL selects OTLP over HTTP. Java needs this, because the Java SDK defaults to gRPC. The other layers already default to HTTP, so the line is harmless there.

Step 2: Point the function at SigNoz

Do not put the ingestion key in the image. Anyone who can pull the image can read its layers. Set the endpoint on the function instead:

aws lambda update-function-configuration \
  --function-name <function-name> \
  --environment "Variables={\
OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.<region>.signoz.cloud:443,\
OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key=<your-ingestion-key>,\
OTEL_SERVICE_NAME=<service-name>,\
OTEL_TRACES_SAMPLER=always_on}"

Verify these values:

  • <function-name>: The name of your Lambda function.
  • <region>: Your SigNoz Cloud region.
  • <your-ingestion-key>: Your SigNoz ingestion key.
  • <service-name>: The name this function gets in SigNoz. If you omit it, the layer uses the function name.

OTEL_TRACES_SAMPLER=always_on records every invocation. The default sampler honors the caller's decision, which drops spans when an upstream service samples them out.

Step 3: Build and push the image

Build for the architecture of the function. The --provenance=false option keeps the image compatible with Lambda, and --load writes the result into your local image store so that docker tag can find it.

docker buildx build --platform linux/amd64 --provenance=false --load -t <image-name>:latest .
 
aws ecr get-login-password --region <aws-region> \
  | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<aws-region>.amazonaws.com
 
docker tag <image-name>:latest <account-id>.dkr.ecr.<aws-region>.amazonaws.com/<repository>:latest
docker push <account-id>.dkr.ecr.<aws-region>.amazonaws.com/<repository>:latest
 
aws lambda update-function-code \
  --function-name <function-name> \
  --image-uri <account-id>.dkr.ecr.<aws-region>.amazonaws.com/<repository>:latest

Verify these values:

  • <aws-region>: The AWS region of the ECR repository and the function. Both must be in the same region.
  • <account-id>: Your 12-digit AWS account ID.
  • <repository>: The name of your ECR repository.

If the function runs on Graviton, build with --platform linux/arm64 instead.

Validate

  1. Invoke the function once, either from the AWS console or with aws lambda invoke.
  2. Open Traces in the SigNoz left navigation menu.
  3. Filter on service.name with the value you set in OTEL_SERVICE_NAME.
SigNoz Traces Explorer listing spans from a Lambda container image function
Spans from a container image function in the Traces Explorer

Each invocation produces one server span for the invocation itself, plus a client span for every outbound call the layer instruments. Open a trace to see the full tree.

Trace detail view showing the Lambda invocation span with a nested outbound HTTP client span
The invocation span, with the outbound HTTP call captured by the layer

If you see only the invocation span, the layer loaded and the export works. The library that you called is not on the layer's instrumentation list.

Troubleshooting

No traces reach SigNoz

  • Likely cause: the wrapper never ran, or the export is failing silently.
  • Fix: Make sure that AWS_LAMBDA_EXEC_WRAPPER matches the wrapper path for your language, and that the ingestion key is set on the function.
  • Verify: Set OTEL_LOG_LEVEL=debug on the function, invoke it again, and read the export errors in the CloudWatch logs.

The function fails with Runtime.InvalidEntrypoint

  • Likely cause: the image overrides the entrypoint that the AWS base image provides.
  • Fix: Keep the handler in CMD and do not add your own ENTRYPOINT. The base image entrypoint takes exactly one argument, the handler.
  • Verify: Invoke the function again. It reaches your handler code.

Deploying the image fails with image manifest ... is not supported

  • Likely cause: Buildx attached provenance attestations to the image, and Lambda rejects those.
  • Fix: Rebuild with --provenance=false. If your deployment tool runs the build, set the same option there.
  • Verify: Deploy again. Lambda accepts the image.

Java throws NoClassDefFoundError on the first invocation

  • Likely cause: If the error names APIGatewayProxyRequestEvent, the aws-lambda-java-events dependency is missing. If it names an OpenTelemetry class, the layer JARs are not on the classpath.
  • Fix: Add aws-lambda-java-events to pom.xml, as shown in Step 1. For the layer JARs, make sure that the Dockerfile copies /opt/java/lib/ into ${LAMBDA_TASK_ROOT}/lib/ and that a later COPY does not overwrite that directory.
  • Verify: Invoke the function again. The invocation span appears in SigNoz.

Export through the OTel Collector layer (Optional)

The setup above sends spans from the function process straight to SigNoz. To batch, filter, or enrich telemetry before it leaves the function environment, run the OpenTelemetry Collector as a Lambda extension instead.

Add the Collector layer to the image

Unpack the Collector layer in a third build stage:

Dockerfile
# Stage 3: unpack the OpenTelemetry Collector layer
FROM public.ecr.aws/docker/library/alpine:3 AS otelcol
RUN apk add --no-cache curl unzip \
    && curl -fsSL -o /tmp/collector.zip \
       https://github.com/open-telemetry/opentelemetry-lambda/releases/download/layer-collector/0.23.0/opentelemetry-collector-layer-amd64.zip \
    && unzip /tmp/collector.zip -d /opt \
    && chmod -R 755 /opt

Then add these lines to the stage that builds your function image, next to the COPY --from=otel line from Step 1:

Dockerfile
COPY --from=otelcol /opt/extensions /opt/extensions
COPY collector.yaml /opt/collector-config/config.yaml
ENV OPENTELEMETRY_COLLECTOR_CONFIG_URI=/opt/collector-config/config.yaml

For an arm64 function, download opentelemetry-collector-layer-arm64.zip instead. The Collector layer is architecture-specific, so it must match the platform you build the image for.

Add the Collector configuration

Add a file named collector.yaml next to your Dockerfile. The COPY above places it where the extension reads it:

collector.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 'localhost:4318'
 
processors:
  batch:
  # decouple must be last. It lets the function return before the export
  # to SigNoz finishes.
  decouple:
 
exporters:
  # Collector layer 0.23.0 bundles Collector v0.157.0.
  # On Collector v0.143.0 and older, use "otlphttp" instead.
  otlp_http:
    endpoint: 'https://ingest.<region>.signoz.cloud:443'
    headers:
      # The image is readable by anyone who can pull it, so read the key from
      # the function environment instead of writing it into this file.
      signoz-ingestion-key: '${env:SIGNOZ_INGESTION_KEY}'
 
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch, decouple]
      exporters: [otlp_http]
    # The layers export metrics to the same endpoint. Without this pipeline
    # the function logs a 404 on every invocation.
    metrics:
      receivers: [otlp]
      processors: [batch, decouple]
      exporters: [otlp_http]

Point the function at the Collector

The function must now export to the Collector on localhost. Replace the endpoint from Step 2, and keep the ingestion key on the function, where collector.yaml reads it:

OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
SIGNOZ_INGESTION_KEY=<your-ingestion-key>

Remove OTEL_EXPORTER_OTLP_HEADERS. The Collector adds the key now, so the function no longer needs it.

Next steps

Get Help

If you need help with the steps in this topic, please reach out to us on SigNoz Community Slack. If you are a SigNoz Cloud user, please use in product chat support located at the bottom right corner of your SigNoz instance or contact us at cloud-support@signoz.io.

Is this page helpful

Last updated—August 31, 2026

Edit on GitHub