Skip to content

Commit bcbd21f

Browse files
committed
Provide excluded_urls argument to Flask instrumentation
The FastAPI instrumentation accepts an 'explicit_urls' argument on initialization. This commit translates that concept to the Flask instrumentation.
1 parent f70af95 commit bcbd21f

File tree

4 files changed

+105
-39
lines changed

4 files changed

+105
-39
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.4.0-0.23b0...HEAD)
99

10+
### Changed
11+
- Enable explicit excluded\_urls `opentelemetry-instrumentation-flask`
12+
([#604](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/604))
13+
1014
## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-21
1115

1216
### Removed

instrumentation/opentelemetry-instrumentation-flask/README.rst

+6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ For example,
3131

3232
will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
3333

34+
You can also pass the comma delimited regexes to the ``instrument_app`` method directly:
35+
36+
.. code-block:: python
37+
38+
FlaskInstrumentor().instrument_app(app, excluded_urls="client/.*/info,healthcheck")
39+
3440
References
3541
----------
3642

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

+60-26
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def hello():
6363
from opentelemetry.propagate import extract
6464
from opentelemetry.semconv.trace import SpanAttributes
6565
from opentelemetry.util._time import _time_ns
66-
from opentelemetry.util.http import get_excluded_urls
66+
from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls
6767

6868
_logger = getLogger(__name__)
6969

@@ -73,7 +73,7 @@ def hello():
7373
_ENVIRON_TOKEN = "opentelemetry-flask.token"
7474

7575

76-
_excluded_urls = get_excluded_urls("FLASK")
76+
_excluded_urls_from_env = get_excluded_urls("FLASK")
7777

7878

7979
def get_default_span_name():
@@ -85,7 +85,7 @@ def get_default_span_name():
8585
return span_name
8686

8787

88-
def _rewrapped_app(wsgi_app, response_hook=None):
88+
def _rewrapped_app(wsgi_app, response_hook=None, excluded_urls=None):
8989
def _wrapped_app(wrapped_app_environ, start_response):
9090
# We want to measure the time for route matching, etc.
9191
# In theory, we could start the span here and use
@@ -94,7 +94,9 @@ def _wrapped_app(wrapped_app_environ, start_response):
9494
wrapped_app_environ[_ENVIRON_STARTTIME_KEY] = _time_ns()
9595

9696
def _start_response(status, response_headers, *args, **kwargs):
97-
if not _excluded_urls.url_disabled(flask.request.url):
97+
if excluded_urls is None or not excluded_urls.url_disabled(
98+
flask.request.url
99+
):
98100
span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
99101

100102
propagator = get_global_response_propagator()
@@ -123,9 +125,11 @@ def _start_response(status, response_headers, *args, **kwargs):
123125
return _wrapped_app
124126

125127

126-
def _wrapped_before_request(request_hook=None, tracer=None):
128+
def _wrapped_before_request(
129+
request_hook=None, tracer=None, excluded_urls=None
130+
):
127131
def _before_request():
128-
if _excluded_urls.url_disabled(flask.request.url):
132+
if excluded_urls and excluded_urls.url_disabled(flask.request.url):
129133
return
130134
flask_request_environ = flask.request.environ
131135
span_name = get_default_span_name()
@@ -163,29 +167,33 @@ def _before_request():
163167
return _before_request
164168

165169

166-
def _teardown_request(exc):
167-
# pylint: disable=E1101
168-
if _excluded_urls.url_disabled(flask.request.url):
169-
return
170+
def _wrapped_teardown_request(excluded_urls=None):
171+
def _teardown_request(exc):
172+
# pylint: disable=E1101
173+
if excluded_urls and excluded_urls.url_disabled(flask.request.url):
174+
return
170175

171-
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
172-
if not activation:
173-
# This request didn't start a span, maybe because it was created in a
174-
# way that doesn't run `before_request`, like when it is created with
175-
# `app.test_request_context`.
176-
return
176+
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
177+
if not activation:
178+
# This request didn't start a span, maybe because it was created in
179+
# a way that doesn't run `before_request`, like when it is created
180+
# with `app.test_request_context`.
181+
return
177182

178-
if exc is None:
179-
activation.__exit__(None, None, None)
180-
else:
181-
activation.__exit__(
182-
type(exc), exc, getattr(exc, "__traceback__", None)
183-
)
184-
context.detach(flask.request.environ.get(_ENVIRON_TOKEN))
183+
if exc is None:
184+
activation.__exit__(None, None, None)
185+
else:
186+
activation.__exit__(
187+
type(exc), exc, getattr(exc, "__traceback__", None)
188+
)
189+
context.detach(flask.request.environ.get(_ENVIRON_TOKEN))
190+
191+
return _teardown_request
185192

186193

187194
class _InstrumentedFlask(flask.Flask):
188195

196+
_excluded_urls = None
189197
_tracer_provider = None
190198
_request_hook = None
191199
_response_hook = None
@@ -209,6 +217,10 @@ def __init__(self, *args, **kwargs):
209217
)
210218
self._before_request = _before_request
211219
self.before_request(_before_request)
220+
221+
_teardown_request = _wrapped_teardown_request(
222+
excluded_urls=_InstrumentedFlask._excluded_urls,
223+
)
212224
self.teardown_request(_teardown_request)
213225

214226

@@ -232,27 +244,49 @@ def _instrument(self, **kwargs):
232244
_InstrumentedFlask._response_hook = response_hook
233245
tracer_provider = kwargs.get("tracer_provider")
234246
_InstrumentedFlask._tracer_provider = tracer_provider
247+
excluded_urls = kwargs.get("excluded_urls")
248+
_InstrumentedFlask._excluded_urls = (
249+
_excluded_urls_from_env
250+
if excluded_urls is None
251+
else parse_excluded_urls(excluded_urls)
252+
)
235253
flask.Flask = _InstrumentedFlask
236254

237255
def _uninstrument(self, **kwargs):
238256
flask.Flask = self._original_flask
239257

240258
@staticmethod
241259
def instrument_app(
242-
app, request_hook=None, response_hook=None, tracer_provider=None
260+
app,
261+
request_hook=None,
262+
response_hook=None,
263+
tracer_provider=None,
264+
excluded_urls=None,
243265
):
244266
if not hasattr(app, "_is_instrumented_by_opentelemetry"):
245267
app._is_instrumented_by_opentelemetry = False
246268

247269
if not app._is_instrumented_by_opentelemetry:
270+
excluded_urls = (
271+
parse_excluded_urls(excluded_urls)
272+
if excluded_urls is not None
273+
else _excluded_urls_from_env
274+
)
248275
app._original_wsgi_app = app.wsgi_app
249276
app.wsgi_app = _rewrapped_app(app.wsgi_app, response_hook)
250277

251278
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
252279

253-
_before_request = _wrapped_before_request(request_hook, tracer)
280+
_before_request = _wrapped_before_request(
281+
request_hook, tracer, excluded_urls=excluded_urls,
282+
)
254283
app._before_request = _before_request
255284
app.before_request(_before_request)
285+
286+
_teardown_request = _wrapped_teardown_request(
287+
excluded_urls=excluded_urls,
288+
)
289+
app._teardown_request = _teardown_request
256290
app.teardown_request(_teardown_request)
257291
app._is_instrumented_by_opentelemetry = True
258292
else:
@@ -267,7 +301,7 @@ def uninstrument_app(app):
267301

268302
# FIXME add support for other Flask blueprints that are not None
269303
app.before_request_funcs[None].remove(app._before_request)
270-
app.teardown_request_funcs[None].remove(_teardown_request)
304+
app.teardown_request_funcs[None].remove(app._teardown_request)
271305
del app._original_wsgi_app
272306
app._is_instrumented_by_opentelemetry = False
273307
else:

instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py

+35-13
Original file line numberDiff line numberDiff line change
@@ -53,25 +53,25 @@ class TestProgrammatic(InstrumentationTest, TestBase, WsgiTestBase):
5353
def setUp(self):
5454
super().setUp()
5555

56-
self.app = Flask(__name__)
57-
58-
FlaskInstrumentor().instrument_app(self.app)
59-
60-
self._common_initialization()
61-
6256
self.env_patch = patch.dict(
6357
"os.environ",
6458
{
65-
"OTEL_PYTHON_FLASK_EXCLUDED_URLS": "http://localhost/excluded_arg/123,excluded_noarg"
59+
"OTEL_PYTHON_FLASK_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg"
6660
},
6761
)
6862
self.env_patch.start()
63+
6964
self.exclude_patch = patch(
70-
"opentelemetry.instrumentation.flask._excluded_urls",
65+
"opentelemetry.instrumentation.flask._excluded_urls_from_env",
7166
get_excluded_urls("FLASK"),
7267
)
7368
self.exclude_patch.start()
7469

70+
self.app = Flask(__name__)
71+
FlaskInstrumentor().instrument_app(self.app)
72+
73+
self._common_initialization()
74+
7575
def tearDown(self):
7676
super().tearDown()
7777
self.env_patch.stop()
@@ -221,20 +221,42 @@ def test_internal_error(self):
221221
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
222222
self.assertEqual(span_list[0].attributes, expected_attrs)
223223

224-
def test_exclude_lists(self):
225-
self.client.get("/excluded_arg/123")
224+
def test_exclude_lists_from_env(self):
225+
self.client.get("/env_excluded_arg/123")
226+
span_list = self.memory_exporter.get_finished_spans()
227+
self.assertEqual(len(span_list), 0)
228+
229+
self.client.get("/env_excluded_arg/125")
230+
span_list = self.memory_exporter.get_finished_spans()
231+
self.assertEqual(len(span_list), 1)
232+
233+
self.client.get("/env_excluded_noarg")
234+
span_list = self.memory_exporter.get_finished_spans()
235+
self.assertEqual(len(span_list), 1)
236+
237+
self.client.get("/env_excluded_noarg2")
238+
span_list = self.memory_exporter.get_finished_spans()
239+
self.assertEqual(len(span_list), 1)
240+
241+
def test_exclude_lists_from_explicit(self):
242+
excluded_urls = "http://localhost/explicit_excluded_arg/123,explicit_excluded_noarg"
243+
app = Flask(__name__)
244+
FlaskInstrumentor().instrument_app(app, excluded_urls=excluded_urls)
245+
client = app.test_client()
246+
247+
client.get("/explicit_excluded_arg/123")
226248
span_list = self.memory_exporter.get_finished_spans()
227249
self.assertEqual(len(span_list), 0)
228250

229-
self.client.get("/excluded_arg/125")
251+
client.get("/explicit_excluded_arg/125")
230252
span_list = self.memory_exporter.get_finished_spans()
231253
self.assertEqual(len(span_list), 1)
232254

233-
self.client.get("/excluded_noarg")
255+
client.get("/explicit_excluded_noarg")
234256
span_list = self.memory_exporter.get_finished_spans()
235257
self.assertEqual(len(span_list), 1)
236258

237-
self.client.get("/excluded_noarg2")
259+
client.get("/explicit_excluded_noarg2")
238260
span_list = self.memory_exporter.get_finished_spans()
239261
self.assertEqual(len(span_list), 1)
240262

0 commit comments

Comments
 (0)