Skip to content

Commit efee7a0

Browse files
Dan Rogerssrikanthccv
authored and
CircleCI
committed
Add support for regular expression matching and sanitizing of headers in Flask. (open-telemetry#1413)
Co-authored-by: Srikanth Chekuri <[email protected]>
1 parent 9ee61bf commit efee7a0

File tree

5 files changed

+128
-78
lines changed

5 files changed

+128
-78
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
([#1323](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1323))
2828
- `opentelemetry-instrumentation-wsgi` Add support for regular expression matching and sanitization of HTTP headers.
2929
([#1402](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1402))
30+
- `opentelemetry-instrumentation-flask` Add support for regular expression matching and sanitization of HTTP headers.
31+
([#1413](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1413))
3032
- `opentelemetry-instrumentation-pyramid` Add support for regular expression matching and sanitization of HTTP headers.
3133
([#1414](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1414))
3234

instrumentation/opentelemetry-instrumentation-flask/README.rst

-42
Original file line numberDiff line numberDiff line change
@@ -16,48 +16,6 @@ Installation
1616

1717
pip install opentelemetry-instrumentation-flask
1818

19-
Configuration
20-
-------------
21-
22-
Exclude lists
23-
*************
24-
To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FLASK_EXCLUDED_URLS``
25-
(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude.
26-
27-
For example,
28-
29-
::
30-
31-
export OTEL_PYTHON_FLASK_EXCLUDED_URLS="client/.*/info,healthcheck"
32-
33-
will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
34-
35-
You can also pass the comma delimited regexes to the ``instrument_app`` method directly:
36-
37-
.. code-block:: python
38-
39-
FlaskInstrumentor().instrument_app(app, excluded_urls="client/.*/info,healthcheck")
40-
41-
Request/Response hooks
42-
**********************
43-
44-
Utilize request/response hooks to execute custom logic to be performed before/after performing a request. Environ is an instance of WSGIEnvironment (flask.request.environ).
45-
Response_headers is a list of key-value (tuples) representing the response headers returned from the response.
46-
47-
.. code-block:: python
48-
49-
def request_hook(span: Span, environ: WSGIEnvironment):
50-
if span and span.is_recording():
51-
span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
52-
53-
def response_hook(span: Span, status: str, response_headers: List):
54-
if span and span.is_recording():
55-
span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
56-
57-
FlaskInstrumentation().instrument(request_hook=request_hook, response_hook=response_hook)
58-
59-
Flask Request object reference: https://flask.palletsprojects.com/en/2.0.x/api/#flask.Request
60-
6119
References
6220
----------
6321

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

+73-26
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,9 @@ def hello():
9595
9696
Exclude lists
9797
*************
98-
To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FLASK_EXCLUDED_URLS``
99-
(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude.
98+
To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_FLASK_EXCLUDED_URLS``
99+
(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the
100+
URLs.
100101
101102
For example,
102103
@@ -106,7 +107,7 @@ def hello():
106107
107108
will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
108109
109-
You can also pass the comma delimited regexes to the ``instrument_app`` method directly:
110+
You can also pass comma delimited regexes directly to the ``instrument_app`` method:
110111
111112
.. code-block:: python
112113
@@ -115,8 +116,15 @@ def hello():
115116
Request/Response hooks
116117
**********************
117118
118-
Utilize request/response hooks to execute custom logic to be performed before/after performing a request. Environ is an instance of WSGIEnvironment (flask.request.environ).
119-
Response_headers is a list of key-value (tuples) representing the response headers returned from the response.
119+
This instrumentation supports request and response hooks. These are functions that get called
120+
right after a span is created for a request and right before the span is finished for the response.
121+
122+
- The client request hook is called with the internal span and an instance of WSGIEnvironment (flask.request.environ)
123+
when the method ``receive`` is called.
124+
- The client response hook is called with the internal span, the status of the response and a list of key-value (tuples)
125+
representing the response headers returned from the response when the method ``send`` is called.
126+
127+
For example,
120128
121129
.. code-block:: python
122130
@@ -130,58 +138,97 @@ def response_hook(span: Span, status: str, response_headers: List):
130138
131139
FlaskInstrumentation().instrument(request_hook=request_hook, response_hook=response_hook)
132140
133-
Flask Request object reference: https://flask.palletsprojects.com/en/2.0.x/api/#flask.Request
141+
Flask Request object reference: https://flask.palletsprojects.com/en/2.1.x/api/#flask.Request
134142
135143
Capture HTTP request and response headers
136144
*****************************************
137-
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>`_.
145+
You can configure the agent to capture specified HTTP headers as span attributes, according to the
146+
`semantic convention <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers>`_.
138147
139148
Request headers
140149
***************
141-
To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST``
142-
to a comma-separated list of HTTP header names.
150+
To capture HTTP request headers as span attributes, set the environment variable
151+
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names.
143152
144153
For example,
145-
146154
::
147155
148156
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header"
149157
150-
will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes.
158+
will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes.
159+
160+
Request header names in Flask are case-insensitive and ``-`` characters are replaced by ``_``. So, giving the header
161+
name as ``CUStom_Header`` in the environment variable will capture the header named ``custom-header``.
162+
163+
Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
164+
::
151165
152-
It is recommended that you should give the correct names of the headers to be captured in the environment variable.
153-
Request header names in flask are case insensitive and - characters are replaced by _. So, giving header name as ``CUStom_Header`` in environment variable will be able capture header with name ``custom-header``.
166+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*"
154167
155-
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 _ ).
156-
The value of the attribute will be single item list containing all the header values.
168+
Would match all request headers that start with ``Accept`` and ``X-``.
157169
158-
Example of the added span attribute,
170+
To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``.
171+
::
172+
173+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*"
174+
175+
The name of the added span attribute will follow the format ``http.request.header.<header_name>`` where ``<header_name>``
176+
is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
177+
single item list containing all the header values.
178+
179+
For example:
159180
``http.request.header.custom_request_header = ["<value1>,<value2>"]``
160181
161182
Response headers
162183
****************
163-
To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE``
164-
to a comma-separated list of HTTP header names.
184+
To capture HTTP response headers as span attributes, set the environment variable
185+
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names.
165186
166187
For example,
167-
168188
::
169189
170190
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header"
171191
172-
will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes.
192+
will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes.
173193
174-
It is recommended that you should give the correct names of the headers to be captured in the environment variable.
175-
Response header names captured in flask are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``.
194+
Response header names in Flask are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
195+
variable will capture the header named ``custom-header``.
176196
177-
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 _ ).
178-
The value of the attribute will be single item list containing all the header values.
197+
Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
198+
::
179199
180-
Example of the added span attribute,
200+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*"
201+
202+
Would match all response headers that start with ``Content`` and ``X-``.
203+
204+
To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``.
205+
::
206+
207+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*"
208+
209+
The name of the added span attribute will follow the format ``http.response.header.<header_name>`` where ``<header_name>``
210+
is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
211+
single item list containing all the header values.
212+
213+
For example:
181214
``http.response.header.custom_response_header = ["<value1>,<value2>"]``
182215
216+
Sanitizing headers
217+
******************
218+
In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords,
219+
etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS``
220+
to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be
221+
matched in a case-insensitive manner.
222+
223+
For example,
224+
::
225+
226+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie"
227+
228+
will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span.
229+
183230
Note:
184-
Environment variable names to capture http headers are still experimental, and thus are subject to change.
231+
The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change.
185232
186233
API
187234
---

instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py

+7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ def _custom_response_headers():
4242
resp.headers[
4343
"my-custom-header"
4444
] = "my-custom-value-1,my-custom-header-2"
45+
resp.headers[
46+
"my-custom-regex-header-1"
47+
] = "my-custom-regex-value-1,my-custom-regex-value-2"
48+
resp.headers[
49+
"My-Custom-Regex-Header-2"
50+
] = "my-custom-regex-value-3,my-custom-regex-value-4"
51+
resp.headers["my-secret-header"] = "my-secret-value"
4552
return resp
4653

4754
def _common_initialization(self):

instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py

+46-10
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@
3636
from opentelemetry.sdk.resources import Resource
3737
from opentelemetry.semconv.trace import SpanAttributes
3838
from opentelemetry.test.wsgitestutil import WsgiTestBase
39-
from opentelemetry.util.http import get_excluded_urls
39+
from opentelemetry.util.http import (
40+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
41+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
42+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
43+
get_excluded_urls,
44+
)
4045

4146
# pylint: disable=import-error
4247
from .base_test import InstrumentationTest
@@ -558,33 +563,35 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self):
558563
)
559564

560565

566+
@patch.dict(
567+
"os.environ",
568+
{
569+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
570+
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.*",
571+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*",
572+
},
573+
)
561574
class TestCustomRequestResponseHeaders(InstrumentationTest, WsgiTestBase):
562575
def setUp(self):
563576
super().setUp()
564577

565-
self.env_patch = patch.dict(
566-
"os.environ",
567-
{
568-
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST": "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
569-
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE": "content-type,content-length,my-custom-header,invalid-header",
570-
},
571-
)
572-
self.env_patch.start()
573578
self.app = Flask(__name__)
574579
FlaskInstrumentor().instrument_app(self.app)
575580

576581
self._common_initialization()
577582

578583
def tearDown(self):
579584
super().tearDown()
580-
self.env_patch.stop()
581585
with self.disable_logging():
582586
FlaskInstrumentor().uninstrument_app(self.app)
583587

584588
def test_custom_request_header_added_in_server_span(self):
585589
headers = {
586590
"Custom-Test-Header-1": "Test Value 1",
587591
"Custom-Test-Header-2": "TestValue2,TestValue3",
592+
"Regex-Test-Header-1": "Regex Test Value 1",
593+
"regex-test-header-2": "RegexTestValue2,RegexTestValue3",
594+
"My-Secret-Header": "My Secret Value",
588595
}
589596
resp = self.client.get("/hello/123", headers=headers)
590597
self.assertEqual(200, resp.status_code)
@@ -594,6 +601,11 @@ def test_custom_request_header_added_in_server_span(self):
594601
"http.request.header.custom_test_header_2": (
595602
"TestValue2,TestValue3",
596603
),
604+
"http.request.header.regex_test_header_1": ("Regex Test Value 1",),
605+
"http.request.header.regex_test_header_2": (
606+
"RegexTestValue2,RegexTestValue3",
607+
),
608+
"http.request.header.my_secret_header": ("[REDACTED]",),
597609
}
598610
self.assertEqual(span.kind, trace.SpanKind.SERVER)
599611
self.assertSpanHasAttributes(span, expected)
@@ -604,6 +616,9 @@ def test_custom_request_header_not_added_in_internal_span(self):
604616
headers = {
605617
"Custom-Test-Header-1": "Test Value 1",
606618
"Custom-Test-Header-2": "TestValue2,TestValue3",
619+
"Regex-Test-Header-1": "Regex Test Value 1",
620+
"regex-test-header-2": "RegexTestValue2,RegexTestValue3",
621+
"My-Secret-Header": "My Secret Value",
607622
}
608623
resp = self.client.get("/hello/123", headers=headers)
609624
self.assertEqual(200, resp.status_code)
@@ -613,6 +628,13 @@ def test_custom_request_header_not_added_in_internal_span(self):
613628
"http.request.header.custom_test_header_2": (
614629
"TestValue2,TestValue3",
615630
),
631+
"http.request.header.regex_test_header_1": (
632+
"Regex Test Value 1",
633+
),
634+
"http.request.header.regex_test_header_2": (
635+
"RegexTestValue2,RegexTestValue3",
636+
),
637+
"http.request.header.my_secret_header": ("[REDACTED]",),
616638
}
617639
self.assertEqual(span.kind, trace.SpanKind.INTERNAL)
618640
for key, _ in not_expected.items():
@@ -630,6 +652,13 @@ def test_custom_response_header_added_in_server_span(self):
630652
"http.response.header.my_custom_header": (
631653
"my-custom-value-1,my-custom-header-2",
632654
),
655+
"http.response.header.my_custom_regex_header_1": (
656+
"my-custom-regex-value-1,my-custom-regex-value-2",
657+
),
658+
"http.response.header.my_custom_regex_header_2": (
659+
"my-custom-regex-value-3,my-custom-regex-value-4",
660+
),
661+
"http.response.header.my_secret_header": ("[REDACTED]",),
633662
}
634663
self.assertEqual(span.kind, trace.SpanKind.SERVER)
635664
self.assertSpanHasAttributes(span, expected)
@@ -648,6 +677,13 @@ def test_custom_response_header_not_added_in_internal_span(self):
648677
"http.response.header.my_custom_header": (
649678
"my-custom-value-1,my-custom-header-2",
650679
),
680+
"http.response.header.my_custom_regex_header_1": (
681+
"my-custom-regex-value-1,my-custom-regex-value-2",
682+
),
683+
"http.response.header.my_custom_regex_header_2": (
684+
"my-custom-regex-value-3,my-custom-regex-value-4",
685+
),
686+
"http.response.header.my_secret_header": ("[REDACTED]",),
651687
}
652688
self.assertEqual(span.kind, trace.SpanKind.INTERNAL)
653689
for key, _ in not_expected.items():

0 commit comments

Comments
 (0)