@@ -98,14 +98,105 @@ def response_hook(span: Span, params: typing.Union[
9898
9999will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
100100
101+ Capture HTTP request and response headers
102+ *****************************************
103+ You can configure the agent to capture specified HTTP headers as span attributes, according to the
104+ `semantic conventions <https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span>`_.
105+
106+ Request headers
107+ ***************
108+ To capture HTTP request headers as span attributes, set the environment variable
109+ ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to a comma delimited list of HTTP header names.
110+
111+ For example using the environment variable,
112+ ::
113+
114+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="content-type,custom_request_header"
115+
116+ will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes.
117+
118+ Request header names in aiohttp are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
119+ variable will capture the header named ``custom-header``.
120+
121+ Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
122+ ::
123+
124+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="Accept.*,X-.*"
125+
126+ Would match all request headers that start with ``Accept`` and ``X-``.
127+
128+ To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to ``".*"``.
129+ ::
130+
131+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST=".*"
132+
133+ The name of the added span attribute will follow the format ``http.request.header.<header_name>`` where ``<header_name>``
134+ is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
135+ single item list containing all the header values.
136+
137+ For example:
138+ ``http.request.header.custom_request_header = ["<value1>", "<value2>"]``
139+
140+ Response headers
141+ ****************
142+ To capture HTTP response headers as span attributes, set the environment variable
143+ ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to a comma delimited list of HTTP header names.
144+
145+ For example using the environment variable,
146+ ::
147+
148+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="content-type,custom_response_header"
149+
150+ will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes.
151+
152+ Response header names in aiohttp are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
153+ variable will capture the header named ``custom-header``.
154+
155+ Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
156+ ::
157+
158+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="Content.*,X-.*"
159+
160+ Would match all response headers that start with ``Content`` and ``X-``.
161+
162+ To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to ``".*"``.
163+ ::
164+
165+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE=".*"
166+
167+ The name of the added span attribute will follow the format ``http.response.header.<header_name>`` where ``<header_name>``
168+ is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
169+ list containing the header values.
170+
171+ For example:
172+ ``http.response.header.custom_response_header = ["<value1>", "<value2>"]``
173+
174+ Sanitizing headers
175+ ******************
176+ In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords,
177+ etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS``
178+ to a comma delimited list of HTTP header names to be sanitized.
179+
180+ Regexes may be used, and all header names will be matched in a case-insensitive manner.
181+
182+ For example using the environment variable,
183+ ::
184+
185+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie"
186+
187+ will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span.
188+
189+ Note:
190+ The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change.
191+
101192API
102193---
103194"""
104195
105196import types
106197import typing
107198from timeit import default_timer
108- from typing import Collection
199+ from typing import Callable , Collection , Mapping
109200from urllib .parse import urlparse
110201
111202import aiohttp
@@ -150,7 +241,14 @@ def response_hook(span: Span, params: typing.Union[
150241from opentelemetry .trace import Span , SpanKind , TracerProvider , get_tracer
151242from opentelemetry .trace .status import Status , StatusCode
152243from opentelemetry .util .http import (
244+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST ,
245+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE ,
246+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS ,
247+ SanitizeValue ,
248+ get_custom_headers ,
153249 get_excluded_urls ,
250+ normalise_request_header_name ,
251+ normalise_response_header_name ,
154252 redact_url ,
155253 sanitize_method ,
156254)
@@ -206,6 +304,35 @@ def _set_http_status_code_attribute(
206304 )
207305
208306
307+ def _get_custom_header_attributes (
308+ headers : Mapping [str , str | list [str ]] | None ,
309+ captured_headers : list [str ] | None ,
310+ sensitive_headers : list [str ] | None ,
311+ normalize_function : Callable [[str ], str ],
312+ ) -> dict [str , list [str ]]:
313+ """Extract and sanitize HTTP headers for span attributes.
314+
315+ Args:
316+ headers: The HTTP headers to process, either from a request or response.
317+ Can be None if no headers are available.
318+ captured_headers: List of header regexes to capture as span attributes.
319+ If None or empty, no headers will be captured.
320+ sensitive_headers: List of header regexes whose values should be sanitized
321+ (redacted). If None, no sanitization is applied.
322+ normalize_function: Function to normalize header names.
323+
324+ Returns:
325+ Dictionary of normalized header attribute names to their values
326+ as lists of strings.
327+ """
328+ if not headers or not captured_headers :
329+ return {}
330+ sanitize : SanitizeValue = SanitizeValue (sensitive_headers or ())
331+ return sanitize .sanitize_header_values (
332+ headers , captured_headers , normalize_function
333+ )
334+
335+
209336# pylint: disable=too-many-locals
210337# pylint: disable=too-many-statements
211338def create_trace_config (
@@ -215,6 +342,9 @@ def create_trace_config(
215342 tracer_provider : TracerProvider = None ,
216343 meter_provider : MeterProvider = None ,
217344 sem_conv_opt_in_mode : _StabilityMode = _StabilityMode .DEFAULT ,
345+ captured_request_headers : list [str ] | None = None ,
346+ captured_response_headers : list [str ] | None = None ,
347+ sensitive_headers : list [str ] | None = None ,
218348) -> aiohttp .TraceConfig :
219349 """Create an aiohttp-compatible trace configuration.
220350
@@ -243,6 +373,15 @@ def create_trace_config(
243373 :param Callable response_hook: Optional callback that can modify span name and response params.
244374 :param tracer_provider: optional TracerProvider from which to get a Tracer
245375 :param meter_provider: optional Meter provider to use
376+ :param captured_request_headers: List of HTTP request header regexes to capture as
377+ span attributes. Header names matching these patterns will be added as span
378+ attributes with the format ``http.request.header.<header_name>``.
379+ :param captured_response_headers: List of HTTP response header regexes to capture as
380+ span attributes. Header names matching these patterns will be added as span
381+ attributes with the format ``http.response.header.<header_name>``.
382+ :param sensitive_headers: List of HTTP header regexes whose values should be
383+ sanitized (redacted) when captured. Header values matching these patterns
384+ will be replaced with ``[REDACTED]``.
246385
247386 :return: An object suitable for use with :py:class:`aiohttp.ClientSession`.
248387 :rtype: :py:class:`aiohttp.TraceConfig`
@@ -385,6 +524,15 @@ async def on_request_start(
385524 except ValueError :
386525 pass
387526
527+ span_attributes .update (
528+ _get_custom_header_attributes (
529+ params .headers ,
530+ captured_request_headers ,
531+ sensitive_headers ,
532+ normalise_request_header_name ,
533+ )
534+ )
535+
388536 trace_config_ctx .span = trace_config_ctx .tracer .start_span (
389537 request_span_name , kind = SpanKind .CLIENT , attributes = span_attributes
390538 )
@@ -415,6 +563,15 @@ async def on_request_end(
415563 sem_conv_opt_in_mode ,
416564 )
417565
566+ trace_config_ctx .span .set_attributes (
567+ _get_custom_header_attributes (
568+ params .headers ,
569+ captured_response_headers ,
570+ sensitive_headers ,
571+ normalise_response_header_name ,
572+ )
573+ )
574+
418575 _end_trace (trace_config_ctx )
419576
420577 async def on_request_exception (
@@ -475,6 +632,9 @@ def _instrument(
475632 typing .Sequence [aiohttp .TraceConfig ]
476633 ] = None ,
477634 sem_conv_opt_in_mode : _StabilityMode = _StabilityMode .DEFAULT ,
635+ captured_request_headers : list [str ] | None = None ,
636+ captured_response_headers : list [str ] | None = None ,
637+ sensitive_headers : list [str ] | None = None ,
478638):
479639 """Enables tracing of all ClientSessions
480640
@@ -496,6 +656,9 @@ def instrumented_init(wrapped, instance, args, kwargs):
496656 tracer_provider = tracer_provider ,
497657 meter_provider = meter_provider ,
498658 sem_conv_opt_in_mode = sem_conv_opt_in_mode ,
659+ captured_request_headers = captured_request_headers ,
660+ captured_response_headers = captured_response_headers ,
661+ sensitive_headers = sensitive_headers ,
499662 )
500663 trace_config ._is_instrumented_by_opentelemetry = True
501664 client_trace_configs .append (trace_config )
@@ -560,6 +723,15 @@ def _instrument(self, **kwargs):
560723 response_hook = kwargs .get ("response_hook" ),
561724 trace_configs = kwargs .get ("trace_configs" ),
562725 sem_conv_opt_in_mode = _sem_conv_opt_in_mode ,
726+ captured_request_headers = get_custom_headers (
727+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST
728+ ),
729+ captured_response_headers = get_custom_headers (
730+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE
731+ ),
732+ sensitive_headers = get_custom_headers (
733+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
734+ ),
563735 )
564736
565737 def _uninstrument (self , ** kwargs ):
0 commit comments