From 5436807e3d0dbe2717caf35bf93c7c78541b4ec2 Mon Sep 17 00:00:00 2001 From: Josh Fenton Date: Tue, 19 May 2026 21:00:51 +1000 Subject: [PATCH 1/3] fix(aws-lambda): support ALB multiValueHeaders --- .changelog/4605.fixed | 1 + .../instrumentation/aws_lambda/__init__.py | 37 +++++++++++++---- .../test_aws_lambda_instrumentation_manual.py | 40 +++++++++++++++++++ 3 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 .changelog/4605.fixed diff --git a/.changelog/4605.fixed b/.changelog/4605.fixed new file mode 100644 index 0000000000..80e84cade8 --- /dev/null +++ b/.changelog/4605.fixed @@ -0,0 +1 @@ +`opentelemetry-instrumentation-aws-lambda`: support ALB multiValueHeaders diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py index 02f3bc7946..3267b5df05 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py @@ -157,20 +157,40 @@ def _default_event_context_extractor(lambda_event: Any) -> Context: Returns: A Context with configuration found in the event. """ - headers = None - try: - headers = lambda_event["headers"] - except (TypeError, KeyError): + headers = _extract_http_headers(lambda_event) + if not headers: logger.debug( "Extracting context from Lambda Event failed: either enable X-Ray active tracing or configure API Gateway to trigger this Lambda function as a pure proxy. Otherwise, generated spans will have an invalid (empty) parent context." ) - if not isinstance(headers, dict): - headers = {} return get_global_textmap().extract( CIDict(headers), ) +def _extract_http_headers(lambda_event: Any) -> dict[str, Any]: + try: + headers = lambda_event["headers"] + except (TypeError, KeyError): + headers = None + + if isinstance(headers, dict): + return headers + + try: + multi_value_headers = lambda_event["multiValueHeaders"] + except (TypeError, KeyError): + return {} + + if not isinstance(multi_value_headers, dict): + return {} + + normalized_headers = {} + for key, values in multi_value_headers.items(): + if isinstance(values, list) and values: + normalized_headers[key] = values[0] + return normalized_headers + + def _determine_parent_context( lambda_event: Any, event_context_extractor: Callable[[Any], Context], @@ -207,8 +227,9 @@ def _set_api_gateway_v1_proxy_attributes( """ span.set_attribute(HTTP_METHOD, lambda_event.get("httpMethod")) - if lambda_event.get("headers"): - headers = CIDict(lambda_event["headers"]) + headers = _extract_http_headers(lambda_event) + if headers: + headers = CIDict(headers) if "User-Agent" in headers: span.set_attribute( HTTP_USER_AGENT, diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py index cf7e1bee8a..d0d8697c52 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py @@ -3,6 +3,7 @@ import logging import os +from copy import deepcopy from dataclasses import dataclass from importlib import import_module, reload from typing import Any, Callable, Dict @@ -741,9 +742,48 @@ def test_alb_multi_value_header_event_sets_attributes(self): { FAAS_TRIGGER: "http", HTTP_METHOD: "GET", + HTTP_SCHEME: "https", + HTTP_USER_AGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)", + NET_HOST_NAME: "lambda-846800462-us-east-2.elb.amazonaws.com", }, ) + def test_alb_multi_value_header_event_extracts_parent_context(self): + test_env_patch = mock.patch.dict( + "os.environ", + { + **os.environ, + _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, + OTEL_PROPAGATORS: "tracecontext", + }, + ) + test_env_patch.start() + reload(propagate) + + AwsLambdaInstrumentor().instrument() + + event = deepcopy(MOCK_LAMBDA_ALB_MULTI_VALUE_HEADER_EVENT) + event["multiValueHeaders"][ + TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME + ] = [MOCK_W3C_TRACE_CONTEXT_SAMPLED] + + mock_execute_lambda(event) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID) + + parent_context = span.parent + self.assertEqual( + parent_context.trace_id, span.get_span_context().trace_id + ) + self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID) + self.assertTrue(parent_context.is_remote) + + test_env_patch.stop() + def test_dynamo_db_event_sets_attributes(self): AwsLambdaInstrumentor().instrument() From c1de289e250ceb39672b4fc7e2e77292bb737b04 Mon Sep 17 00:00:00 2001 From: Josh Fenton Date: Sat, 23 May 2026 23:00:32 +1000 Subject: [PATCH 2/3] Fix type annotation in lambda instrumentation --- .../src/opentelemetry/instrumentation/aws_lambda/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py index 3267b5df05..e712015f17 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py @@ -353,8 +353,8 @@ def _instrument( wrapped_function_name, flush_timeout, event_context_extractor: Callable[[Any], Context], - tracer_provider: TracerProvider = None, - meter_provider: MeterProvider = None, + tracer_provider: TracerProvider | None = None, + meter_provider: MeterProvider | None = None, ): # pylint: disable=too-many-locals # pylint: disable=too-many-statements From 43938c914dd4f5a0f7aaf4a2c47e90df8cad6211 Mon Sep 17 00:00:00 2001 From: Josh Fenton Date: Tue, 26 May 2026 10:13:38 +1000 Subject: [PATCH 3/3] Revert "Fix type annotation in lambda instrumentation" This reverts commit c1de289e250ceb39672b4fc7e2e77292bb737b04. --- .../src/opentelemetry/instrumentation/aws_lambda/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py index e712015f17..3267b5df05 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py @@ -353,8 +353,8 @@ def _instrument( wrapped_function_name, flush_timeout, event_context_extractor: Callable[[Any], Context], - tracer_provider: TracerProvider | None = None, - meter_provider: MeterProvider | None = None, + tracer_provider: TracerProvider = None, + meter_provider: MeterProvider = None, ): # pylint: disable=too-many-locals # pylint: disable=too-many-statements