Skip to content

Commit a6bcee9

Browse files
Dan RogersCircleCI
Dan Rogers
authored and
CircleCI
committed
Add support for regular expression matching and sanitizing of headers in Django. (open-telemetry#1411)
1 parent 8c18c1f commit a6bcee9

File tree

5 files changed

+129
-44
lines changed

5 files changed

+129
-44
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
([#1402](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1402))
3232
- Add support for py3.11
3333
([#1415](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1415))
34+
- `opentelemetry-instrumentation-django` Add support for regular expression matching and sanitization of HTTP headers.
35+
([#1411](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1411))
3436
- `opentelemetry-instrumentation-falcon` Add support for regular expression matching and sanitization of HTTP headers.
3537
([#1412](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1412))
3638
- `opentelemetry-instrumentation-flask` Add support for regular expression matching and sanitization of HTTP headers.

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

+72-26
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,9 @@
9494
9595
Exclude lists
9696
*************
97-
To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_DJANGO_EXCLUDED_URLS``
98-
(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude.
97+
To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_DJANGO_EXCLUDED_URLS``
98+
(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the
99+
URLs.
99100
100101
For example,
101102
@@ -107,23 +108,24 @@
107108
108109
Request attributes
109110
********************
110-
To extract certain attributes from Django's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma
111-
delimited list of request attribute names.
111+
To extract attributes from Django's request object and use them as span attributes, set the environment variable
112+
``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma delimited list of request attribute names.
112113
113114
For example,
114115
115116
::
116117
117118
export OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS='path_info,content_type'
118119
119-
will extract path_info and content_type attributes from every traced request and add them as span attritbues.
120+
will extract the ``path_info`` and ``content_type`` attributes from every traced request and add them as span attributes.
120121
121122
Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes
122123
123124
Request and Response hooks
124125
***************************
125-
The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a Span is created for a request
126-
and right before the span is finished while processing a response. The hooks can be configured as follows:
126+
This instrumentation supports request and response hooks. These are functions that get called
127+
right after a span is created for a request and right before the span is finished for the response.
128+
The hooks can be configured as follows:
127129
128130
.. code:: python
129131
@@ -140,50 +142,94 @@ def response_hook(span, request, response):
140142
141143
Capture HTTP request and response headers
142144
*****************************************
143-
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>`_.
144147
145148
Request headers
146149
***************
147-
To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST``
148-
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.
149152
150153
For example,
151154
::
152155
153-
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content_type,custom_request_header"
156+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header"
154157
155-
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.
156159
157-
It is recommended that you should give the correct names of the headers to be captured in the environment variable.
158-
Request header names in django are case insensitive. So, giving header name as ``CUStom_Header`` in environment variable will be able capture header with name ``custom-header``.
160+
Request header names in Django are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
161+
variable will capture the header named ``custom-header``.
159162
160-
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 _ ).
161-
The value of the attribute will be single item list containing all the header values.
163+
Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
164+
::
165+
166+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*"
167+
168+
Would match all request headers that start with ``Accept`` and ``X-``.
169+
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=".*"
162174
163-
Example of the added span attribute,
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:
164180
``http.request.header.custom_request_header = ["<value1>,<value2>"]``
165181
166182
Response headers
167183
****************
168-
To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE``
169-
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.
170186
171187
For example,
172188
::
173189
174-
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content_type,custom_response_header"
190+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header"
191+
192+
will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes.
175193
176-
will extract content_type and custom_response_header from response headers and add them as span attributes.
194+
Response header names in Django are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
195+
variable will capture the header named ``custom-header``.
177196
178-
It is recommended that you should give the correct names of the headers to be captured in the environment variable.
179-
Response header names captured in django are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``.
197+
Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
198+
::
180199
181-
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 _ ).
182-
The value of the attribute will be single item list containing all the header values.
200+
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*"
183201
184-
Example of the added span attribute,
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:
185214
``http.response.header.custom_response_header = ["<value1>,<value2>"]``
186215
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+
230+
Note:
231+
The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change.
232+
187233
API
188234
---
189235

instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py

+24-9
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
format_trace_id,
4949
)
5050
from opentelemetry.util.http import (
51+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
5152
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
5253
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
5354
_active_requests_count_attrs,
@@ -530,6 +531,14 @@ def test_django_with_wsgi_instrumented(self):
530531
)
531532

532533

534+
@patch.dict(
535+
"os.environ",
536+
{
537+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
538+
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.*",
539+
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.*",
540+
},
541+
)
533542
class TestMiddlewareWsgiWithCustomHeaders(WsgiTestBase):
534543
@classmethod
535544
def setUpClass(cls):
@@ -542,18 +551,9 @@ def setUp(self):
542551
tracer_provider, exporter = self.create_tracer_provider()
543552
self.exporter = exporter
544553
_django_instrumentor.instrument(tracer_provider=tracer_provider)
545-
self.env_patch = patch.dict(
546-
"os.environ",
547-
{
548-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
549-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
550-
},
551-
)
552-
self.env_patch.start()
553554

554555
def tearDown(self):
555556
super().tearDown()
556-
self.env_patch.stop()
557557
teardown_test_environment()
558558
_django_instrumentor.uninstrument()
559559

@@ -570,10 +570,18 @@ def test_http_custom_request_headers_in_span_attributes(self):
570570
"http.request.header.custom_test_header_2": (
571571
"test-header-value-2",
572572
),
573+
"http.request.header.regex_test_header_1": ("Regex Test Value 1",),
574+
"http.request.header.regex_test_header_2": (
575+
"RegexTestValue2,RegexTestValue3",
576+
),
577+
"http.request.header.my_secret_header": ("[REDACTED]",),
573578
}
574579
Client(
575580
HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1",
576581
HTTP_CUSTOM_TEST_HEADER_2="test-header-value-2",
582+
HTTP_REGEX_TEST_HEADER_1="Regex Test Value 1",
583+
HTTP_REGEX_TEST_HEADER_2="RegexTestValue2,RegexTestValue3",
584+
HTTP_MY_SECRET_HEADER="My Secret Value",
577585
).get("/traced/")
578586
spans = self.exporter.get_finished_spans()
579587
self.assertEqual(len(spans), 1)
@@ -607,6 +615,13 @@ def test_http_custom_response_headers_in_span_attributes(self):
607615
"http.response.header.custom_test_header_2": (
608616
"test-header-value-2",
609617
),
618+
"http.response.header.my_custom_regex_header_1": (
619+
"my-custom-regex-value-1,my-custom-regex-value-2",
620+
),
621+
"http.response.header.my_custom_regex_header_2": (
622+
"my-custom-regex-value-3,my-custom-regex-value-4",
623+
),
624+
"http.response.header.my_secret_header": ("[REDACTED]",),
610625
}
611626
Client().get("/traced_custom_header/")
612627
spans = self.exporter.get_finished_spans()

instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py

+24-9
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
format_trace_id,
4444
)
4545
from opentelemetry.util.http import (
46+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
4647
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
4748
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
4849
get_excluded_urls,
@@ -424,6 +425,14 @@ async def test_tracer_provider_traced(self):
424425
)
425426

426427

428+
@patch.dict(
429+
"os.environ",
430+
{
431+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
432+
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.*",
433+
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.*",
434+
},
435+
)
427436
class TestMiddlewareAsgiWithCustomHeaders(SimpleTestCase, TestBase):
428437
@classmethod
429438
def setUpClass(cls):
@@ -437,18 +446,9 @@ def setUp(self):
437446
tracer_provider, exporter = self.create_tracer_provider()
438447
self.exporter = exporter
439448
_django_instrumentor.instrument(tracer_provider=tracer_provider)
440-
self.env_patch = patch.dict(
441-
"os.environ",
442-
{
443-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
444-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
445-
},
446-
)
447-
self.env_patch.start()
448449

449450
def tearDown(self):
450451
super().tearDown()
451-
self.env_patch.stop()
452452
teardown_test_environment()
453453
_django_instrumentor.uninstrument()
454454

@@ -465,12 +465,20 @@ async def test_http_custom_request_headers_in_span_attributes(self):
465465
"http.request.header.custom_test_header_2": (
466466
"test-header-value-2",
467467
),
468+
"http.request.header.regex_test_header_1": ("Regex Test Value 1",),
469+
"http.request.header.regex_test_header_2": (
470+
"RegexTestValue2,RegexTestValue3",
471+
),
472+
"http.request.header.my_secret_header": ("[REDACTED]",),
468473
}
469474
await self.async_client.get(
470475
"/traced/",
471476
**{
472477
"custom-test-header-1": "test-header-value-1",
473478
"custom-test-header-2": "test-header-value-2",
479+
"Regex-Test-Header-1": "Regex Test Value 1",
480+
"regex-test-header-2": "RegexTestValue2,RegexTestValue3",
481+
"My-Secret-Header": "My Secret Value",
474482
},
475483
)
476484
spans = self.exporter.get_finished_spans()
@@ -510,6 +518,13 @@ async def test_http_custom_response_headers_in_span_attributes(self):
510518
"http.response.header.custom_test_header_2": (
511519
"test-header-value-2",
512520
),
521+
"http.response.header.my_custom_regex_header_1": (
522+
"my-custom-regex-value-1,my-custom-regex-value-2",
523+
),
524+
"http.response.header.my_custom_regex_header_2": (
525+
"my-custom-regex-value-3,my-custom-regex-value-4",
526+
),
527+
"http.response.header.my_secret_header": ("[REDACTED]",),
513528
}
514529
await self.async_client.get("/traced_custom_header/")
515530
spans = self.exporter.get_finished_spans()

instrumentation/opentelemetry-instrumentation-django/tests/views.py

+7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def response_with_custom_header(request):
3535
response = HttpResponse()
3636
response["custom-test-header-1"] = "test-header-value-1"
3737
response["custom-test-header-2"] = "test-header-value-2"
38+
response[
39+
"my-custom-regex-header-1"
40+
] = "my-custom-regex-value-1,my-custom-regex-value-2"
41+
response[
42+
"my-custom-regex-header-2"
43+
] = "my-custom-regex-value-3,my-custom-regex-value-4"
44+
response["my-secret-header"] = "my-secret-value"
3845
return response
3946

4047

0 commit comments

Comments
 (0)