diff --git a/ext/opentelemetry-ext-flask/CHANGELOG.md b/ext/opentelemetry-ext-flask/CHANGELOG.md index f7523f36c4..7d4d85b719 100644 --- a/ext/opentelemetry-ext-flask/CHANGELOG.md +++ b/ext/opentelemetry-ext-flask/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add exclude list for paths and hosts + ([#630](https://github.com/open-telemetry/opentelemetry-python/pull/630)) + ## 0.6b0 Released 2020-03-30 diff --git a/ext/opentelemetry-ext-flask/README.rst b/ext/opentelemetry-ext-flask/README.rst index 135b2c398c..0a4c894077 100644 --- a/ext/opentelemetry-ext-flask/README.rst +++ b/ext/opentelemetry-ext-flask/README.rst @@ -16,6 +16,17 @@ Installation pip install opentelemetry-ext-flask +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_FLASK_EXCLUDED_HOSTS +Excluded paths: OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS + References ---------- 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 eb008eeadc..1e936da115 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -51,10 +51,14 @@ def hello(): import flask import opentelemetry.ext.wsgi as otel_wsgi -from opentelemetry import context, propagators, trace +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 time_ns +from opentelemetry.util import ( + disable_tracing_hostname, + disable_tracing_path, + time_ns, +) logger = logging.getLogger(__name__) @@ -80,17 +84,18 @@ def wrapped_app(environ, start_response): environ[_ENVIRON_STARTTIME_KEY] = time_ns() def _start_response(status, response_headers, *args, **kwargs): - span = flask.request.environ.get(_ENVIRON_SPAN_KEY) - if span: - otel_wsgi.add_response_attributes( - span, status, response_headers - ) - else: - logger.warning( - "Flask environ's OpenTelemetry span " - "missing at _start_response(%s)", - status, - ) + if not _disable_trace(flask.request.url): + span = flask.request.environ.get(_ENVIRON_SPAN_KEY) + if span: + otel_wsgi.add_response_attributes( + span, status, response_headers + ) + else: + logger.warning( + "Flask environ's OpenTelemetry span " + "missing at _start_response(%s)", + status, + ) return start_response( status, response_headers, *args, **kwargs @@ -102,6 +107,9 @@ def _start_response(status, response_headers, *args, **kwargs): @self.before_request def _before_flask_request(): + # Do not trace if the url is excluded + if _disable_trace(flask.request.url): + return environ = flask.request.environ span_name = ( flask.request.endpoint @@ -132,6 +140,9 @@ def _before_flask_request(): @self.teardown_request def _teardown_flask_request(exc): + # Not traced if the url is excluded + if _disable_trace(flask.request.url): + return activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) if not activation: logger.warning( @@ -150,6 +161,20 @@ def _teardown_flask_request(exc): context.detach(flask.request.environ.get(_ENVIRON_TOKEN)) +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): """A instrumentor for flask.Flask diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index 34432be3dd..1babfff2f5 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -13,12 +13,14 @@ # limitations under the License. import unittest +from unittest.mock import patch from flask import Flask, request from werkzeug.test import Client from werkzeug.wrappers import BaseResponse from opentelemetry import trace as trace_api +from opentelemetry.configuration import Configuration from opentelemetry.test.wsgitestutil import WsgiTestBase @@ -45,7 +47,8 @@ def setUp(self): # No instrumentation code is here because it is present in the # conftest.py file next to this file. super().setUp() - + Configuration._instance = None # pylint:disable=protected-access + Configuration.__slots__ = [] self.app = Flask(__name__) def hello_endpoint(helloid): @@ -53,10 +56,22 @@ def hello_endpoint(helloid): raise ValueError(":-(") return "Hello: " + str(helloid) + def excluded_endpoint(): + return "excluded" + + def excluded2_endpoint(): + return "excluded2" + self.app.route("/hello/")(hello_endpoint) + self.app.route("/excluded")(excluded_endpoint) + self.app.route("/excluded2")(excluded2_endpoint) self.client = Client(self.app, BaseResponse) + def tearDown(self): + Configuration._instance = None # pylint:disable=protected-access + Configuration.__slots__ = [] + def test_only_strings_in_environ(self): """ Some WSGI servers (such as Gunicorn) expect keys in the environ object @@ -80,9 +95,8 @@ def test_simple(self): expected_attrs = expected_attributes( {"http.target": "/hello/123", "http.route": "/hello/"} ) - resp = self.client.get("/hello/123") - self.assertEqual(200, resp.status_code) - self.assertEqual([b"Hello: 123"], list(resp.response)) + 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") @@ -126,6 +140,21 @@ def test_internal_error(self): self.assertEqual(span_list[0].kind, trace_api.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") + if __name__ == "__main__": unittest.main() diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index 8701d9ffba..48c350730e 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -11,9 +11,10 @@ # 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 Union +from typing import Sequence, Union from pkg_resources import iter_entry_points @@ -33,18 +34,39 @@ def time_ns() -> int: return int(time.time() * 1e9) -def _load_provider(provider: str) -> Union["TracerProvider", "MeterProvider"]: # type: ignore +def _load_provider( + provider: str, +) -> Union["TracerProvider", "MeterProvider"]: # type: ignore try: return next( # type: ignore iter_entry_points( "opentelemetry_{}".format(provider), - name=getattr( # type: ignore - Configuration(), provider, "default_{}".format(provider), # type: ignore + name=getattr( + Configuration(), # type: ignore + provider, + "default_{}".format(provider), ), ) ).load()() except Exception: # pylint: disable=broad-except - logger.error( - "Failed to load configured provider %s", provider, - ) + logger.error("Failed to load configured provider %s", 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): + return True + return False + + +def disable_tracing_hostname( + url: str, excluded_hostnames: Sequence[str] +) -> bool: + return url in excluded_hostnames