Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exclude lists for Django #670

Merged
merged 19 commits into from
May 27, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ext/opentelemetry-ext-django/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
toumorokoshi marked this conversation as resolved.
Show resolved Hide resolved

Excluded hosts: OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_HOSTS
Excluded paths: OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_PATHS

References
----------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from django.utils.deprecation import MiddlewareMixin

from opentelemetry import configuration
from opentelemetry.context import attach, detach
from opentelemetry.ext.django.version import __version__
from opentelemetry.ext.wsgi import (
Expand All @@ -25,10 +26,26 @@
)
from opentelemetry.propagators import extract
from opentelemetry.trace import SpanKind, get_tracer
from opentelemetry.util import disable_tracing_hostname, disable_tracing_path

_logger = getLogger(__name__)


def _disable_trace(url, path):
excluded_hosts = configuration.Configuration().DJANGO_EXCLUDED_HOSTS
excluded_paths = configuration.Configuration().DJANGO_EXCLUDED_PATHS

if excluded_hosts:
excluded_hosts = str.split(excluded_hosts, ",")
lzchen marked this conversation as resolved.
Show resolved Hide resolved
if disable_tracing_hostname(url, excluded_hosts):
return True
if excluded_paths:
excluded_paths = str.split(excluded_paths, ",")
lzchen marked this conversation as resolved.
Show resolved Hide resolved
if disable_tracing_path(path, excluded_paths):
return True
return False


class _DjangoMiddleware(MiddlewareMixin):
"""Django Middleware for OpenTelemetry
"""
Expand All @@ -50,6 +67,8 @@ def process_view(
# key.lower().replace('_', '-').replace("http-", "", 1): value
# for key, value in request.META.items()
# }
if _disable_trace(request.build_absolute_uri("?"), request.path[1:]):
return

environ = request.META

Expand Down Expand Up @@ -79,7 +98,12 @@ 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 self._environ_activation_key in request.META.keys():
if _disable_trace(request.build_absolute_uri("?"), request.path[1:]):
return
if (
self._environ_activation_key in request.META.keys()
and self._environ_token in request.META.keys()
lzchen marked this conversation as resolved.
Show resolved Hide resolved
):
request.META[self._environ_activation_key].__exit__(
type(exception),
exception,
Expand All @@ -91,6 +115,8 @@ 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("?"), request.path[1:]):
return response
if (
self._environ_activation_key in request.META.keys()
and self._environ_span_key in request.META.keys()
Expand Down
38 changes: 37 additions & 1 deletion ext/opentelemetry-ext-django/tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,27 @@
# limitations under the License.

from sys import modules
from unittest.mock import patch

from django.conf import settings
from django.conf.urls import url
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()

Expand All @@ -43,6 +48,7 @@ def setUp(self):
super().setUp()
setup_test_environment()
_django_instrumentor.instrument()
Configuration._reset() # pylint: disable=protected-access

def tearDown(self):
super().tearDown()
Expand Down Expand Up @@ -106,3 +112,33 @@ def test_error(self):
span.attributes["http.url"], "http://testserver/error/"
)
self.assertEqual(span.attributes["http.scheme"], "http")

@patch.dict(
"os.environ", # type: ignore
{
"OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_HOSTS": (
"http://testserver/excluded/"
),
"OPENTELEMETRY_PYTHON_DJANGO_EXCLUDED_PATHS": "excluded2",
},
)
def test_exclude(self):
Client().get("/traced/")
Client().get("/excluded/")
Client().get("/excluded2/")

spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 1)

span = spans[0]

self.assertEqual(span.name, "traced")
self.assertEqual(span.kind, SpanKind.SERVER)
self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK)
self.assertEqual(span.attributes["http.method"], "GET")
self.assertEqual(
span.attributes["http.url"], "http://testserver/traced/"
)
self.assertEqual(span.attributes["http.scheme"], "http")
self.assertEqual(span.attributes["http.status_code"], 200)
self.assertEqual(span.attributes["http.status_text"], "OK")
8 changes: 8 additions & 0 deletions ext/opentelemetry-ext-django/tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
34 changes: 17 additions & 17 deletions ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ def hello():
_ENVIRON_TOKEN = "opentelemetry-flask.token"


def _disable_trace(url, path):
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(path, excluded_paths):
return True
return False


def _rewrapped_app(wsgi_app):
def _wrapped_app(environ, start_response):
# We want to measure the time for route matching, etc.
Expand All @@ -79,7 +94,7 @@ def _wrapped_app(environ, start_response):

def _start_response(status, response_headers, *args, **kwargs):

if not _disable_trace(flask.request.url):
if not _disable_trace(flask.request.url, flask.request.path[1:]):

span = flask.request.environ.get(_ENVIRON_SPAN_KEY)

Expand All @@ -102,7 +117,7 @@ def _start_response(status, response_headers, *args, **kwargs):


def _before_request():
if _disable_trace(flask.request.url):
if _disable_trace(flask.request.url, flask.request.path[1:]):
return

environ = flask.request.environ
Expand Down Expand Up @@ -163,21 +178,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
Expand Down
13 changes: 3 additions & 10 deletions opentelemetry-api/src/opentelemetry/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
import time
from logging import getLogger
from typing import Sequence, Union
Expand Down Expand Up @@ -53,15 +52,9 @@ def _load_provider(
raise


# Pattern for matching up until the first '/' after the 'https://' part.
_URL_PATTERN = r"(https?|ftp)://.*?/"


def disable_tracing_path(url: str, excluded_paths: Sequence[str]) -> bool:
if excluded_paths:
# Match only the part after the first '/' that is not in _URL_PATTERN
regex = "{}({})".format(_URL_PATTERN, "|".join(excluded_paths))
if re.match(regex, url):
def disable_tracing_path(url_path: str, excluded_paths: Sequence[str]) -> bool:
lzchen marked this conversation as resolved.
Show resolved Hide resolved
for path in excluded_paths:
if url_path.startswith(path):
return True
return False

Expand Down