Skip to content

AWS Lambda

Wrap SQS-triggered Lambda handlers in Python or Node.js, and read what the wrapper records.

Based on 12 repo files at 54ea61b

python/src/trailbeam/aws_lambda.py, python/README.md, python/tests/test_aws_lambda.py, node/src/lambda.js, node/index.d.ts, node/README.md, node/test/lambda.test.js, docs/span-contract.md, server/src/trailbeam_store/timeline.py, server/src/trailbeam_store/web.py, server/src/trailbeam_store/__main__.py, server/src/trailbeam_store/server.py

On this page

The wrapper records one trailbeam receive per SQS record before your handler runs, and one trailbeam outcome per record after it returns or throws. Lambda's event source mapping makes the ReceiveMessage and DeleteMessage calls, so the wrapper cannot see them; Trailbeam infers what it can.

What the Lambda wrapper records, in orderLambda's poller, the event source mapping, calls ReceiveMessage, which Trailbeam does not observe, and invokes the function with a batch of records. The Trailbeam wrapper emits one Trailbeam receive per record and flushes them to Trailbeam, waiting at most 2000 ms by default, before your handler runs. When the handler returns or throws, the wrapper emits one Trailbeam outcome per record and flushes again, then returns the response with batchItemFailures. The event source mapping then deletes the successful records, which Trailbeam does not observe; Trailbeam infers a deleted_by_lambda step from each success outcome. Amazon SQS Lambda poller Trailbeam wrapper Your handler Trailbeam server ReceiveMessage not observed batch of records invoke(event) Trailbeam receive, one per record flushed before the handler, at most 2000 ms handler(event) returns or throws Trailbeam outcome, one per record flushed again batchItemFailures DeleteMessage for the successes Dashed: the poller talks to SQS outside your function, so Trailbeam sees neither call. Trailbeam infers deleted_by_lambda from each success outcome.
Figure 1 One invocation, top to bottom. The Lambda poller is the event source mapping. Dashed arrows happen outside your function, so Trailbeam cannot record them: the poll that delivered the batch and the delete that follows a success. The wrapper flushes twice, so a function that dies mid-handler has still exported its receives.

Install#

The wrapper ships in the same packages as the client hooks. Bundle them into your deployment package:

  • Python: pip install ./trailbeam/python -t build/ into your deployment package directory. See Python.
  • Node.js 20+: npm install ./trailbeam/node. See Node.js.

Set these on the function:

shell
TRAILBEAM_RECEIPT_HASH_KEY=<the key every service on the queue uses>
TRAILBEAM_OTLP_TRACES_ENDPOINT=https://trailbeam-collector.internal:4318/v1/traces
OTEL_SERVICE_NAME=orders-lambda

The endpoint must be reachable from the function's network.

Wrap the handler#

Python#

python
import trailbeam.aws_lambda

@trailbeam.aws_lambda.wrap
def handler(event, context):
    failures = []
    for record in event["Records"]:
        try:
            process(record["body"])
        except Exception:
            failures.append({"itemIdentifier": record["messageId"]})
    return {"batchItemFailures": failures}

With options: @trailbeam.aws_lambda.wrap(receipt_hash_key=..., report_batch_item_failures=False).

Node.js#

javascript
const trailbeam = require('@trailbeam/sdk');

exports.handler = trailbeam.wrapHandler(async (event, context) => {
  const batchItemFailures = [];
  for (const record of event.Records) {
    try {
      await processBody(record.body);
    } catch {
      batchItemFailures.push({ itemIdentifier: record.messageId });
    }
  }
  return { batchItemFailures };
}, { reportBatchItemFailures: true });

Async, sync and callback-style (event, context, callback) handlers are accepted. The wrapper is always an async handler with two declared parameters, so the runtime treats it as async.

The wrapper creates its tracer provider when the module loads (once per execution environment), not per invocation.

Options#

Python Node.js Type Default What it does
receipt_hash_key receiptHashKey string TRAILBEAM_RECEIPT_HASH_KEY, then the published default key Key for trailbeam.receipt_handle.hmac.
report_batch_item_failures reportBatchItemFailures bool True / true Must match whether the event source mapping has ReportBatchItemFailures in FunctionResponseTypes. When false, batchItemFailures is ignored, as Lambda ignores it.
flush_timeout_millis flushTimeoutMs int (ms) 2000 Longest wait for each flush. An unreachable endpoint cannot hold the invocation longer.
span_exporter spanExporter SpanExporter OTLP/HTTP to TRAILBEAM_OTLP_TRACES_ENDPOINT Replaces the exporter (batch processor kept).
span_processor spanProcessor SpanProcessor none Replaces processor and exporter.

The wrapped function exposes its provider as trailbeam_tracer_provider (Python) or trailbeamTracerProvider (Node.js).

What each invocation records#

  1. The wrapper picks the records with eventSource = aws:sqs and a non-empty messageId. An event with none (API Gateway, a test event) goes to your handler untouched, with no spans.
  2. It emits one trailbeam receive per record and flushes before calling your handler, so a timeout or out-of-memory kill cannot lose it.
  3. It calls your handler.
  4. It emits one trailbeam outcome per record and flushes again.

