Skip to content

Commit 1fb6de4

Browse files
author
Daniel Rogers
committed
Add support for regular expression matching and sanitizing of headers in Starlette.
1 parent 5f85a5b commit 1fb6de4

File tree

3 files changed

+152
-34
lines changed

3 files changed

+152
-34
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
([#1424](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1424))
1414
- `opentelemetry-instrumentation-fastapi` Add support for regular expression matching and sanitization of HTTP headers.
1515
([#1403](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1403))
16+
- `opentelemetry-instrumentation-starlette` Add support for regular expression matching and sanitization of HTTP headers.
17+
([#1404](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1404))
1618

1719
### Fixed
1820

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

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ def home(request):
3636
3737
Exclude lists
3838
*************
39-
To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_STARLETTE_EXCLUDED_URLS``
40-
(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude.
39+
To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_STARLETTE_EXCLUDED_URLS``
40+
(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the
41+
URLs.
4142
4243
For example,
4344
@@ -50,9 +51,14 @@ def home(request):
5051
Request/Response hooks
5152
**********************
5253
53-
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
54-
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.
55-
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.
54+
This instrumentation supports request and response hooks. These are functions that get called
55+
right after a span is created for a request and right before the span is finished for the response.
56+
57+
- The server request hook is passed a server span and ASGI scope object for every incoming request.
58+
- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called.
59+
- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called.
60+
61+
For example,
5662
5763
.. code-block:: python
5864
@@ -70,54 +76,93 @@ def client_response_hook(span: Span, message: dict):
7076
7177
Capture HTTP request and response headers
7278
*****************************************
73-
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>`_.
79+
You can configure the agent to capture specified HTTP headers as span attributes, according to the
80+
`semantic convention <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers>`_.
7481
7582
Request headers
7683
***************
77-
To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST``
78-
to a comma-separated list of HTTP header names.
84+
To capture HTTP request headers as span attributes, set the environment variable
85+
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names.
7986
8087
For example,
81-
8288
::
8389
8490
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header"
8591
86-
will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes.
92+
will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes.
93+
94+
Request header names in Starlette are case-insensitive. So, giving the header name as ``CUStom-Header`` in the
95+
environment variable will capture the header named ``custom-header``.
96+
97+
Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
98+
::
8799
88-
It is recommended that you should give the correct names of the headers to be captured in the environment variable.
89-
Request header names in starlette are case insensitive. So, giving header name as ``CUStom-Header`` in environment variable will be able capture header with name ``custom-header``.
100+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*"
90101
91-
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 _ ).
92-
The value of the attribute will be single item list containing all the header values.
102+
Would match all request headers that start with ``Accept`` and ``X-``.
93103
94-
Example of the added span attribute,
104+
Additionally, the special keyword ``all`` can be used to capture all request headers.
105+
::
106+
107+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="all"
108+
109+
The name of the added span attribute will follow the format ``http.request.header.<header_name>`` where ``<header_name>``
110+
is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
111+
single item list containing all the header values.
112+
113+
For example:
95114
``http.request.header.custom_request_header = ["<value1>,<value2>"]``
96115
97116
Response headers
98117
****************
99-
To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE``
100-
to a comma-separated list of HTTP header names.
118+
To capture HTTP response headers as span attributes, set the environment variable
119+
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names.
101120
102121
For example,
103-
104122
::
105123
106124
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header"
107125
108-
will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes.
126+
will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes.
109127
110-
It is recommended that you should give the correct names of the headers to be captured in the environment variable.
111-
Response header names captured in starlette are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``.
128+
Response header names in Starlette are case-insensitive. So, giving the header name as ``CUStom-Header`` in the
129+
environment variable will capture the header named ``custom-header``.
112130
113-
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 _ ).
114-
The value of the attribute will be single item list containing all the header values.
131+
Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
132+
::
115133
116-
Example of the added span attribute,
134+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*"
135+
136+
Would match all response headers that start with ``Content`` and ``X-``.
137+
138+
Additionally, the special keyword ``all`` can be used to capture all response headers.
139+
::
140+
141+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="all"
142+
143+
The name of the added span attribute will follow the format ``http.response.header.<header_name>`` where ``<header_name>``
144+
is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
145+
single item list containing all the header values.
146+
147+
For example:
117148
``http.response.header.custom_response_header = ["<value1>,<value2>"]``
118149
150+
Sanitizing headers
151+
******************
152+
In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords,
153+
etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS``
154+
to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be
155+
matched in a case-insensitive manner.
156+
157+
For example,
158+
::
159+
160+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie"
161+
162+
will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span.
163+
119164
Note:
120-
Environment variable names to capture http headers are still experimental, and thus are subject to change.
165+
The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change.
121166
122167
API
123168
---

instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
set_tracer_provider,
3939
)
4040
from opentelemetry.util.http import (
41+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
4142
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
4243
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
4344
_active_requests_count_attrs,
@@ -384,21 +385,12 @@ def create_app(self):
384385

385386
def setUp(self):
386387
super().setUp()
387-
self.env_patch = patch.dict(
388-
"os.environ",
389-
{
390-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
391-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
392-
},
393-
)
394-
self.env_patch.start()
395388
self._instrumentor = otel_starlette.StarletteInstrumentor()
396389
self._app = self.create_app()
397390
self._client = TestClient(self._app)
398391

399392
def tearDown(self) -> None:
400393
super().tearDown()
401-
self.env_patch.stop()
402394
with self.disable_logging():
403395
self._instrumentor.uninstrument()
404396

@@ -413,6 +405,9 @@ def _(request):
413405
headers={
414406
"custom-test-header-1": "test-header-value-1",
415407
"custom-test-header-2": "test-header-value-2",
408+
"my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2",
409+
"My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4",
410+
"my-secret-header": "my-secret-value",
416411
},
417412
)
418413

@@ -426,6 +421,15 @@ async def _(websocket: WebSocket) -> None:
426421
"headers": [
427422
(b"custom-test-header-1", b"test-header-value-1"),
428423
(b"custom-test-header-2", b"test-header-value-2"),
424+
(
425+
b"my-custom-regex-header-1",
426+
b"my-custom-regex-value-1,my-custom-regex-value-2",
427+
),
428+
(
429+
b"My-Custom-Regex-Header-2",
430+
b"my-custom-regex-value-3,my-custom-regex-value-4",
431+
),
432+
(b"my-secret-header", b"my-secret-value"),
429433
],
430434
}
431435
)
@@ -437,6 +441,14 @@ async def _(websocket: WebSocket) -> None:
437441
return app
438442

439443

444+
@patch.dict(
445+
"os.environ",
446+
{
447+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
448+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*",
449+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*",
450+
},
451+
)
440452
class TestHTTPAppWithCustomHeaders(TestBaseWithCustomHeaders):
441453
def test_custom_request_headers_in_span_attributes(self):
442454
expected = {
@@ -446,12 +458,20 @@ def test_custom_request_headers_in_span_attributes(self):
446458
"http.request.header.custom_test_header_2": (
447459
"test-header-value-2",
448460
),
461+
"http.request.header.regex_test_header_1": ("Regex Test Value 1",),
462+
"http.request.header.regex_test_header_2": (
463+
"RegexTestValue2,RegexTestValue3",
464+
),
465+
"http.request.header.my_secret_header": ("[REDACTED]",),
449466
}
450467
resp = self._client.get(
451468
"/foobar",
452469
headers={
453470
"custom-test-header-1": "test-header-value-1",
454471
"custom-test-header-2": "test-header-value-2",
472+
"Regex-Test-Header-1": "Regex Test Value 1",
473+
"regex-test-header-2": "RegexTestValue2,RegexTestValue3",
474+
"My-Secret-Header": "My Secret Value",
455475
},
456476
)
457477
self.assertEqual(200, resp.status_code)
@@ -464,6 +484,13 @@ def test_custom_request_headers_in_span_attributes(self):
464484

465485
self.assertSpanHasAttributes(server_span, expected)
466486

487+
@patch.dict(
488+
"os.environ",
489+
{
490+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
491+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*",
492+
},
493+
)
467494
def test_custom_request_headers_not_in_span_attributes(self):
468495
not_expected = {
469496
"http.request.header.custom_test_header_3": (
@@ -475,6 +502,9 @@ def test_custom_request_headers_not_in_span_attributes(self):
475502
headers={
476503
"custom-test-header-1": "test-header-value-1",
477504
"custom-test-header-2": "test-header-value-2",
505+
"Regex-Test-Header-1": "Regex Test Value 1",
506+
"regex-test-header-2": "RegexTestValue2,RegexTestValue3",
507+
"My-Secret-Header": "My Secret Value",
478508
},
479509
)
480510
self.assertEqual(200, resp.status_code)
@@ -496,6 +526,13 @@ def test_custom_response_headers_in_span_attributes(self):
496526
"http.response.header.custom_test_header_2": (
497527
"test-header-value-2",
498528
),
529+
"http.response.header.my_custom_regex_header_1": (
530+
"my-custom-regex-value-1,my-custom-regex-value-2",
531+
),
532+
"http.response.header.my_custom_regex_header_2": (
533+
"my-custom-regex-value-3,my-custom-regex-value-4",
534+
),
535+
"http.response.header.my_secret_header": ("[REDACTED]",),
499536
}
500537
resp = self._client.get("/foobar")
501538
self.assertEqual(200, resp.status_code)
@@ -527,6 +564,14 @@ def test_custom_response_headers_not_in_span_attributes(self):
527564
self.assertNotIn(key, server_span.attributes)
528565

529566

567+
@patch.dict(
568+
"os.environ",
569+
{
570+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
571+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*",
572+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*",
573+
},
574+
)
530575
class TestWebSocketAppWithCustomHeaders(TestBaseWithCustomHeaders):
531576
def test_custom_request_headers_in_span_attributes(self):
532577
expected = {
@@ -536,12 +581,20 @@ def test_custom_request_headers_in_span_attributes(self):
536581
"http.request.header.custom_test_header_2": (
537582
"test-header-value-2",
538583
),
584+
"http.request.header.regex_test_header_1": ("Regex Test Value 1",),
585+
"http.request.header.regex_test_header_2": (
586+
"RegexTestValue2,RegexTestValue3",
587+
),
588+
"http.request.header.my_secret_header": ("[REDACTED]",),
539589
}
540590
with self._client.websocket_connect(
541591
"/foobar_web",
542592
headers={
543593
"custom-test-header-1": "test-header-value-1",
544594
"custom-test-header-2": "test-header-value-2",
595+
"Regex-Test-Header-1": "Regex Test Value 1",
596+
"regex-test-header-2": "RegexTestValue2,RegexTestValue3",
597+
"My-Secret-Header": "My Secret Value",
545598
},
546599
) as websocket:
547600
data = websocket.receive_json()
@@ -566,6 +619,9 @@ def test_custom_request_headers_not_in_span_attributes(self):
566619
headers={
567620
"custom-test-header-1": "test-header-value-1",
568621
"custom-test-header-2": "test-header-value-2",
622+
"Regex-Test-Header-1": "Regex Test Value 1",
623+
"regex-test-header-2": "RegexTestValue2,RegexTestValue3",
624+
"My-Secret-Header": "My Secret Value",
569625
},
570626
) as websocket:
571627
data = websocket.receive_json()
@@ -589,6 +645,13 @@ def test_custom_response_headers_in_span_attributes(self):
589645
"http.response.header.custom_test_header_2": (
590646
"test-header-value-2",
591647
),
648+
"http.response.header.my_custom_regex_header_1": (
649+
"my-custom-regex-value-1,my-custom-regex-value-2",
650+
),
651+
"http.response.header.my_custom_regex_header_2": (
652+
"my-custom-regex-value-3,my-custom-regex-value-4",
653+
),
654+
"http.response.header.my_secret_header": ("[REDACTED]",),
592655
}
593656
with self._client.websocket_connect("/foobar_web") as websocket:
594657
data = websocket.receive_json()
@@ -624,6 +687,14 @@ def test_custom_response_headers_not_in_span_attributes(self):
624687
self.assertNotIn(key, server_span.attributes)
625688

626689

690+
@patch.dict(
691+
"os.environ",
692+
{
693+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
694+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*",
695+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*",
696+
},
697+
)
627698
class TestNonRecordingSpanWithCustomHeaders(TestBaseWithCustomHeaders):
628699
def setUp(self):
629700
super().setUp()

0 commit comments

Comments
 (0)