Skip to content

Commit 9d6ba63

Browse files
author
Dan Rogers
authored
Add support for regular expression matching and sanitizing of headers in ASGI. (#1333)
1 parent d5369a4 commit 9d6ba63

File tree

5 files changed

+483
-329
lines changed

5 files changed

+483
-329
lines changed

CHANGELOG.md

+12-6
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0-0.34b0...HEAD)
9-
- Add metric instrumentation for tornado
10-
([#1252](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1252))
11-
- Fix bug in Falcon instrumentation
12-
([#1377](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1377))
139

1410

1511
### Added
1612

13+
- Add metric instrumentation for tornado
14+
([#1252](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1252))
1715
- `opentelemetry-instrumentation-django` Fixed bug where auto-instrumentation fails when django is installed and settings are not configured.
1816
([#1369](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1369))
1917
- `opentelemetry-instrumentation-system-metrics` add supports to collect system thread count. ([#1339](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1339))
2018
- `opentelemetry-exporter-richconsole` Fixing RichConsoleExpoter to allow multiple traces, fixing duplicate spans and include resources ([#1336](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1336))
19+
- `opentelemetry-instrumentation-asgi` Add support for regular expression matching of HTTP headers.
20+
([#1333](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1333))
2121
- `opentelemetry-instrumentation-asgi` metrics record target attribute (FastAPI only)
2222
([#1323](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1323))
2323

24-
## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0-0.34b0) - 2022-09-26
24+
### Fixed
2525

26+
- Fix bug in Falcon instrumentation
27+
([#1377](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1377))
28+
- `opentelemetry-instrumentation-asgi` Fix keys() in class ASGIGetter so it decodes the keys before returning them.
29+
([#1333](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1333))
30+
- `opentelemetry-instrumentation-asgi` Make ASGIGetter.get() compare all keys in a case insensitive manner.
31+
([#1333](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1333))
2632

33+
## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0-0.34b0) - 2022-09-26
2734

2835
- `opentelemetry-instrumentation-asyncpg` Fix high cardinality in the span name
2936
([#1324](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1324))
@@ -44,7 +51,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4451
- Add metric instrumentation in starlette
4552
([#1327](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1327))
4653

47-
4854
### Fixed
4955

5056
- `opentelemetry-instrumentation-boto3sqs` Make propagation compatible with other SQS instrumentations, add 'messaging.url' span attribute, and fix missing package dependencies.

instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py

+108-48
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515

1616
"""
1717
The opentelemetry-instrumentation-asgi package provides an ASGI middleware that can be used
18-
on any ASGI framework (such as Django-channels / Quart) to track requests
19-
timing through OpenTelemetry.
18+
on any ASGI framework (such as Django-channels / Quart) to track request timing through OpenTelemetry.
2019
2120
Usage (Quart)
2221
-------------
@@ -71,9 +70,14 @@ async def hello():
7170
Request/Response hooks
7271
**********************
7372
74-
Utilize request/response hooks to execute custom logic to be performed before/after performing a request. The server request hook takes in a server span and ASGI
75-
scope object for every incoming request. The client request hook is called with the internal span and an ASGI scope which is sent as a dictionary for when the method receive is called.
76-
The client response hook is called with the internal span and an ASGI event which is sent as a dictionary for when the method send is called.
73+
This instrumentation supports request and response hooks. These are functions that get called
74+
right after a span is created for a request and right before the span is finished for the response.
75+
76+
- The server request hook is passed a server span and ASGI scope object for every incoming request.
77+
- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called.
78+
- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called.
79+
80+
For example,
7781
7882
.. code-block:: python
7983
@@ -93,54 +97,93 @@ def client_response_hook(span: Span, message: dict):
9397
9498
Capture HTTP request and response headers
9599
*****************************************
96-
You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers>`_.
100+
You can configure the agent to capture specified HTTP headers as span attributes, according to the
101+
`semantic convention <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers>`_.
97102
98103
Request headers
99104
***************
100-
To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST``
101-
to a comma-separated list of HTTP header names.
105+
To capture HTTP request headers as span attributes, set the environment variable
106+
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names.
102107
103108
For example,
104-
105109
::
106110
107111
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header"
108112
109-
will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes.
113+
will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes.
114+
115+
Request header names in ASGI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
116+
variable will capture the header named ``custom-header``.
117+
118+
Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
119+
::
120+
121+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*"
110122
111-
It is recommended that you should give the correct names of the headers to be captured in the environment variable.
112-
Request header names in ASGI are case insensitive. So, giving header name as ``CUStom-Header`` in environment variable will be able capture header with name ``custom-header``.
123+
Would match all request headers that start with ``Accept`` and ``X-``.
113124
114-
The name of the added span attribute will follow the format ``http.request.header.<header_name>`` where ``<header_name>`` being the normalized HTTP header name (lowercase, with - characters replaced by _ ).
115-
The value of the attribute will be single item list containing all the header values.
125+
To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``.
126+
::
127+
128+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*"
116129
117-
Example of the added span attribute,
130+
The name of the added span attribute will follow the format ``http.request.header.<header_name>`` where ``<header_name>``
131+
is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
132+
single item list containing all the header values.
133+
134+
For example:
118135
``http.request.header.custom_request_header = ["<value1>,<value2>"]``
119136
120137
Response headers
121138
****************
122-
To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE``
123-
to a comma-separated list of HTTP header names.
139+
To capture HTTP response headers as span attributes, set the environment variable
140+
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names.
124141
125142
For example,
126-
127143
::
128144
129145
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header"
130146
131-
will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes.
147+
will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes.
148+
149+
Response header names in ASGI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
150+
variable will capture the header named ``custom-header``.
151+
152+
Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
153+
::
154+
155+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*"
132156
133-
It is recommended that you should give the correct names of the headers to be captured in the environment variable.
134-
Response header names captured in ASGI are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``.
157+
Would match all response headers that start with ``Content`` and ``X-``.
135158
136-
The name of the added span attribute will follow the format ``http.response.header.<header_name>`` where ``<header_name>`` being the normalized HTTP header name (lowercase, with - characters replaced by _ ).
137-
The value of the attribute will be single item list containing all the header values.
159+
To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``.
160+
::
161+
162+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*"
138163
139-
Example of the added span attribute,
164+
The name of the added span attribute will follow the format ``http.response.header.<header_name>`` where ``<header_name>``
165+
is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
166+
single item list containing all the header values.
167+
168+
For example:
140169
``http.response.header.custom_response_header = ["<value1>,<value2>"]``
141170
171+
Sanitizing headers
172+
******************
173+
In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords,
174+
etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS``
175+
to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be
176+
matched in a case-insensitive manner.
177+
178+
For example,
179+
::
180+
181+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie"
182+
183+
will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span.
184+
142185
Note:
143-
Environment variable names to capture http headers are still experimental, and thus are subject to change.
186+
The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change.
144187
145188
API
146189
---
@@ -170,8 +213,10 @@ def client_response_hook(span: Span, message: dict):
170213
from opentelemetry.trace import Span, set_span_in_context
171214
from opentelemetry.trace.status import Status, StatusCode
172215
from opentelemetry.util.http import (
216+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
173217
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
174218
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
219+
SanitizeValue,
175220
_parse_active_request_count_attrs,
176221
_parse_duration_attrs,
177222
get_custom_headers,
@@ -203,19 +248,19 @@ def get(
203248
if not headers:
204249
return None
205250

206-
# asgi header keys are in lower case
251+
# ASGI header keys are in lower case
207252
key = key.lower()
208253
decoded = [
209254
_value.decode("utf8")
210255
for (_key, _value) in headers
211-
if _key.decode("utf8") == key
256+
if _key.decode("utf8").lower() == key
212257
]
213258
if not decoded:
214259
return None
215260
return decoded
216261

217262
def keys(self, carrier: dict) -> typing.List[str]:
218-
return list(carrier.keys())
263+
return [_key.decode("utf8") for (_key, _value) in carrier]
219264

220265

221266
asgi_getter = ASGIGetter()
@@ -290,35 +335,50 @@ def collect_custom_request_headers_attributes(scope):
290335
"""returns custom HTTP request headers to be added into SERVER span as span attributes
291336
Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers"""
292337

293-
attributes = {}
294-
custom_request_headers = get_custom_headers(
295-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
338+
sanitize = SanitizeValue(
339+
get_custom_headers(
340+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
341+
)
296342
)
297343

298-
for header in custom_request_headers:
299-
values = asgi_getter.get(scope, header)
300-
if values:
301-
key = normalise_request_header_name(header)
302-
attributes.setdefault(key, []).extend(values)
344+
# Decode headers before processing.
345+
headers = {
346+
_key.decode("utf8"): _value.decode("utf8")
347+
for (_key, _value) in scope.get("headers")
348+
}
303349

304-
return attributes
350+
return sanitize.sanitize_header_values(
351+
headers,
352+
get_custom_headers(
353+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
354+
),
355+
normalise_request_header_name,
356+
)
305357

306358

307359
def collect_custom_response_headers_attributes(message):
308360
"""returns custom HTTP response headers to be added into SERVER span as span attributes
309361
Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers"""
310-
attributes = {}
311-
custom_response_headers = get_custom_headers(
312-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
362+
363+
sanitize = SanitizeValue(
364+
get_custom_headers(
365+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
366+
)
313367
)
314368

315-
for header in custom_response_headers:
316-
values = asgi_getter.get(message, header)
317-
if values:
318-
key = normalise_response_header_name(header)
319-
attributes.setdefault(key, []).extend(values)
369+
# Decode headers before processing.
370+
headers = {
371+
_key.decode("utf8"): _value.decode("utf8")
372+
for (_key, _value) in message.get("headers")
373+
}
320374

321-
return attributes
375+
return sanitize.sanitize_header_values(
376+
headers,
377+
get_custom_headers(
378+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
379+
),
380+
normalise_response_header_name,
381+
)
322382

323383

324384
def get_host_port_url_tuple(scope):
@@ -354,7 +414,7 @@ def set_status_code(span, status_code):
354414
def get_default_span_details(scope: dict) -> Tuple[str, dict]:
355415
"""Default implementation for get_default_span_details
356416
Args:
357-
scope: the asgi scope dictionary
417+
scope: the ASGI scope dictionary
358418
Returns:
359419
a tuple of the span name, and any attributes to attach to the span.
360420
"""
@@ -455,7 +515,7 @@ async def __call__(self, scope, receive, send):
455515
"""The ASGI application
456516
457517
Args:
458-
scope: A ASGI environment.
518+
scope: An ASGI environment.
459519
receive: An awaitable callable yielding dictionaries
460520
send: An awaitable callable taking a single dictionary as argument.
461521
"""

0 commit comments

Comments
 (0)