Each flush waits at most the flush timeout, so the wrapper adds up to two flush timeouts to an invocation when the endpoint is slow. A flush that fails never fails the invocation.

The trailbeam receive span#

Attribute Value
trailbeam.source lambda
messaging.operation.name LambdaEvent
messaging.message.id record messageId
trailbeam.receipt_handle.hmac, trailbeam.receipt_handle.key_id hash of record receiptHandle
trailbeam.receive_count, trailbeam.sent_timestamp_ms, trailbeam.first_receive_timestamp_ms, trailbeam.aws_trace_header, trailbeam.dead_letter_queue_source_arn from record attributes (ApproximateReceiveCount, SentTimestamp, ...)
trailbeam.queue.arn record eventSourceARN
messaging.destination.name, aws.sqs.queue.url built from the ARN: https://sqs.<region>.amazonaws.com/<account>/<name>
faas.name context.function_name / context.functionName, else AWS_LAMBDA_FUNCTION_NAME
faas.invocation_id aws_request_id / awsRequestId (omitted if missing)
messaging.client.id AWS_LAMBDA_LOG_STREAM_NAME (one execution environment), else service.instance.id
trailbeam.start_observed false: the span is zero-length at the moment the event reached the wrapper
trailbeam.order.returned one tick; there is no trailbeam.order.issued

The receive time is an upper bound: the real receive happened no later than the handler start. Whether Lambda records ever carry DeadLetterQueueSourceArn is not verified; the wrapper copies it if present.

The trailbeam outcome span#

One per record, zero-length, with trailbeam.order.issued = trailbeam.order.returned (one tick after the handler). Same faas.* attributes as the receive.

trailbeam.outcome When
success Handler returned and the record is not in batchItemFailures. Also when the result is None/null, {}, a non-object, or batchItemFailures is null or [].
failed_reported The record's messageId is an itemIdentifier in batchItemFailures.
failed_invalid_response batchItemFailures is not a list, or any entry has a missing, empty or unknown itemIdentifier. Lambda fails the whole batch, so every record gets this.
failed_exception The handler threw. trailbeam.error_type is the class name only (ValueError, TypeError), never the message. A thrown non-object in Node.js gives its typeof, e.g. string. The error is rethrown.

In Python only Exception is caught. SystemExit and KeyboardInterrupt pass through with no outcome, the same as a kill.

How Trailbeam reads it#

Trailbeam matches an outcome to its receive by faas.invocation_id.

  • After a success outcome it adds an inferred step Deleted by Lambda, and the message state is deleted_inferred. The real delete is not observed.
  • A Lambda receive with no outcome is processing while it is the latest receive and younger than --lambda-timeout (default 900 s, the Lambda maximum). After that, Trailbeam adds Lambda invocation ended without an outcome with a window of receive time to receive time plus the timeout.

A record whose first invocation timed out and was redelivered:

Timeline sample
  1. 09:14:02.118Sentorders-api · SendMessage
  2. 09:14:02.403Receivedorders-lambda · receive count 1 · no later than
  3. inferredLambda invocation ended without an outcomemost likely timed out or ran out of memory
  4. inferredVisibility timeout expiredSQS delivered it again
  5. 09:14:33.512Receivedorders-lambda · receive count 2 · no later than
  6. 09:14:33.790Lambda outcomesuccess
  7. inferredDeleted by Lambdadelete not observed

Client hooks inside a Lambda#

If your handler also calls SQS through an instrumented client (trailbeam.boto.install() or trailbeam.instrument(...)), those spans use a separate provider that the wrapper does not flush. Flush it before returning: in Node.js await trailbeam.forceFlush(); in Python call .force_flush() on the provider install() returned.

Verify it works#

Invoke the function with a sample SQS event that has a messageId, then:

shell
curl -s http://127.0.0.1:4318/api/messages/<messageId>

Look for a received event with "consumer": {"source": "lambda", "faas_name": ..., "faas_invocation_id": ...} and a lambda_outcome event with outcome. In tests, pass span_processor=SimpleSpanProcessor(InMemorySpanExporter()) (Python) or spanProcessor (Node.js).

Common errors#

What you see Cause Fix
No spans at all Event has no aws:sqs records, or the endpoint is unreachable from the VPC Check the trigger; open the network path to Trailbeam or Collector.
Invocations slower by about 2 s Endpoint unreachable, each flush waits flush_timeout_millis Fix the endpoint or lower the timeout.
failed_invalid_response for a whole batch An itemIdentifier that is not a messageId in this batch Return record.messageId exactly.
failed_reported for records Lambda did not retry Mapping lacks ReportBatchItemFailures, so Lambda ignored batchItemFailures Turn it on in the mapping, or set the option to false.
Kills appear only after 15 minutes Trailbeam uses the default --lambda-timeout 900 Lower it to the function timeout.
default_key notice No key set on the function Set TRAILBEAM_RECEIPT_HASH_KEY.