Skip to content

Commit 807e106

Browse files
lzchenocelotlalrextoumorokoshi
authored
django: Add exclude lists (#670)
Similar to flask, enabling exclusion of spans based on host and path. Co-authored-by: Diego Hurtado <[email protected]> Co-authored-by: alrex <[email protected]> Co-authored-by: Yusuke Tsutsumi <[email protected]>
1 parent 8a946b5 commit 807e106

File tree

9 files changed

+179
-135
lines changed

9 files changed

+179
-135
lines changed

ext/opentelemetry-ext-django/CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
## Unreleased
44

5-
- Add support for django >= 1.10 (#717)
5+
- Add exclude list for paths and hosts to prevent from tracing
6+
([#670](https://github.com/open-telemetry/opentelemetry-python/pull/670))
7+
- Add support for django >= 1.10 (#717)
68

79
## 0.7b1
810

ext/opentelemetry-ext-django/README.rst

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ Installation
1515

1616
pip install opentelemetry-ext-django
1717

18+
Configuration
19+
-------------
20+
21+
Exclude lists
22+
*************
23+
Excludes certain hosts and paths from being tracked. Pass in comma delimited string into environment variables.
24+
Host refers to the entire url and path refers to the part of the url after the domain. Host matches the exact string that is given, where as path matches if the url starts with the given excluded path.
25+
26+
Excluded hosts: OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_HOSTS
27+
Excluded paths: OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_PATHS
1828

1929
References
2030
----------

ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py

+29
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from logging import getLogger
1616

17+
from opentelemetry.configuration import Configuration
1718
from opentelemetry.context import attach, detach
1819
from opentelemetry.ext.django.version import __version__
1920
from opentelemetry.ext.wsgi import (
@@ -23,6 +24,7 @@
2324
)
2425
from opentelemetry.propagators import extract
2526
from opentelemetry.trace import SpanKind, get_tracer
27+
from opentelemetry.util import disable_trace
2628

2729
try:
2830
from django.utils.deprecation import MiddlewareMixin
@@ -42,6 +44,13 @@ class _DjangoMiddleware(MiddlewareMixin):
4244
_environ_token = "opentelemetry-instrumentor-django.token"
4345
_environ_span_key = "opentelemetry-instrumentor-django.span_key"
4446

47+
_excluded_hosts = Configuration().DJANGO_EXCLUDED_HOSTS or []
48+
_excluded_paths = Configuration().DJANGO_EXCLUDED_PATHS or []
49+
if _excluded_hosts:
50+
_excluded_hosts = str.split(_excluded_hosts, ",")
51+
if _excluded_paths:
52+
_excluded_paths = str.split(_excluded_paths, ",")
53+
4554
def process_view(
4655
self, request, view_func, view_args, view_kwargs
4756
): # pylint: disable=unused-argument
@@ -53,6 +62,12 @@ def process_view(
5362
# key.lower().replace('_', '-').replace("http-", "", 1): value
5463
# for key, value in request.META.items()
5564
# }
65+
if disable_trace(
66+
request.build_absolute_uri("?"),
67+
self._excluded_hosts,
68+
self._excluded_paths,
69+
):
70+
return
5671

5772
environ = request.META
5873

@@ -82,6 +97,13 @@ def process_exception(self, request, exception):
8297
# Django can call this method and process_response later. In order
8398
# to avoid __exit__ and detach from being called twice then, the
8499
# respective keys are being removed here.
100+
if disable_trace(
101+
request.build_absolute_uri("?"),
102+
self._excluded_hosts,
103+
self._excluded_paths,
104+
):
105+
return
106+
85107
if self._environ_activation_key in request.META.keys():
86108
request.META[self._environ_activation_key].__exit__(
87109
type(exception),
@@ -94,6 +116,13 @@ def process_exception(self, request, exception):
94116
request.META.pop(self._environ_token, None)
95117

96118
def process_response(self, request, response):
119+
if disable_trace(
120+
request.build_absolute_uri("?"),
121+
self._excluded_hosts,
122+
self._excluded_paths,
123+
):
124+
return response
125+
97126
if (
98127
self._environ_activation_key in request.META.keys()
99128
and self._environ_span_key in request.META.keys()

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,20 @@
1919
from django.test import Client
2020
from django.test.utils import setup_test_environment, teardown_test_environment
2121

22+
from opentelemetry.configuration import Configuration
2223
from opentelemetry.ext.django import DjangoInstrumentor
2324
from opentelemetry.test.wsgitestutil import WsgiTestBase
2425
from opentelemetry.trace import SpanKind
2526
from opentelemetry.trace.status import StatusCanonicalCode
2627

27-
from .views import error, traced # pylint: disable=import-error
28+
# pylint: disable=import-error
29+
from .views import error, excluded, excluded2, traced
2830

2931
urlpatterns = [
3032
url(r"^traced/", traced),
3133
url(r"^error/", error),
34+
url(r"^excluded/", excluded),
35+
url(r"^excluded2/", excluded2),
3236
]
3337
_django_instrumentor = DjangoInstrumentor()
3438

@@ -43,6 +47,7 @@ def setUp(self):
4347
super().setUp()
4448
setup_test_environment()
4549
_django_instrumentor.instrument()
50+
Configuration._reset() # pylint: disable=protected-access
4651

4752
def tearDown(self):
4853
super().tearDown()

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

+8
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@ def traced(request): # pylint: disable=unused-argument
77

88
def error(request): # pylint: disable=unused-argument
99
raise ValueError("error")
10+
11+
12+
def excluded(request): # pylint: disable=unused-argument
13+
return HttpResponse()
14+
15+
16+
def excluded2(request): # pylint: disable=unused-argument
17+
return HttpResponse()

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

+26-24
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,7 @@ def hello():
5555
from opentelemetry import configuration, context, propagators, trace
5656
from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor
5757
from opentelemetry.ext.flask.version import __version__
58-
from opentelemetry.util import (
59-
disable_tracing_hostname,
60-
disable_tracing_path,
61-
time_ns,
62-
)
58+
from opentelemetry.util import disable_trace, time_ns
6359

6460
_logger = getLogger(__name__)
6561

@@ -69,6 +65,24 @@ def hello():
6965
_ENVIRON_TOKEN = "opentelemetry-flask.token"
7066

7167

68+
def get_excluded_hosts():
69+
hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS or []
70+
if hosts:
71+
hosts = str.split(hosts, ",")
72+
return hosts
73+
74+
75+
def get_excluded_paths():
76+
paths = configuration.Configuration().FLASK_EXCLUDED_PATHS or []
77+
if paths:
78+
paths = str.split(paths, ",")
79+
return paths
80+
81+
82+
_excluded_hosts = get_excluded_hosts()
83+
_excluded_paths = get_excluded_paths()
84+
85+
7286
def _rewrapped_app(wsgi_app):
7387
def _wrapped_app(environ, start_response):
7488
# We want to measure the time for route matching, etc.
@@ -78,9 +92,9 @@ def _wrapped_app(environ, start_response):
7892
environ[_ENVIRON_STARTTIME_KEY] = time_ns()
7993

8094
def _start_response(status, response_headers, *args, **kwargs):
81-
82-
if not _disable_trace(flask.request.url):
83-
95+
if not disable_trace(
96+
flask.request.url, _excluded_hosts, _excluded_paths
97+
):
8498
span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
8599

86100
if span:
@@ -102,7 +116,7 @@ def _start_response(status, response_headers, *args, **kwargs):
102116

103117

104118
def _before_request():
105-
if _disable_trace(flask.request.url):
119+
if disable_trace(flask.request.url, _excluded_hosts, _excluded_paths):
106120
return
107121

108122
environ = flask.request.environ
@@ -134,6 +148,9 @@ def _before_request():
134148

135149

136150
def _teardown_request(exc):
151+
if disable_trace(flask.request.url, _excluded_hosts, _excluded_paths):
152+
return
153+
137154
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
138155
if not activation:
139156
_logger.warning(
@@ -163,21 +180,6 @@ def __init__(self, *args, **kwargs):
163180
self.teardown_request(_teardown_request)
164181

165182

166-
def _disable_trace(url):
167-
excluded_hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS
168-
excluded_paths = configuration.Configuration().FLASK_EXCLUDED_PATHS
169-
170-
if excluded_hosts:
171-
excluded_hosts = str.split(excluded_hosts, ",")
172-
if disable_tracing_hostname(url, excluded_hosts):
173-
return True
174-
if excluded_paths:
175-
excluded_paths = str.split(excluded_paths, ",")
176-
if disable_tracing_path(url, excluded_paths):
177-
return True
178-
return False
179-
180-
181183
class FlaskInstrumentor(BaseInstrumentor):
182184
# pylint: disable=protected-access,attribute-defined-outside-init
183185
"""An instrumentor for flask.Flask

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

-108
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from unittest.mock import patch
16-
17-
from flask import request
1815
from werkzeug.test import Client
1916
from werkzeug.wrappers import BaseResponse
2017

21-
from opentelemetry import trace
2218
from opentelemetry.configuration import Configuration
2319

2420

25-
def expected_attributes(override_attributes):
26-
default_attributes = {
27-
"component": "http",
28-
"http.method": "GET",
29-
"http.server_name": "localhost",
30-
"http.scheme": "http",
31-
"host.port": 80,
32-
"http.host": "localhost",
33-
"http.target": "/",
34-
"http.flavor": "1.1",
35-
"http.status_text": "OK",
36-
"http.status_code": 200,
37-
}
38-
for key, val in override_attributes.items():
39-
default_attributes[key] = val
40-
return default_attributes
41-
42-
4321
class InstrumentationTest:
4422
def setUp(self): # pylint: disable=invalid-name
4523
super().setUp() # pylint: disable=no-member
@@ -66,89 +44,3 @@ def excluded2_endpoint():
6644

6745
# pylint: disable=attribute-defined-outside-init
6846
self.client = Client(self.app, BaseResponse)
69-
70-
# pylint: disable=no-member
71-
def test_only_strings_in_environ(self):
72-
"""
73-
Some WSGI servers (such as Gunicorn) expect keys in the environ object
74-
to be strings
75-
76-
OpenTelemetry should adhere to this convention.
77-
"""
78-
nonstring_keys = set()
79-
80-
def assert_environ():
81-
for key in request.environ:
82-
if not isinstance(key, str):
83-
nonstring_keys.add(key)
84-
return "hi"
85-
86-
self.app.route("/assert_environ")(assert_environ)
87-
self.client.get("/assert_environ")
88-
self.assertEqual(nonstring_keys, set())
89-
90-
def test_simple(self):
91-
expected_attrs = expected_attributes(
92-
{"http.target": "/hello/123", "http.route": "/hello/<int:helloid>"}
93-
)
94-
self.client.get("/hello/123")
95-
96-
span_list = self.memory_exporter.get_finished_spans()
97-
self.assertEqual(len(span_list), 1)
98-
self.assertEqual(span_list[0].name, "_hello_endpoint")
99-
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
100-
self.assertEqual(span_list[0].attributes, expected_attrs)
101-
102-
def test_404(self):
103-
expected_attrs = expected_attributes(
104-
{
105-
"http.method": "POST",
106-
"http.target": "/bye",
107-
"http.status_text": "NOT FOUND",
108-
"http.status_code": 404,
109-
}
110-
)
111-
112-
resp = self.client.post("/bye")
113-
self.assertEqual(404, resp.status_code)
114-
resp.close()
115-
span_list = self.memory_exporter.get_finished_spans()
116-
self.assertEqual(len(span_list), 1)
117-
self.assertEqual(span_list[0].name, "/bye")
118-
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
119-
self.assertEqual(span_list[0].attributes, expected_attrs)
120-
121-
def test_internal_error(self):
122-
expected_attrs = expected_attributes(
123-
{
124-
"http.target": "/hello/500",
125-
"http.route": "/hello/<int:helloid>",
126-
"http.status_text": "INTERNAL SERVER ERROR",
127-
"http.status_code": 500,
128-
}
129-
)
130-
resp = self.client.get("/hello/500")
131-
self.assertEqual(500, resp.status_code)
132-
resp.close()
133-
span_list = self.memory_exporter.get_finished_spans()
134-
self.assertEqual(len(span_list), 1)
135-
self.assertEqual(span_list[0].name, "_hello_endpoint")
136-
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
137-
self.assertEqual(span_list[0].attributes, expected_attrs)
138-
139-
@patch.dict(
140-
"os.environ", # type: ignore
141-
{
142-
"OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS": (
143-
"http://localhost/excluded"
144-
),
145-
"OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS": "excluded2",
146-
},
147-
)
148-
def test_excluded_path(self):
149-
self.client.get("/hello/123")
150-
self.client.get("/excluded")
151-
self.client.get("/excluded2")
152-
span_list = self.memory_exporter.get_finished_spans()
153-
self.assertEqual(len(span_list), 1)
154-
self.assertEqual(span_list[0].name, "_hello_endpoint")

0 commit comments

Comments
 (0)