AWS Lambda
Wrap SQS-triggered Lambda handlers in Python or Node.js, and read what the wrapper records.
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.
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:
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-lambdaThe endpoint must be reachable from the function's network.
Wrap the handler#
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#
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#
- The wrapper picks the records with
eventSource=aws:sqsand a non-emptymessageId. An event with none (API Gateway, a test event) goes to your handler untouched, with no spans. - It emits one
trailbeam receiveper record and flushes before calling your handler, so a timeout or out-of-memory kill cannot lose it. - It calls your handler.
- It emits one
trailbeam outcomeper 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(default900s, 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:
- 09:14:02.118Sentorders-api · SendMessage
- 09:14:02.403Receivedorders-lambda · receive count 1 · no later than
- inferredLambda invocation ended without an outcomemost likely timed out or ran out of memory
- inferredVisibility timeout expiredSQS delivered it again
- 09:14:33.512Receivedorders-lambda · receive count 2 · no later than
- 09:14:33.790Lambda outcomesuccess
- 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:
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. |
Related#
- Debug Lambda timeouts and crashes
- Recorded and inferred steps
- Run Trailbeam
- Span contract: Trailbeam outcome
Record
AWS Lambda terms in Vocabulary
Handler
Amazon SQS terms in Vocabulary
Event source mapping
AWS Lambda terms in Vocabulary
Hooks
trailbeam.boto.install()), the AWS SDK v3 middleware (trailbeam.instrument(client)) and the Lambda wrapper.Batch item failures
ReportBatchItemFailures turned on, your handler can return the IDs of the records that failed. Lambda deletes the rest and leaves those to become visible again. Without it, any error fails the whole batch.AWS Lambda terms in Vocabulary
OTLP
OpenTelemetry terms in Vocabulary
Span
OpenTelemetry terms in Vocabulary
Receive count
ApproximateReceiveCount: how many times SQS has returned the message. It goes up by one on every receive.Amazon SQS terms in Vocabulary
SentTimestamp
Amazon SQS terms in Vocabulary
Receive
ReceiveMessage call returning a message. A receive does not remove the message; it starts its visibility timeout.Amazon SQS terms in Vocabulary
DeadLetterQueueSourceArn
Amazon SQS terms in Vocabulary
Message
Amazon SQS terms in Vocabulary
Inferred
inferred: true. See Recorded and inferred steps.Step
Function timeout
Task timed out after N seconds.AWS Lambda terms in Vocabulary
Collector