@@ -54,11 +54,104 @@ async def hello(request):
5454
5555will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
5656
57+ Capture HTTP request and response headers
58+ *****************************************
59+ You can configure the agent to capture specified HTTP headers as span attributes, according to the
60+ `semantic conventions <https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-server-span>`_.
61+
62+ Request headers
63+ ***************
64+ To capture HTTP request headers as span attributes, set the environment variable
65+ ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names.
66+
67+ For example,
68+ ::
69+
70+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header"
71+
72+ will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes.
73+
74+ Request header names in aiohttp are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
75+ variable will capture the header named ``custom-header``.
76+
77+ Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
78+ ::
79+
80+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*"
81+
82+ Would match all request headers that start with ``Accept`` and ``X-``.
83+
84+ To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``.
85+ ::
86+
87+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*"
88+
89+ The name of the added span attribute will follow the format ``http.request.header.<header_name>`` where ``<header_name>``
90+ is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
91+ list containing the header values.
92+
93+ For example:
94+ ``http.request.header.custom_request_header = ["<value1>, <value2>"]``
95+
96+ Response headers
97+ ****************
98+ To capture HTTP response headers as span attributes, set the environment variable
99+ ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names.
100+
101+ For example,
102+ ::
103+
104+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header"
105+
106+ will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes.
107+
108+ Response header names in aiohttp are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
109+ variable will capture the header named ``custom-header``.
110+
111+ Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
112+ ::
113+
114+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*"
115+
116+ Would match all response headers that start with ``Content`` and ``X-``.
117+
118+ To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``.
119+ ::
120+
121+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*"
122+
123+ The name of the added span attribute will follow the format ``http.response.header.<header_name>`` where ``<header_name>``
124+ is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
125+ list containing the header values.
126+
127+ For example:
128+ ``http.response.header.custom_response_header = ["<value1>, <value2>"]``
129+
130+ Sanitizing headers
131+ ******************
132+ In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords,
133+ etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS``
134+ to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be
135+ matched in a case-insensitive manner.
136+
137+ For example,
138+ ::
139+
140+ export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie"
141+
142+ will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span.
143+
144+ Note:
145+ The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change.
146+
147+ API
148+ ---
57149"""
58150
151+ from __future__ import annotations
152+
59153import urllib
60154from timeit import default_timer
61- from typing import Dict , List , Tuple , Union
62155
63156from aiohttp import web
64157from multidict import CIMultiDictProxy
@@ -91,7 +184,17 @@ async def hello(request):
91184)
92185from opentelemetry .semconv .metrics import MetricInstruments
93186from opentelemetry .trace .status import Status , StatusCode
94- from opentelemetry .util .http import get_excluded_urls , redact_url
187+ from opentelemetry .util .http import (
188+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS ,
189+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST ,
190+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE ,
191+ SanitizeValue ,
192+ get_custom_headers ,
193+ get_excluded_urls ,
194+ normalise_request_header_name ,
195+ normalise_response_header_name ,
196+ redact_url ,
197+ )
95198
96199_duration_attrs = [
97200 HTTP_METHOD ,
@@ -134,15 +237,15 @@ def _parse_active_request_count_attrs(req_attrs):
134237 return active_requests_count_attrs
135238
136239
137- def get_default_span_details (request : web .Request ) -> Tuple [ str , dict ] :
240+ def get_default_span_name (request : web .Request ) -> str :
138241 """Default implementation for get_default_span_details
139242 Args:
140243 request: the request object itself.
141244 Returns:
142- a tuple of the span name, and any attributes to attach to the span .
245+ The span name.
143246 """
144247 span_name = request .path .strip () or f"HTTP { request .method } "
145- return span_name , {}
248+ return span_name
146249
147250
148251def _get_view_func (request : web .Request ) -> str :
@@ -158,7 +261,7 @@ def _get_view_func(request: web.Request) -> str:
158261 return "unknown"
159262
160263
161- def collect_request_attributes (request : web .Request ) -> Dict :
264+ def collect_request_attributes (request : web .Request ) -> dict :
162265 """Collects HTTP request attributes from the ASGI scope and returns a
163266 dictionary to be used as span creation attributes."""
164267
@@ -203,6 +306,42 @@ def collect_request_attributes(request: web.Request) -> Dict:
203306 return result
204307
205308
309+ def collect_request_headers_attributes (
310+ request : web .Request ,
311+ ) -> dict [str , list [str ]]:
312+ sanitize = SanitizeValue (
313+ get_custom_headers (
314+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
315+ )
316+ )
317+
318+ return sanitize .sanitize_header_values (
319+ request .headers ,
320+ get_custom_headers (
321+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
322+ ),
323+ normalise_request_header_name ,
324+ )
325+
326+
327+ def collect_response_headers_attributes (
328+ response : web .Response ,
329+ ) -> dict [str , list [str ]]:
330+ sanitize = SanitizeValue (
331+ get_custom_headers (
332+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
333+ )
334+ )
335+
336+ return sanitize .sanitize_header_values (
337+ response .headers ,
338+ get_custom_headers (
339+ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
340+ ),
341+ normalise_response_header_name ,
342+ )
343+
344+
206345def set_status_code (span , status_code : int ) -> None :
207346 """Adds HTTP response attributes to span using the status_code argument."""
208347
@@ -225,7 +364,7 @@ def set_status_code(span, status_code: int) -> None:
225364class AiohttpGetter (Getter ):
226365 """Extract current trace from headers"""
227366
228- def get (self , carrier , key : str ) -> Union [ List , None ] :
367+ def get (self , carrier , key : str ) -> list | None :
229368 """Getter implementation to retrieve an HTTP header value from the ASGI
230369 scope.
231370
@@ -241,7 +380,7 @@ def get(self, carrier, key: str) -> Union[List, None]:
241380 return None
242381 return headers .getall (key , None )
243382
244- def keys (self , carrier : Dict ) -> List :
383+ def keys (self , carrier : dict ) -> list :
245384 return list (carrier .keys ())
246385
247386
@@ -256,11 +395,13 @@ async def middleware(request, handler):
256395 ):
257396 return await handler (request )
258397
259- span_name , additional_attributes = get_default_span_details (request )
398+ span_name = get_default_span_name (request )
260399
261- req_attrs = collect_request_attributes (request )
262- duration_attrs = _parse_duration_attrs (req_attrs )
263- active_requests_count_attrs = _parse_active_request_count_attrs (req_attrs )
400+ request_attrs = collect_request_attributes (request )
401+ duration_attrs = _parse_duration_attrs (request_attrs )
402+ active_requests_count_attrs = _parse_active_request_count_attrs (
403+ request_attrs
404+ )
264405
265406 duration_histogram = meter .create_histogram (
266407 name = MetricInstruments .HTTP_SERVER_DURATION ,
@@ -279,14 +420,22 @@ async def middleware(request, handler):
279420 context = extract (request , getter = getter ),
280421 kind = trace .SpanKind .SERVER ,
281422 ) as span :
282- attributes = collect_request_attributes (request )
283- attributes .update (additional_attributes )
284- span .set_attributes (attributes )
423+ if span .is_recording ():
424+ request_headers_attributes = collect_request_headers_attributes (
425+ request
426+ )
427+ request_attrs .update (request_headers_attributes )
428+ span .set_attributes (request_attrs )
285429 start = default_timer ()
286430 active_requests_counter .add (1 , active_requests_count_attrs )
287431 try :
288432 resp = await handler (request )
289433 set_status_code (span , resp .status )
434+ if span .is_recording ():
435+ response_headers_attributes = (
436+ collect_response_headers_attributes (resp )
437+ )
438+ span .set_attributes (response_headers_attributes )
290439 except web .HTTPException as ex :
291440 set_status_code (span , ex .status_code )
292441 raise
0 commit comments