diff --git a/CHANGELOG.md b/CHANGELOG.md index 4996f9b9f3..80462f2b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Capture common HTTP attributes from API Gateway proxy events in `opentelemetry-instrumentation-aws-lambda` + ([#1233](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1233)) - Add metric instrumentation for tornado ([#1252](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1252)) - `opentelemetry-instrumentation-django` Fixed bug where auto-instrumentation fails when django is installed and settings are not configured. @@ -29,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1402](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1402)) - Add support for py3.11 ([#1415](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1415)) +- `opentelemetry-instrumentation-django` Add support for regular expression matching and sanitization of HTTP headers. + ([#1411](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1411)) - `opentelemetry-instrumentation-falcon` Add support for regular expression matching and sanitization of HTTP headers. ([#1412](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1412)) - `opentelemetry-instrumentation-flask` Add support for regular expression matching and sanitization of HTTP headers. @@ -58,7 +62,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `opentelemetry-instrumentation-grpc` add supports to filter requests to instrument. ([#1241](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1241)) +- `opentelemetry-instrumentation-grpc` add supports to filter requests to instrument. + ([#1241](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1241)) - Flask sqlalchemy psycopg2 integration ([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224)) - Add metric instrumentation in Falcon 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 8467422fb9..115709bc83 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 @@ -64,11 +64,11 @@ def custom_event_context_extractor(lambda_event): event_context_extractor=custom_event_context_extractor ) """ - import logging import os from importlib import import_module from typing import Any, Callable, Collection +from urllib.parse import urlencode from wrapt import wrap_function_wrapper @@ -85,6 +85,7 @@ def custom_event_context_extractor(lambda_event): from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import ( + Span, SpanKind, TracerProvider, get_tracer, @@ -171,6 +172,86 @@ def _determine_parent_context( return parent_context +def _set_api_gateway_v1_proxy_attributes( + lambda_event: Any, span: Span +) -> 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( + SpanAttributes.HTTP_METHOD, lambda_event.get("httpMethod") + ) + span.set_attribute(SpanAttributes.HTTP_ROUTE, lambda_event.get("resource")) + + if lambda_event.get("headers"): + span.set_attribute( + SpanAttributes.HTTP_USER_AGENT, + lambda_event["headers"].get("User-Agent"), + ) + span.set_attribute( + SpanAttributes.HTTP_SCHEME, + lambda_event["headers"].get("X-Forwarded-Proto"), + ) + span.set_attribute( + SpanAttributes.NET_HOST_NAME, lambda_event["headers"].get("Host") + ) + + if lambda_event.get("queryStringParameters"): + span.set_attribute( + SpanAttributes.HTTP_TARGET, + f"{lambda_event.get('resource')}?{urlencode(lambda_event.get('queryStringParameters'))}", + ) + else: + span.set_attribute( + SpanAttributes.HTTP_TARGET, lambda_event.get("resource") + ) + + return span + + +def _set_api_gateway_v2_proxy_attributes( + lambda_event: Any, span: Span +) -> 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.set_attribute( + SpanAttributes.NET_HOST_NAME, + lambda_event["requestContext"].get("domainName"), + ) + + if lambda_event["requestContext"].get("http"): + span.set_attribute( + SpanAttributes.HTTP_METHOD, + lambda_event["requestContext"]["http"].get("method"), + ) + span.set_attribute( + SpanAttributes.HTTP_USER_AGENT, + lambda_event["requestContext"]["http"].get("userAgent"), + ) + span.set_attribute( + SpanAttributes.HTTP_ROUTE, + lambda_event["requestContext"]["http"].get("path"), + ) + + if lambda_event.get("rawQueryString"): + span.set_attribute( + SpanAttributes.HTTP_TARGET, + f"{lambda_event['requestContext']['http'].get('path')}?{lambda_event.get('rawQueryString')}", + ) + else: + span.set_attribute( + SpanAttributes.HTTP_TARGET, + lambda_event["requestContext"]["http"].get("path"), + ) + + return span + + def _instrument( wrapped_module_name, wrapped_function_name, @@ -233,6 +314,23 @@ def _instrumented_lambda_handler_call( result = call_wrapped(*args, **kwargs) + # If the request came from an API Gateway, extract http attributes from the event + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#api-gateway + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-server-semantic-conventions + if lambda_event and lambda_event.get("requestContext"): + span.set_attribute(SpanAttributes.FAAS_TRIGGER, "http") + + if lambda_event.get("version") == "2.0": + _set_api_gateway_v2_proxy_attributes(lambda_event, span) + else: + _set_api_gateway_v1_proxy_attributes(lambda_event, span) + + if isinstance(result, dict) and result.get("statusCode"): + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, + result.get("statusCode"), + ) + _tracer_provider = tracer_provider or get_tracer_provider() try: # NOTE: `force_flush` before function quit in case of Lambda freeze. diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_http_api_event.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_http_api_event.py new file mode 100644 index 0000000000..77454a6bb5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_http_api_event.py @@ -0,0 +1,54 @@ +# Generated via `sam local generate-event apigateway http-api-proxy` + +MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT = { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/path/to/resource", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": ["cookie1", "cookie2"], + "headers": {"header1": "value1", "Header2": "value1,value2"}, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value", + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT", + }, + } + }, + "authorizer": { + "jwt": { + "claims": {"claim1": "value1", "claim2": "value2"}, + "scopes": ["scope1", "scope2"], + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/path/to/resource", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1/32", + "userAgent": "agent", + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390, + }, + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "pathParameters": {"parameter1": "value1"}, + "isBase64Encoded": True, + "stageVariables": {"stageVariable1": "value1", "stageVariable2": "value2"}, +} diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_proxy_event.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_proxy_event.py new file mode 100644 index 0000000000..f812dbbca2 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_proxy_event.py @@ -0,0 +1,85 @@ +# Generated via `sam local generate-event apigateway aws-proxy` + +MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT = { + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": True, + "queryStringParameters": {"foo": "bar"}, + "multiValueQueryStringParameters": {"foo": ["bar"]}, + "pathParameters": {"proxy": "/path/to/resource"}, + "stageVariables": {"baz": "qux"}, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https", + }, + "multiValueHeaders": { + "Accept": [ + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" + ], + "Accept-Encoding": ["gzip, deflate, sdch"], + "Accept-Language": ["en-US,en;q=0.8"], + "Cache-Control": ["max-age=0"], + "CloudFront-Forwarded-Proto": ["https"], + "CloudFront-Is-Desktop-Viewer": ["true"], + "CloudFront-Is-Mobile-Viewer": ["false"], + "CloudFront-Is-SmartTV-Viewer": ["false"], + "CloudFront-Is-Tablet-Viewer": ["false"], + "CloudFront-Viewer-Country": ["US"], + "Host": ["0123456789.execute-api.us-east-1.amazonaws.com"], + "Upgrade-Insecure-Requests": ["1"], + "User-Agent": ["Custom User Agent String"], + "Via": [ + "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" + ], + "X-Forwarded-For": ["127.0.0.1, 127.0.0.2"], + "X-Forwarded-Port": ["443"], + "X-Forwarded-Proto": ["https"], + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": None, + "accountId": None, + "cognitoIdentityId": None, + "caller": None, + "accessKey": None, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": None, + "cognitoAuthenticationProvider": None, + "userArn": None, + "userAgent": "Custom User Agent String", + "user": None, + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1", + }, +} diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py index c292575651..259375c481 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py @@ -15,3 +15,7 @@ def handler(event, context): return "200 ok" + + +def rest_api_handler(event, context): + return {"statusCode": 200, "body": "200 ok"} 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 e463a09b47..496829fe4e 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 @@ -15,6 +15,11 @@ from importlib import import_module from unittest import mock +from mocks.api_gateway_http_api_event import ( + MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT, +) +from mocks.api_gateway_proxy_event import MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT + from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.instrumentation.aws_lambda import ( _HANDLER, @@ -300,3 +305,49 @@ def test_lambda_handles_multiple_consumers(self): assert spans test_env_patch.stop() + + def test_api_gateway_proxy_event_sets_attributes(self): + handler_patch = mock.patch.dict( + "os.environ", + {_HANDLER: "mocks.lambda_function.rest_api_handler"}, + ) + handler_patch.start() + + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT) + + span = self.memory_exporter.get_finished_spans()[0] + + self.assertSpanHasAttributes( + span, + { + SpanAttributes.FAAS_TRIGGER: "http", + SpanAttributes.HTTP_METHOD: "POST", + SpanAttributes.HTTP_ROUTE: "/{proxy+}", + SpanAttributes.HTTP_TARGET: "/{proxy+}?foo=bar", + SpanAttributes.NET_HOST_NAME: "1234567890.execute-api.us-east-1.amazonaws.com", + SpanAttributes.HTTP_USER_AGENT: "Custom User Agent String", + SpanAttributes.HTTP_SCHEME: "https", + SpanAttributes.HTTP_STATUS_CODE: 200, + }, + ) + + def test_api_gateway_http_api_proxy_event_sets_attributes(self): + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT) + + span = self.memory_exporter.get_finished_spans()[0] + + self.assertSpanHasAttributes( + span, + { + SpanAttributes.FAAS_TRIGGER: "http", + SpanAttributes.HTTP_METHOD: "POST", + SpanAttributes.HTTP_ROUTE: "/path/to/resource", + SpanAttributes.HTTP_TARGET: "/path/to/resource?parameter1=value1¶meter1=value2¶meter2=value", + SpanAttributes.NET_HOST_NAME: "id.execute-api.us-east-1.amazonaws.com", + SpanAttributes.HTTP_USER_AGENT: "agent", + }, + ) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index 7fdbc92eb8..ea2ba8598b 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -94,8 +94,9 @@ Exclude lists ************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_DJANGO_EXCLUDED_URLS`` -(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. +To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_DJANGO_EXCLUDED_URLS`` +(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the +URLs. For example, @@ -107,8 +108,8 @@ Request attributes ******************** -To extract certain attributes from Django's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma -delimited list of request attribute names. +To extract attributes from Django's request object and use them as span attributes, set the environment variable +``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma delimited list of request attribute names. For example, @@ -116,14 +117,15 @@ export OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS='path_info,content_type' -will extract path_info and content_type attributes from every traced request and add them as span attritbues. +will extract the ``path_info`` and ``content_type`` attributes from every traced request and add them as span attributes. Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes Request and Response hooks *************************** -The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a Span is created for a request -and right before the span is finished while processing a response. The hooks can be configured as follows: +This instrumentation supports request and response hooks. These are functions that get called +right after a span is created for a request and right before the span is finished for the response. +The hooks can be configured as follows: .. code:: python @@ -140,50 +142,94 @@ def response_hook(span, request, response): Capture HTTP request and response headers ***************************************** -You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. +You can configure the agent to capture specified HTTP headers as span attributes, according to the +`semantic convention `_. Request headers *************** -To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` -to a comma-separated list of HTTP header names. +To capture HTTP request headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, :: - export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content_type,custom_request_header" + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" -will extract content_type and custom_request_header from request headers and add them as span attributes. +will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Request header names in django are case insensitive. So, giving header name as ``CUStom_Header`` in environment variable will be able capture header with name ``custom-header``. +Request header names in Django are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. -The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" + +Would match all request headers that start with ``Accept`` and ``X-``. + +To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" -Example of the added span attribute, +The name of the added span attribute will follow the format ``http.request.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** -To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` -to a comma-separated list of HTTP header names. +To capture HTTP response headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, :: - export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content_type,custom_response_header" + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" + +will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. -will extract content_type and custom_response_header from response headers and add them as span attributes. +Response header names in Django are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Response header names captured in django are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: -The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" -Example of the added span attribute, +Would match all response headers that start with ``Content`` and ``X-``. + +To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" + +The name of the added span attribute will follow the format ``http.response.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: ``http.response.header.custom_response_header = [","]`` +Sanitizing headers +****************** +In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, +etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` +to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be +matched in a case-insensitive manner. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" + +will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. + +Note: + The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. + API --- diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 8b584bfc3b..3845c55dcb 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -48,6 +48,7 @@ format_trace_id, ) from opentelemetry.util.http import ( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, _active_requests_count_attrs, @@ -530,6 +531,14 @@ def test_django_with_wsgi_instrumented(self): ) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestMiddlewareWsgiWithCustomHeaders(WsgiTestBase): @classmethod def setUpClass(cls): @@ -542,18 +551,9 @@ def setUp(self): tracer_provider, exporter = self.create_tracer_provider() self.exporter = exporter _django_instrumentor.instrument(tracer_provider=tracer_provider) - self.env_patch = patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - }, - ) - self.env_patch.start() def tearDown(self): super().tearDown() - self.env_patch.stop() teardown_test_environment() _django_instrumentor.uninstrument() @@ -570,10 +570,18 @@ def test_http_custom_request_headers_in_span_attributes(self): "http.request.header.custom_test_header_2": ( "test-header-value-2", ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } Client( HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1", HTTP_CUSTOM_TEST_HEADER_2="test-header-value-2", + HTTP_REGEX_TEST_HEADER_1="Regex Test Value 1", + HTTP_REGEX_TEST_HEADER_2="RegexTestValue2,RegexTestValue3", + HTTP_MY_SECRET_HEADER="My Secret Value", ).get("/traced/") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) @@ -607,6 +615,13 @@ def test_http_custom_response_headers_in_span_attributes(self): "http.response.header.custom_test_header_2": ( "test-header-value-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } Client().get("/traced_custom_header/") spans = self.exporter.get_finished_spans() diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py index 941fda49bb..784f8e24ec 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py @@ -43,6 +43,7 @@ format_trace_id, ) from opentelemetry.util.http import ( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, get_excluded_urls, @@ -424,6 +425,14 @@ async def test_tracer_provider_traced(self): ) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestMiddlewareAsgiWithCustomHeaders(SimpleTestCase, TestBase): @classmethod def setUpClass(cls): @@ -437,18 +446,9 @@ def setUp(self): tracer_provider, exporter = self.create_tracer_provider() self.exporter = exporter _django_instrumentor.instrument(tracer_provider=tracer_provider) - self.env_patch = patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - }, - ) - self.env_patch.start() def tearDown(self): super().tearDown() - self.env_patch.stop() teardown_test_environment() _django_instrumentor.uninstrument() @@ -465,12 +465,20 @@ async def test_http_custom_request_headers_in_span_attributes(self): "http.request.header.custom_test_header_2": ( "test-header-value-2", ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } await self.async_client.get( "/traced/", **{ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", }, ) spans = self.exporter.get_finished_spans() @@ -510,6 +518,13 @@ async def test_http_custom_response_headers_in_span_attributes(self): "http.response.header.custom_test_header_2": ( "test-header-value-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } await self.async_client.get("/traced_custom_header/") spans = self.exporter.get_finished_spans() diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index f97933cfd8..452a7c0fdd 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -35,6 +35,13 @@ def response_with_custom_header(request): response = HttpResponse() response["custom-test-header-1"] = "test-header-value-1" response["custom-test-header-2"] = "test-header-value-2" + response[ + "my-custom-regex-header-1" + ] = "my-custom-regex-value-1,my-custom-regex-value-2" + response[ + "my-custom-regex-header-2" + ] = "my-custom-regex-value-3,my-custom-regex-value-4" + response["my-secret-header"] = "my-secret-value" return response