Skip to content

Commit 1d84ee9

Browse files
authored
flask: Add exclude lists for flask integration (#630)
Leverage global configurations to allow users to specify paths and hosts that they do not want to trace within their Flask applications. OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS and OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS are the env variables used. Use a comma delimited string to represent seperate hosts/paths to blacklist.
1 parent 6babff1 commit 1d84ee9

File tree

5 files changed

+114
-24
lines changed

5 files changed

+114
-24
lines changed

ext/opentelemetry-ext-flask/CHANGELOG.md

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

33
## Unreleased
44

5+
- Add exclude list for paths and hosts
6+
([#630](https://github.com/open-telemetry/opentelemetry-python/pull/630))
7+
58
## 0.6b0
69

710
Released 2020-03-30

ext/opentelemetry-ext-flask/README.rst

+11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ Installation
1616

1717
pip install opentelemetry-ext-flask
1818

19+
Configuration
20+
-------------
21+
22+
Exclude lists
23+
*************
24+
Excludes certain hosts and paths from being tracked. Pass in comma delimited string into environment variables.
25+
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.
26+
27+
Excluded hosts: OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS
28+
Excluded paths: OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS
29+
1930

2031
References
2132
----------

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

+38-13
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,14 @@ def hello():
5151
import flask
5252

5353
import opentelemetry.ext.wsgi as otel_wsgi
54-
from opentelemetry import context, propagators, trace
54+
from opentelemetry import configuration, context, propagators, trace
5555
from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor
5656
from opentelemetry.ext.flask.version import __version__
57-
from opentelemetry.util import time_ns
57+
from opentelemetry.util import (
58+
disable_tracing_hostname,
59+
disable_tracing_path,
60+
time_ns,
61+
)
5862

5963
logger = logging.getLogger(__name__)
6064

@@ -80,17 +84,18 @@ def wrapped_app(environ, start_response):
8084
environ[_ENVIRON_STARTTIME_KEY] = time_ns()
8185

8286
def _start_response(status, response_headers, *args, **kwargs):
83-
span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
84-
if span:
85-
otel_wsgi.add_response_attributes(
86-
span, status, response_headers
87-
)
88-
else:
89-
logger.warning(
90-
"Flask environ's OpenTelemetry span "
91-
"missing at _start_response(%s)",
92-
status,
93-
)
87+
if not _disable_trace(flask.request.url):
88+
span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
89+
if span:
90+
otel_wsgi.add_response_attributes(
91+
span, status, response_headers
92+
)
93+
else:
94+
logger.warning(
95+
"Flask environ's OpenTelemetry span "
96+
"missing at _start_response(%s)",
97+
status,
98+
)
9499

95100
return start_response(
96101
status, response_headers, *args, **kwargs
@@ -102,6 +107,9 @@ def _start_response(status, response_headers, *args, **kwargs):
102107

103108
@self.before_request
104109
def _before_flask_request():
110+
# Do not trace if the url is excluded
111+
if _disable_trace(flask.request.url):
112+
return
105113
environ = flask.request.environ
106114
span_name = (
107115
flask.request.endpoint
@@ -132,6 +140,9 @@ def _before_flask_request():
132140

133141
@self.teardown_request
134142
def _teardown_flask_request(exc):
143+
# Not traced if the url is excluded
144+
if _disable_trace(flask.request.url):
145+
return
135146
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
136147
if not activation:
137148
logger.warning(
@@ -150,6 +161,20 @@ def _teardown_flask_request(exc):
150161
context.detach(flask.request.environ.get(_ENVIRON_TOKEN))
151162

152163

164+
def _disable_trace(url):
165+
excluded_hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS
166+
excluded_paths = configuration.Configuration().FLASK_EXCLUDED_PATHS
167+
if excluded_hosts:
168+
excluded_hosts = str.split(excluded_hosts, ",")
169+
if disable_tracing_hostname(url, excluded_hosts):
170+
return True
171+
if excluded_paths:
172+
excluded_paths = str.split(excluded_paths, ",")
173+
if disable_tracing_path(url, excluded_paths):
174+
return True
175+
return False
176+
177+
153178
class FlaskInstrumentor(BaseInstrumentor):
154179
"""A instrumentor for flask.Flask
155180

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

+33-4
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
# limitations under the License.
1414

1515
import unittest
16+
from unittest.mock import patch
1617

1718
from flask import Flask, request
1819
from werkzeug.test import Client
1920
from werkzeug.wrappers import BaseResponse
2021

2122
from opentelemetry import trace as trace_api
23+
from opentelemetry.configuration import Configuration
2224
from opentelemetry.test.wsgitestutil import WsgiTestBase
2325

2426

@@ -45,18 +47,31 @@ def setUp(self):
4547
# No instrumentation code is here because it is present in the
4648
# conftest.py file next to this file.
4749
super().setUp()
48-
50+
Configuration._instance = None # pylint:disable=protected-access
51+
Configuration.__slots__ = []
4952
self.app = Flask(__name__)
5053

5154
def hello_endpoint(helloid):
5255
if helloid == 500:
5356
raise ValueError(":-(")
5457
return "Hello: " + str(helloid)
5558

59+
def excluded_endpoint():
60+
return "excluded"
61+
62+
def excluded2_endpoint():
63+
return "excluded2"
64+
5665
self.app.route("/hello/<int:helloid>")(hello_endpoint)
66+
self.app.route("/excluded")(excluded_endpoint)
67+
self.app.route("/excluded2")(excluded2_endpoint)
5768

5869
self.client = Client(self.app, BaseResponse)
5970

71+
def tearDown(self):
72+
Configuration._instance = None # pylint:disable=protected-access
73+
Configuration.__slots__ = []
74+
6075
def test_only_strings_in_environ(self):
6176
"""
6277
Some WSGI servers (such as Gunicorn) expect keys in the environ object
@@ -80,9 +95,8 @@ def test_simple(self):
8095
expected_attrs = expected_attributes(
8196
{"http.target": "/hello/123", "http.route": "/hello/<int:helloid>"}
8297
)
83-
resp = self.client.get("/hello/123")
84-
self.assertEqual(200, resp.status_code)
85-
self.assertEqual([b"Hello: 123"], list(resp.response))
98+
self.client.get("/hello/123")
99+
86100
span_list = self.memory_exporter.get_finished_spans()
87101
self.assertEqual(len(span_list), 1)
88102
self.assertEqual(span_list[0].name, "hello_endpoint")
@@ -126,6 +140,21 @@ def test_internal_error(self):
126140
self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER)
127141
self.assertEqual(span_list[0].attributes, expected_attrs)
128142

143+
@patch.dict(
144+
"os.environ", # type: ignore
145+
{
146+
"OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_HOSTS": "http://localhost/excluded",
147+
"OPENTELEMETRY_PYTHON_FLASK_EXCLUDED_PATHS": "excluded2",
148+
},
149+
)
150+
def test_excluded_path(self):
151+
self.client.get("/hello/123")
152+
self.client.get("/excluded")
153+
self.client.get("/excluded2")
154+
span_list = self.memory_exporter.get_finished_spans()
155+
self.assertEqual(len(span_list), 1)
156+
self.assertEqual(span_list[0].name, "hello_endpoint")
157+
129158

130159
if __name__ == "__main__":
131160
unittest.main()

opentelemetry-api/src/opentelemetry/util/__init__.py

+29-7
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
import re
1415
import time
1516
from logging import getLogger
16-
from typing import Union
17+
from typing import Sequence, Union
1718

1819
from pkg_resources import iter_entry_points
1920

@@ -33,18 +34,39 @@ def time_ns() -> int:
3334
return int(time.time() * 1e9)
3435

3536

36-
def _load_provider(provider: str) -> Union["TracerProvider", "MeterProvider"]: # type: ignore
37+
def _load_provider(
38+
provider: str,
39+
) -> Union["TracerProvider", "MeterProvider"]: # type: ignore
3740
try:
3841
return next( # type: ignore
3942
iter_entry_points(
4043
"opentelemetry_{}".format(provider),
41-
name=getattr( # type: ignore
42-
Configuration(), provider, "default_{}".format(provider), # type: ignore
44+
name=getattr(
45+
Configuration(), # type: ignore
46+
provider,
47+
"default_{}".format(provider),
4348
),
4449
)
4550
).load()()
4651
except Exception: # pylint: disable=broad-except
47-
logger.error(
48-
"Failed to load configured provider %s", provider,
49-
)
52+
logger.error("Failed to load configured provider %s", provider)
5053
raise
54+
55+
56+
# Pattern for matching up until the first '/' after the 'https://' part.
57+
_URL_PATTERN = r"(https?|ftp)://.*?/"
58+
59+
60+
def disable_tracing_path(url: str, excluded_paths: Sequence[str]) -> bool:
61+
if excluded_paths:
62+
# Match only the part after the first '/' that is not in _URL_PATTERN
63+
regex = "{}({})".format(_URL_PATTERN, "|".join(excluded_paths))
64+
if re.match(regex, url):
65+
return True
66+
return False
67+
68+
69+
def disable_tracing_hostname(
70+
url: str, excluded_hostnames: Sequence[str]
71+
) -> bool:
72+
return url in excluded_hostnames

0 commit comments

Comments
 (0)