diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml index ac039f15d1..77cabf9338 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "opentelemetry-instrumentation == 0.60b0.dev", "opentelemetry-semantic-conventions == 0.60b0.dev", "opentelemetry-propagator-aws-xray ~= 1.0", + "opentelemetry-util-http == 0.60b0.dev", ] [project.optional-dependencies] 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 9b450cdf21..24cba48cdd 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 @@ -80,6 +80,18 @@ def custom_event_context_extractor(lambda_event): from opentelemetry import context as context_api from opentelemetry.context.context import Context +from opentelemetry.instrumentation._semconv import ( + _get_schema_url, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _set_http_method, + _set_http_net_host, + _set_http_scheme, + _set_http_status_code, + _set_http_target, + _set_http_user_agent, + _StabilityMode, +) from opentelemetry.instrumentation.aws_lambda.package import _instruments from opentelemetry.instrumentation.aws_lambda.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor @@ -95,15 +107,7 @@ def custom_event_context_extractor(lambda_event): FAAS_TRIGGER, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( - HTTP_METHOD, HTTP_ROUTE, - HTTP_SCHEME, - HTTP_STATUS_CODE, - HTTP_TARGET, - HTTP_USER_AGENT, -) -from opentelemetry.semconv._incubating.attributes.net_attributes import ( - NET_HOST_NAME, ) from opentelemetry.trace import ( Span, @@ -113,6 +117,10 @@ def custom_event_context_extractor(lambda_event): get_tracer_provider, ) from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.util.http import ( + _parse_url_query, + sanitize_method, +) logger = logging.getLogger(__name__) @@ -181,69 +189,105 @@ def _determine_parent_context( def _set_api_gateway_v1_proxy_attributes( - lambda_event: Any, span: Span + lambda_event: Any, + span: Span, + sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> Span: """Sets HTTP attributes for REST APIs and v1 HTTP APIs More info: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format """ - span.set_attribute(HTTP_METHOD, lambda_event.get("httpMethod")) + http_method = lambda_event.get("httpMethod") + if not http_method: + http_method = ( + lambda_event.get("requestContext", {}) + .get("http", {}) + .get("method") + ) + span_attributes = {} + _set_http_method( + span_attributes, http_method, sanitize_method(http_method), sem_conv_opt_in_mode + ) if lambda_event.get("headers"): if "User-Agent" in lambda_event["headers"]: - span.set_attribute( - HTTP_USER_AGENT, + _set_http_user_agent( + span_attributes, lambda_event["headers"]["User-Agent"], + sem_conv_opt_in_mode, ) if "X-Forwarded-Proto" in lambda_event["headers"]: - span.set_attribute( - HTTP_SCHEME, + _set_http_scheme( + span_attributes, lambda_event["headers"]["X-Forwarded-Proto"], + sem_conv_opt_in_mode, ) if "Host" in lambda_event["headers"]: - span.set_attribute( - NET_HOST_NAME, - lambda_event["headers"]["Host"], - ) + host = lambda_event["headers"]["Host"] + _set_http_net_host(span_attributes, host, sem_conv_opt_in_mode) if "resource" in lambda_event: span.set_attribute(HTTP_ROUTE, lambda_event["resource"]) if lambda_event.get("queryStringParameters"): - span.set_attribute( - HTTP_TARGET, - f"{lambda_event['resource']}?{urlencode(lambda_event['queryStringParameters'])}", + http_target = f"{lambda_event['resource']}?{urlencode(lambda_event['queryStringParameters'])}" + path, query = _parse_url_query(http_target) + _set_http_target( + span_attributes, + http_target, + path, + query, + sem_conv_opt_in_mode, ) else: - span.set_attribute(HTTP_TARGET, lambda_event["resource"]) + res = lambda_event["resource"] + path, query = _parse_url_query(res) + _set_http_target( + span_attributes, + res, + path, + query, + sem_conv_opt_in_mode, + ) + + for key, value in span_attributes.items(): + span.set_attribute(key, value) return span def _set_api_gateway_v2_proxy_attributes( - lambda_event: Any, span: Span + lambda_event: Any, + span: Span, + sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> Span: """Sets HTTP attributes for v2 HTTP APIs More info: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html """ + span_attributes = {} if "domainName" in lambda_event["requestContext"]: - span.set_attribute( - NET_HOST_NAME, + _set_http_net_host( + span_attributes, lambda_event["requestContext"]["domainName"], + sem_conv_opt_in_mode, ) if lambda_event["requestContext"].get("http"): if "method" in lambda_event["requestContext"]["http"]: - span.set_attribute( - HTTP_METHOD, - lambda_event["requestContext"]["http"]["method"], + http_method = lambda_event["requestContext"]["http"]["method"] + _set_http_method( + span_attributes, + http_method, + sanitize_method(http_method), + sem_conv_opt_in_mode, ) if "userAgent" in lambda_event["requestContext"]["http"]: - span.set_attribute( - HTTP_USER_AGENT, + _set_http_user_agent( + span_attributes, lambda_event["requestContext"]["http"]["userAgent"], + sem_conv_opt_in_mode, ) if "path" in lambda_event["requestContext"]["http"]: span.set_attribute( @@ -251,16 +295,29 @@ def _set_api_gateway_v2_proxy_attributes( lambda_event["requestContext"]["http"]["path"], ) if lambda_event.get("rawQueryString"): - span.set_attribute( - HTTP_TARGET, - f"{lambda_event['requestContext']['http']['path']}?{lambda_event['rawQueryString']}", + http_target = f"{lambda_event['requestContext']['http']['path']}?{lambda_event['rawQueryString']}" + path, query = _parse_url_query(http_target) + _set_http_target( + span_attributes, + http_target, + path, + query, + sem_conv_opt_in_mode, ) else: - span.set_attribute( - HTTP_TARGET, - lambda_event["requestContext"]["http"]["path"], + http_target = lambda_event["requestContext"]["http"]["path"] + path, query = _parse_url_query(http_target) + _set_http_target( + span_attributes, + http_target, + path, + query, + sem_conv_opt_in_mode, ) + for key, value in span_attributes.items(): + span.set_attribute(key, value) + return span @@ -272,6 +329,7 @@ def _instrument( event_context_extractor: Callable[[Any], Context], tracer_provider: TracerProvider = None, meter_provider: MeterProvider = None, + sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): # pylint: disable=too-many-locals # pylint: disable=too-many-statements @@ -314,7 +372,7 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches __name__, __version__, tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(sem_conv_opt_in_mode), ) token = context_api.attach(parent_context) @@ -371,18 +429,22 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches if lambda_event.get("version") == "2.0": _set_api_gateway_v2_proxy_attributes( - lambda_event, span + lambda_event, span, sem_conv_opt_in_mode ) else: _set_api_gateway_v1_proxy_attributes( - lambda_event, span + lambda_event, span, sem_conv_opt_in_mode ) if isinstance(result, dict) and result.get("statusCode"): - span.set_attribute( - HTTP_STATUS_CODE, + attribute = {} + _set_http_status_code( + attribute, result.get("statusCode"), + sem_conv_opt_in_mode, ) + for key, value in attribute.items(): + span.set_attribute(key, value) finally: if token: context_api.detach(token) @@ -480,6 +542,10 @@ def _instrument(self, **kwargs): flush_timeout_env, ) + sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) + _instrument( self._wrapped_module_name, self._wrapped_function_name, @@ -489,6 +555,7 @@ def _instrument(self, **kwargs): ), tracer_provider=kwargs.get("tracer_provider"), meter_provider=kwargs.get("meter_provider"), + sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) def _uninstrument(self, **kwargs): 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 a468cb986a..362e844d4b 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 @@ -22,6 +22,10 @@ from opentelemetry import propagate from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.environment_variables import OTEL_PROPAGATORS +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _OpenTelemetrySemanticConventionStability, +) from opentelemetry.instrumentation.aws_lambda import ( _HANDLER, _X_AMZN_TRACE_ID, @@ -43,6 +47,8 @@ ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_METHOD, + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, HTTP_SCHEME, HTTP_STATUS_CODE, @@ -52,6 +58,17 @@ from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_NAME, ) +from opentelemetry.semconv._incubating.attributes.server_attributes import ( + SERVER_ADDRESS, +) +from opentelemetry.semconv._incubating.attributes.url_attributes import ( + URL_PATH, + URL_QUERY, + URL_SCHEME, +) +from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( + USER_AGENT_ORIGINAL, +) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import NoOpTracerProvider, SpanKind, StatusCode from opentelemetry.trace.propagation.tracecontext import ( @@ -139,6 +156,8 @@ class TestAwsLambdaInstrumentorBase(TestBase): def setUp(self): super().setUp() + _OpenTelemetrySemanticConventionStability._initialized = False + self.common_env_patch = mock.patch.dict( "os.environ", { @@ -801,3 +820,488 @@ def test_lambda_handles_handler_exception_with_api_gateway_proxy_event( self.assertEqual(event.name, "exception") exc_env_patch.stop() + + +class TestAwsLambdaInstrumentorNewSemconv(TestAwsLambdaInstrumentorBase): + """Test AWS Lambda Instrumentation with new semantic conventions""" + + def setUp(self): + super().setUp() + test_name = "" + if hasattr(self, "_testMethodName"): + test_name = self._testMethodName + sem_conv_mode = "default" + if "new_semconv" in test_name: + sem_conv_mode = "http" + elif "both_semconv" in test_name: + sem_conv_mode = "http/dup" + + self.common_env_patch = mock.patch.dict( + "os.environ", + { + _HANDLER: "tests.mocks.lambda_function.rest_api_handler", + "AWS_LAMBDA_FUNCTION_NAME": "mylambda", + OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, + }, + ) + _OpenTelemetrySemanticConventionStability._initialized = False + self.common_env_patch.start() + + def tearDown(self): + super().tearDown() + self.common_env_patch.stop() + AwsLambdaInstrumentor().uninstrument() + + def test_api_gateway_proxy_event_sets_attributes_new_semconv(self): + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes( + span, + MOCK_LAMBDA_CONTEXT_ATTRIBUTES, + ) + self.assertSpanHasAttributes( + span, + { + HTTP_REQUEST_METHOD: "POST", + HTTP_RESPONSE_STATUS_CODE: 200, + URL_PATH: "/{proxy+}", + URL_QUERY: "foo=bar", + SERVER_ADDRESS: "1234567890.execute-api.us-east-1.amazonaws.com", + USER_AGENT_ORIGINAL: "Custom User Agent String", + URL_SCHEME: "https", + }, + ) + # Ensure old attributes are not present + self.assertNotIn(HTTP_METHOD, span.attributes) + self.assertNotIn(HTTP_TARGET, span.attributes) + self.assertNotIn(NET_HOST_NAME, span.attributes) + self.assertNotIn(HTTP_USER_AGENT, span.attributes) + self.assertNotIn(HTTP_SCHEME, span.attributes) + self.assertNotIn(HTTP_STATUS_CODE, span.attributes) + + def test_api_gateway_proxy_event_sets_attributes_both_semconv(self): + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes( + span, + MOCK_LAMBDA_CONTEXT_ATTRIBUTES, + ) + # New semconv attributes + self.assertSpanHasAttributes( + span, + { + HTTP_STATUS_CODE: 200, + HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_REQUEST_METHOD: "POST", + HTTP_USER_AGENT: "Custom User Agent String", + SERVER_ADDRESS: "1234567890.execute-api.us-east-1.amazonaws.com", + URL_PATH: "/{proxy+}", + URL_QUERY: "foo=bar", + URL_SCHEME: "https", + }, + ) + # Old semconv attributes should also be present + self.assertSpanHasAttributes( + span, + { + HTTP_METHOD: "POST", + HTTP_TARGET: "/{proxy+}?foo=bar", + NET_HOST_NAME: "1234567890.execute-api.us-east-1.amazonaws.com", + HTTP_USER_AGENT: "Custom User Agent String", + HTTP_SCHEME: "https", + USER_AGENT_ORIGINAL: "Custom User Agent String", + }, + ) + + def test_api_gateway_http_api_proxy_event_sets_attributes_new_semconv( + self, + ): + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes( + span, + MOCK_LAMBDA_CONTEXT_ATTRIBUTES, + ) + self.assertSpanHasAttributes( + span, + { + FAAS_TRIGGER: "http", + HTTP_REQUEST_METHOD: "POST", + HTTP_ROUTE: "/path/to/resource", + URL_PATH: "/path/to/resource", + URL_QUERY: "parameter1=value1¶meter1=value2¶meter2=value", + SERVER_ADDRESS: "id.execute-api.us-east-1.amazonaws.com", + USER_AGENT_ORIGINAL: "agent", + }, + ) + # Ensure old attributes are not present + self.assertNotIn(HTTP_METHOD, span.attributes) + self.assertNotIn(HTTP_TARGET, span.attributes) + self.assertNotIn(NET_HOST_NAME, span.attributes) + self.assertNotIn(HTTP_USER_AGENT, span.attributes) + + def test_api_gateway_http_api_proxy_event_sets_attributes_both_semconv( + self, + ): + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes( + span, + MOCK_LAMBDA_CONTEXT_ATTRIBUTES, + ) + # New semconv attributes + self.assertSpanHasAttributes( + span, + { + FAAS_TRIGGER: "http", + HTTP_REQUEST_METHOD: "POST", + HTTP_ROUTE: "/path/to/resource", + URL_PATH: "/path/to/resource", + URL_QUERY: "parameter1=value1¶meter1=value2¶meter2=value", + SERVER_ADDRESS: "id.execute-api.us-east-1.amazonaws.com", + USER_AGENT_ORIGINAL: "agent", + }, + ) + # Old semconv attributes should also be present + self.assertSpanHasAttributes( + span, + { + HTTP_METHOD: "POST", + HTTP_TARGET: "/path/to/resource?parameter1=value1¶meter1=value2¶meter2=value", + NET_HOST_NAME: "id.execute-api.us-east-1.amazonaws.com", + HTTP_USER_AGENT: "agent", + }, + ) + + def test_alb_event_sets_attributes_new_semconv(self): + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_ALB_EVENT) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes( + span, + MOCK_LAMBDA_CONTEXT_ATTRIBUTES, + ) + self.assertSpanHasAttributes( + span, + { + FAAS_TRIGGER: "http", + HTTP_REQUEST_METHOD: "GET", + HTTP_RESPONSE_STATUS_CODE: 200, + }, + ) + # Ensure old HTTP_METHOD attribute is not present + self.assertNotIn(HTTP_METHOD, span.attributes) + + def test_alb_event_sets_attributes_both_semconv(self): + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_ALB_EVENT) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes( + span, + MOCK_LAMBDA_CONTEXT_ATTRIBUTES, + ) + # Both old and new attributes should be present + self.assertSpanHasAttributes( + span, + { + FAAS_TRIGGER: "http", + HTTP_REQUEST_METHOD: "GET", + HTTP_METHOD: "GET", + HTTP_STATUS_CODE: 200, + HTTP_RESPONSE_STATUS_CODE: 200, + }, + ) + + def test_api_gateway_without_optional_fields_new_semconv(self): + """Test API Gateway event without optional fields like User-Agent""" + event = { + "requestContext": { + "http": { + "method": "GET", + "path": "/test", + } + }, + "headers": {}, + } + + AwsLambdaInstrumentor().instrument() + mock_execute_lambda(event) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertSpanHasAttributes( + span, + { + FAAS_TRIGGER: "http", + HTTP_REQUEST_METHOD: "GET", + }, + ) + # Ensure old attribute is not present + self.assertNotIn(HTTP_METHOD, span.attributes) + # Optional attributes should not be present + self.assertNotIn(USER_AGENT_ORIGINAL, span.attributes) + self.assertNotIn(HTTP_USER_AGENT, span.attributes) + + def test_api_gateway_v1_with_scheme_and_host_new_semconv(self): + """Test API Gateway v1 event with X-Forwarded-Proto and Host headers""" + event = { + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + }, + "httpMethod": "PUT", + "resource": "/users/{id}", + "headers": { + "X-Forwarded-Proto": "https", + "Host": "api.example.com", + "User-Agent": "Mozilla/5.0", + }, + "queryStringParameters": {"filter": "active"}, + } + + AwsLambdaInstrumentor().instrument() + mock_execute_lambda(event) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertSpanHasAttributes( + span, + { + FAAS_TRIGGER: "http", + HTTP_REQUEST_METHOD: "PUT", + HTTP_ROUTE: "/users/{id}", + URL_PATH: "/users/{id}", + URL_QUERY: "filter=active", + URL_SCHEME: "https", + SERVER_ADDRESS: "api.example.com", + USER_AGENT_ORIGINAL: "Mozilla/5.0", + }, + ) + # Ensure old attributes are not present + self.assertNotIn(HTTP_METHOD, span.attributes) + self.assertNotIn(HTTP_TARGET, span.attributes) + self.assertNotIn(HTTP_SCHEME, span.attributes) + self.assertNotIn(NET_HOST_NAME, span.attributes) + self.assertNotIn(HTTP_USER_AGENT, span.attributes) + + def test_api_gateway_v1_with_scheme_and_host_both_semconv(self): + """Test API Gateway v1 event with both old and new semconv""" + event = { + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + }, + "httpMethod": "PUT", + "resource": "/users/{id}", + "headers": { + "X-Forwarded-Proto": "https", + "Host": "api.example.com", + "User-Agent": "Mozilla/5.0", + }, + "queryStringParameters": {"filter": "active"}, + } + + AwsLambdaInstrumentor().instrument() + mock_execute_lambda(event) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + # New semconv attributes + self.assertSpanHasAttributes( + span, + { + HTTP_REQUEST_METHOD: "PUT", + HTTP_METHOD: "PUT", + HTTP_SCHEME: "https", + URL_SCHEME: "https", + HTTP_STATUS_CODE: 200, + HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_TARGET: "/users/{id}?filter=active", + HTTP_USER_AGENT: "Mozilla/5.0", + USER_AGENT_ORIGINAL: "Mozilla/5.0", + NET_HOST_NAME: "api.example.com", + SERVER_ADDRESS: "api.example.com", + URL_PATH: "/users/{id}", + URL_QUERY: "filter=active", + }, + ) + # Old semconv attributes + self.assertSpanHasAttributes( + span, + { + HTTP_METHOD: "PUT", + HTTP_TARGET: "/users/{id}?filter=active", + HTTP_SCHEME: "https", + NET_HOST_NAME: "api.example.com", + HTTP_USER_AGENT: "Mozilla/5.0", + }, + ) + + def test_api_gateway_v2_without_query_string_new_semconv(self): + """Test API Gateway v2 event without query string""" + event = { + "requestContext": { + "http": { + "method": "DELETE", + "path": "/items/123", + "userAgent": "TestAgent/1.0", + }, + "domainName": "api.test.com", + }, + "version": "2.0", + } + + AwsLambdaInstrumentor().instrument() + mock_execute_lambda(event) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertSpanHasAttributes( + span, + { + HTTP_REQUEST_METHOD: "DELETE", + URL_PATH: "/items/123", + SERVER_ADDRESS: "api.test.com", + USER_AGENT_ORIGINAL: "TestAgent/1.0", + HTTP_RESPONSE_STATUS_CODE: 200, + }, + ) + # URL_QUERY should not be present when there's no query string + self.assertNotIn(URL_QUERY, span.attributes) + # Ensure old attributes are not present + self.assertNotIn(HTTP_METHOD, span.attributes) + self.assertNotIn(HTTP_TARGET, span.attributes) + + def test_status_code_response_new_semconv(self): + """Test that HTTP status code is correctly set with new semconv""" + AwsLambdaInstrumentor().instrument() + + # Create event with request context to trigger HTTP attribute extraction + event = MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT.copy() + + mock_execute_lambda(event) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertSpanHasAttributes( + span, + { + HTTP_RESPONSE_STATUS_CODE: 200, + }, + ) + # Ensure old attribute is not present + self.assertNotIn(HTTP_STATUS_CODE, span.attributes) + + def test_status_code_response_both_semconv(self): + AwsLambdaInstrumentor().instrument() + + event = MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT.copy() + + mock_execute_lambda(event) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + # Both old and new attributes should be present + self.assertSpanHasAttributes( + span, + { + HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_STATUS_CODE: 200, + }, + ) + + def test_non_http_trigger_unaffected_by_semconv_new(self): + """Test that non-HTTP triggers are unaffected by semconv changes""" + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertEqual(span.kind, SpanKind.CONSUMER) + self.assertSpanHasAttributes( + span, + MOCK_LAMBDA_CONTEXT_ATTRIBUTES, + ) + # Should not have any HTTP attributes + self.assertNotIn(HTTP_REQUEST_METHOD, span.attributes) + self.assertNotIn(HTTP_METHOD, span.attributes) + self.assertNotIn(URL_PATH, span.attributes) + self.assertNotIn(HTTP_TARGET, span.attributes) + + def test_non_http_trigger_unaffected_by_semconv_both(self): + """Test that non-HTTP triggers work correctly with both semconv mode""" + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_DYNAMO_DB_EVENT) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span, *_ = spans + self.assertEqual(span.kind, SpanKind.CONSUMER) + self.assertSpanHasAttributes( + span, + MOCK_LAMBDA_CONTEXT_ATTRIBUTES, + ) + # Should not have any HTTP attributes + self.assertNotIn(HTTP_REQUEST_METHOD, span.attributes) + self.assertNotIn(HTTP_METHOD, span.attributes) + self.assertNotIn(URL_PATH, span.attributes) + self.assertNotIn(HTTP_TARGET, span.attributes) diff --git a/uv.lock b/uv.lock index 39c808a69f..4ffe82dd10 100644 --- a/uv.lock +++ b/uv.lock @@ -2867,6 +2867,7 @@ dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-propagator-aws-xray" }, { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, ] [package.metadata] @@ -2874,6 +2875,7 @@ requires-dist = [ { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-propagator-aws-xray", editable = "propagator/opentelemetry-propagator-aws-xray" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, + { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, ] provides-extras = ["instruments"]