Skip to content

Commit a00a3de

Browse files
committed
opentelemetry-instrumentation-aiohttp-client: add support to capture custom headers
1 parent 77170ea commit a00a3de

File tree

2 files changed

+179
-1
lines changed
  • instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client
  • util/opentelemetry-util-http/src/opentelemetry/util/http

2 files changed

+179
-1
lines changed

instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,105 @@ def response_hook(span: Span, params: typing.Union[
9898
9999
will 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+
101192
API
102193
---
103194
"""
104195

105196
import types
106197
import typing
107198
from timeit import default_timer
108-
from typing import Collection
199+
from typing import Callable, Collection, Mapping
109200
from urllib.parse import urlparse
110201

111202
import aiohttp
@@ -150,7 +241,14 @@ def response_hook(span: Span, params: typing.Union[
150241
from opentelemetry.trace import Span, SpanKind, TracerProvider, get_tracer
151242
from opentelemetry.trace.status import Status, StatusCode
152243
from 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
211338
def 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):

util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@
4848
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE = (
4949
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE"
5050
)
51+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST = (
52+
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST"
53+
)
54+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE = (
55+
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE"
56+
)
5157

5258
OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS = (
5359
"OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS"

0 commit comments

Comments
 (0)