Skip to content

Commit 9515f04

Browse files
aiohttp-server: add support for headers capture for request and response (open-telemetry#3916)
* Collect request attributes once * Doen't add request attributes to span if it is not recording While at it also avoid copying empty dictionaries * Add support for collecting custom request and response headers * update type annotations * Add changelog * Add PR number in changelog * Add documentation * Apply suggestions from code review Co-authored-by: Tammy Baylis <[email protected]> --------- Co-authored-by: Tammy Baylis <[email protected]>
1 parent fd5ddf0 commit 9515f04

File tree

4 files changed

+297
-17
lines changed

4 files changed

+297
-17
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
([#3885](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3885))
2626
- `opentelemetry-instrumentation-django`: improve readthedocs for sqlcommenter configuration.
2727
([#3884](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3884))
28+
- `opentelemetry-instrumentation-aiohttp-server`: add support for custom header captures via `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST` and `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`
29+
([#3916](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3916))
2830

2931
### Fixed
3032

instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py

Lines changed: 164 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,104 @@ async def hello(request):
5454
5555
will 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+
59153
import urllib
60154
from timeit import default_timer
61-
from typing import Dict, List, Tuple, Union
62155

63156
from aiohttp import web
64157
from multidict import CIMultiDictProxy
@@ -91,7 +184,17 @@ async def hello(request):
91184
)
92185
from opentelemetry.semconv.metrics import MetricInstruments
93186
from 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

148251
def _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+
206345
def 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:
225364
class 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

Comments
 (0)