diff --git a/ext/opentelemetry-ext-django/CHANGELOG.md b/ext/opentelemetry-ext-django/CHANGELOG.md index f46a42c2239..c7c4966dc96 100644 --- a/ext/opentelemetry-ext-django/CHANGELOG.md +++ b/ext/opentelemetry-ext-django/CHANGELOG.md @@ -2,7 +2,9 @@ ## Unreleased -- Add support for django >= 1.10 (#717) +- Add exclude list for paths and hosts to prevent from tracing + ([#670](https://github.com/open-telemetry/opentelemetry-python/pull/670)) +- Add support for django >= 1.10 (#717) ## 0.7b1 diff --git a/ext/opentelemetry-ext-django/README.rst b/ext/opentelemetry-ext-django/README.rst index b922046ca6f..834bd63249e 100644 --- a/ext/opentelemetry-ext-django/README.rst +++ b/ext/opentelemetry-ext-django/README.rst @@ -15,6 +15,16 @@ Installation pip install opentelemetry-ext-django +Configuration +------------- + +Exclude lists +************* +Excludes certain hosts and paths from being tracked. Pass in comma delimited string into environment variables. +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. + +Excluded hosts: OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_HOSTS +Excluded paths: OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_PATHS References ---------- diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py index 912ce0f1a26..0ce03b97cb7 100644 --- a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py +++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py @@ -14,6 +14,7 @@ from logging import getLogger +from opentelemetry.configuration import Configuration from opentelemetry.context import attach, detach from opentelemetry.ext.django.version import __version__ from opentelemetry.ext.wsgi import ( @@ -23,6 +24,7 @@ ) from opentelemetry.propagators import extract from opentelemetry.trace import SpanKind, get_tracer +from opentelemetry.util import disable_trace try: from django.utils.deprecation import MiddlewareMixin @@ -42,6 +44,13 @@ class _DjangoMiddleware(MiddlewareMixin): _environ_token = "opentelemetry-instrumentor-django.token" _environ_span_key = "opentelemetry-instrumentor-django.span_key" + _excluded_hosts = Configuration().DJANGO_EXCLUDED_HOSTS or [] + _excluded_paths = Configuration().DJANGO_EXCLUDED_PATHS or [] + if _excluded_hosts: + _excluded_hosts = str.split(_excluded_hosts, ",") + if _excluded_paths: + _excluded_paths = str.split(_excluded_paths, ",") + def process_view( self, request, view_func, view_args, view_kwargs ): # pylint: disable=unused-argument @@ -53,6 +62,12 @@ def process_view( # key.lower().replace('_', '-').replace("http-", "", 1): value # for key, value in request.META.items() # } + if disable_trace( + request.build_absolute_uri("?"), + self._excluded_hosts, + self._excluded_paths, + ): + return environ = request.META @@ -82,6 +97,13 @@ def process_exception(self, request, exception): # Django can call this method and process_response later. In order # to avoid __exit__ and detach from being called twice then, the # respective keys are being removed here. + if disable_trace( + request.build_absolute_uri("?"), + self._excluded_hosts, + self._excluded_paths, + ): + return + if self._environ_activation_key in request.META.keys(): request.META[self._environ_activation_key].__exit__( type(exception), @@ -94,6 +116,13 @@ def process_exception(self, request, exception): request.META.pop(self._environ_token, None) def process_response(self, request, response): + if disable_trace( + request.build_absolute_uri("?"), + self._excluded_hosts, + self._excluded_paths, + ): + return response + if ( self._environ_activation_key in request.META.keys() and self._environ_span_key in request.META.keys() diff --git a/ext/opentelemetry-ext-django/tests/test_middleware.py b/ext/opentelemetry-ext-django/tests/test_middleware.py index afee6acf77a..e0ac92de521 100644 --- a/ext/opentelemetry-ext-django/tests/test_middleware.py +++ b/ext/opentelemetry-ext-django/tests/test_middleware.py @@ -19,16 +19,20 @@ from django.test import Client from django.test.utils import setup_test_environment, teardown_test_environment +from opentelemetry.configuration import Configuration from opentelemetry.ext.django import DjangoInstrumentor from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCanonicalCode -from .views import error, traced # pylint: disable=import-error +# pylint: disable=import-error +from .views import error, excluded, excluded2, traced urlpatterns = [ url(r"^traced/", traced), url(r"^error/", error), + url(r"^excluded/", excluded), + url(r"^excluded2/", excluded2), ] _django_instrumentor = DjangoInstrumentor() @@ -43,6 +47,7 @@ def setUp(self): super().setUp() setup_test_environment() _django_instrumentor.instrument() + Configuration._reset() # pylint: disable=protected-access def tearDown(self): super().tearDown() diff --git a/ext/opentelemetry-ext-django/tests/views.py b/ext/opentelemetry-ext-django/tests/views.py index 498a4518eda..9035f0ea03c 100644 --- a/ext/opentelemetry-ext-django/tests/views.py +++ b/ext/opentelemetry-ext-django/tests/views.py @@ -7,3 +7,11 @@ def traced(request): # pylint: disable=unused-argument def error(request): # pylint: disable=unused-argument raise ValueError("error") + + +def excluded(request): # pylint: disable=unused-argument + return HttpResponse() + + +def excluded2(request): # pylint: disable=unused-argument + return HttpResponse() diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index 040c8770c6a..cd94dc7e47b 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -55,11 +55,7 @@ def hello(): from opentelemetry import configuration, context, propagators, trace from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.flask.version import __version__ -from opentelemetry.util import ( - disable_tracing_hostname, - disable_tracing_path, - time_ns, -) +from opentelemetry.util import disable_trace, time_ns _logger = getLogger(__name__) @@ -69,6 +65,24 @@ def hello(): _ENVIRON_TOKEN = "opentelemetry-flask.token" +def get_excluded_hosts(): + hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS or [] + if hosts: + hosts = str.split(hosts, ",") + return hosts + + +def get_excluded_paths(): + paths = configuration.Configuration().FLASK_EXCLUDED_PATHS or [] + if paths: + paths = str.split(paths, ",") + return paths + + +_excluded_hosts = get_excluded_hosts() +_excluded_paths = get_excluded_paths() + + def _rewrapped_app(wsgi_app): def _wrapped_app(environ, start_response): # We want to measure the time for route matching, etc. @@ -78,9 +92,9 @@ def _wrapped_app(environ, start_response): environ[_ENVIRON_STARTTIME_KEY] = time_ns() def _start_response(status, response_headers, *args, **kwargs): - - if not _disable_trace(flask.request.url): - + if not disable_trace( + flask.request.url, _excluded_hosts, _excluded_paths + ): span = flask.request.environ.get(_ENVIRON_SPAN_KEY) if span: @@ -102,7 +116,7 @@ def _start_response(status, response_headers, *args, **kwargs): def _before_request(): - if _disable_trace(flask.request.url): + if disable_trace(flask.request.url, _excluded_hosts, _excluded_paths): return environ = flask.request.environ @@ -134,6 +148,9 @@ def _before_request(): def _teardown_request(exc): + if disable_trace(flask.request.url, _excluded_hosts, _excluded_paths): + return + activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) if not activation: _logger.warning( @@ -163,21 +180,6 @@ def __init__(self, *args, **kwargs): self.teardown_request(_teardown_request) -def _disable_trace(url): - excluded_hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS - excluded_paths = configuration.Configuration().FLASK_EXCLUDED_PATHS - - if excluded_hosts: - excluded_hosts = str.split(excluded_hosts, ",") - if disable_tracing_hostname(url, excluded_hosts): - return True - if excluded_paths: - excluded_paths = str.split(excluded_paths, ",") - if disable_tracing_path(url, excluded_paths): - return True - return False - - class FlaskInstrumentor(BaseInstrumentor): # pylint: disable=protected-access,attribute-defined-outside-init """An instrumentor for flask.Flask diff --git a/ext/opentelemetry-ext-flask/tests/base_test.py b/ext/opentelemetry-ext-flask/tests/base_test.py index 42341826df0..c2bc646e1ba 100644 --- a/ext/opentelemetry-ext-flask/tests/base_test.py +++ b/ext/opentelemetry-ext-flask/tests/base_test.py @@ -12,34 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import patch - -from flask import request from werkzeug.test import Client from werkzeug.wrappers import BaseResponse -from opentelemetry import trace from opentelemetry.configuration import Configuration -def expected_attributes(override_attributes): - default_attributes = { - "component": "http", - "http.method": "GET", - "http.server_name": "localhost", - "http.scheme": "http", - "host.port": 80, - "http.host": "localhost", - "http.target": "/", - "http.flavor": "1.1", - "http.status_text": "OK", - "http.status_code": 200, - } - for key, val in override_attributes.items(): - default_attributes[key] = val - return default_attributes - - class InstrumentationTest: def setUp(self): # pylint: disable=invalid-name super().setUp() # pylint: disable=no-member @@ -66,89 +44,3 @@ def excluded2_endpoint(): # pylint: disable=attribute-defined-outside-init self.client = Client(self.app, BaseResponse) - - # pylint: disable=no-member - def test_only_strings_in_environ(self): - """ - Some WSGI servers (such as Gunicorn) expect keys in the environ object - to be strings - - OpenTelemetry should adhere to this convention. - """ - nonstring_keys = set() - - def assert_environ(): - for key in request.environ: - if not isinstance(key, str): - nonstring_keys.add(key) - return "hi" - - self.app.route("/assert_environ")(assert_environ) - self.client.get("/assert_environ") - self.assertEqual(nonstring_keys, set()) - - def test_simple(self): - expected_attrs = expected_attributes( - {"http.target": "/hello/123", "http.route": "/hello/"} - ) - self.client.get("/hello/123") - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "_hello_endpoint") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - def test_404(self): - expected_attrs = expected_attributes( - { - "http.method": "POST", - "http.target": "/bye", - "http.status_text": "NOT FOUND", - "http.status_code": 404, - } - ) - - resp = self.client.post("/bye") - self.assertEqual(404, resp.status_code) - resp.close() - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "/bye") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - def test_internal_error(self): - expected_attrs = expected_attributes( - { - "http.target": "/hello/500", - "http.route": "/hello/", - "http.status_text": "INTERNAL SERVER ERROR", - "http.status_code": 500, - } - ) - resp = self.client.get("/hello/500") - self.assertEqual(500, resp.status_code) - resp.close() - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "_hello_endpoint") - self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) - self.assertEqual(span_list[0].attributes, expected_attrs) - - @patch.dict( - "os.environ", # type: ignore - { - "OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS": ( - "http://localhost/excluded" - ), - "OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS": "excluded2", - }, - ) - def test_excluded_path(self): - self.client.get("/hello/123") - self.client.get("/excluded") - self.client.get("/excluded2") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "_hello_endpoint") diff --git a/ext/opentelemetry-ext-flask/tests/test_programmatic.py b/ext/opentelemetry-ext-flask/tests/test_programmatic.py index 4e17f25fdc1..95f82373e3d 100644 --- a/ext/opentelemetry-ext-flask/tests/test_programmatic.py +++ b/ext/opentelemetry-ext-flask/tests/test_programmatic.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from flask import Flask +from flask import Flask, request +from opentelemetry import trace from opentelemetry.ext.flask import FlaskInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase @@ -22,6 +23,24 @@ from .base_test import InstrumentationTest +def expected_attributes(override_attributes): + default_attributes = { + "component": "http", + "http.method": "GET", + "http.server_name": "localhost", + "http.scheme": "http", + "host.port": 80, + "http.host": "localhost", + "http.target": "/", + "http.flavor": "1.1", + "http.status_text": "OK", + "http.status_code": 200, + } + for key, val in override_attributes.items(): + default_attributes[key] = val + return default_attributes + + class TestProgrammatic(InstrumentationTest, TestBase, WsgiTestBase): def setUp(self): super().setUp() @@ -51,3 +70,72 @@ def test_uninstrument(self): self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) + + # pylint: disable=no-member + def test_only_strings_in_environ(self): + """ + Some WSGI servers (such as Gunicorn) expect keys in the environ object + to be strings + + OpenTelemetry should adhere to this convention. + """ + nonstring_keys = set() + + def assert_environ(): + for key in request.environ: + if not isinstance(key, str): + nonstring_keys.add(key) + return "hi" + + self.app.route("/assert_environ")(assert_environ) + self.client.get("/assert_environ") + self.assertEqual(nonstring_keys, set()) + + def test_simple(self): + expected_attrs = expected_attributes( + {"http.target": "/hello/123", "http.route": "/hello/"} + ) + self.client.get("/hello/123") + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "_hello_endpoint") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_404(self): + expected_attrs = expected_attributes( + { + "http.method": "POST", + "http.target": "/bye", + "http.status_text": "NOT FOUND", + "http.status_code": 404, + } + ) + + resp = self.client.post("/bye") + self.assertEqual(404, resp.status_code) + resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "/bye") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_internal_error(self): + expected_attrs = expected_attributes( + { + "http.target": "/hello/500", + "http.route": "/hello/", + "http.status_text": "INTERNAL SERVER ERROR", + "http.status_code": 500, + } + ) + resp = self.client.get("/hello/500") + self.assertEqual(500, resp.status_code) + resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "_hello_endpoint") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index 48c350730ea..bab42b0bde2 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -70,3 +70,11 @@ def disable_tracing_hostname( url: str, excluded_hostnames: Sequence[str] ) -> bool: return url in excluded_hostnames + + +def disable_trace( + url: str, excluded_hosts: Sequence[str], excluded_paths: Sequence[str] +) -> bool: + return disable_tracing_hostname( + url, excluded_hosts + ) or disable_tracing_path(url, excluded_paths)