Skip to content

Commit 14b0a3f

Browse files
authored
Provide excluded_urls argument to Flask instrumentation (#604)
1 parent a04fb0e commit 14b0a3f

File tree

5 files changed

+134
-42
lines changed

5 files changed

+134
-42
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
`opentelemetry-instrumentation-starlette`, `opentelemetry-instrumentation-urllib`, `opentelemetry-instrumentation-urllib3` Added `request_hook` and `response_hook` callbacks
1515
([#576](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/576))
1616

17+
### Changed
18+
- Enable explicit `excluded_urls` argument in `opentelemetry-instrumentation-flask`
19+
([#604](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/604))
20+
1721
## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-21
1822

1923
### 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

+69-29
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
@@ -197,18 +205,26 @@ def __init__(self, *args, **kwargs):
197205
self._is_instrumented_by_opentelemetry = True
198206

199207
self.wsgi_app = _rewrapped_app(
200-
self.wsgi_app, _InstrumentedFlask._response_hook
208+
self.wsgi_app,
209+
_InstrumentedFlask._response_hook,
210+
excluded_urls=_InstrumentedFlask._excluded_urls,
201211
)
202212

203213
tracer = trace.get_tracer(
204214
__name__, __version__, _InstrumentedFlask._tracer_provider
205215
)
206216

207217
_before_request = _wrapped_before_request(
208-
_InstrumentedFlask._request_hook, tracer,
218+
_InstrumentedFlask._request_hook,
219+
tracer,
220+
excluded_urls=_InstrumentedFlask._excluded_urls,
209221
)
210222
self._before_request = _before_request
211223
self.before_request(_before_request)
224+
225+
_teardown_request = _wrapped_teardown_request(
226+
excluded_urls=_InstrumentedFlask._excluded_urls,
227+
)
212228
self.teardown_request(_teardown_request)
213229

214230

@@ -232,27 +248,51 @@ def _instrument(self, **kwargs):
232248
_InstrumentedFlask._response_hook = response_hook
233249
tracer_provider = kwargs.get("tracer_provider")
234250
_InstrumentedFlask._tracer_provider = tracer_provider
251+
excluded_urls = kwargs.get("excluded_urls")
252+
_InstrumentedFlask._excluded_urls = (
253+
_excluded_urls_from_env
254+
if excluded_urls is None
255+
else parse_excluded_urls(excluded_urls)
256+
)
235257
flask.Flask = _InstrumentedFlask
236258

237259
def _uninstrument(self, **kwargs):
238260
flask.Flask = self._original_flask
239261

240262
@staticmethod
241263
def instrument_app(
242-
app, request_hook=None, response_hook=None, tracer_provider=None
264+
app,
265+
request_hook=None,
266+
response_hook=None,
267+
tracer_provider=None,
268+
excluded_urls=None,
243269
):
244270
if not hasattr(app, "_is_instrumented_by_opentelemetry"):
245271
app._is_instrumented_by_opentelemetry = False
246272

247273
if not app._is_instrumented_by_opentelemetry:
274+
excluded_urls = (
275+
parse_excluded_urls(excluded_urls)
276+
if excluded_urls is not None
277+
else _excluded_urls_from_env
278+
)
248279
app._original_wsgi_app = app.wsgi_app
249-
app.wsgi_app = _rewrapped_app(app.wsgi_app, response_hook)
280+
app.wsgi_app = _rewrapped_app(
281+
app.wsgi_app, response_hook, excluded_urls=excluded_urls
282+
)
250283

251284
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
252285

253-
_before_request = _wrapped_before_request(request_hook, tracer)
286+
_before_request = _wrapped_before_request(
287+
request_hook, tracer, excluded_urls=excluded_urls,
288+
)
254289
app._before_request = _before_request
255290
app.before_request(_before_request)
291+
292+
_teardown_request = _wrapped_teardown_request(
293+
excluded_urls=excluded_urls,
294+
)
295+
app._teardown_request = _teardown_request
256296
app.teardown_request(_teardown_request)
257297
app._is_instrumented_by_opentelemetry = True
258298
else:
@@ -267,7 +307,7 @@ def uninstrument_app(app):
267307

268308
# FIXME add support for other Flask blueprints that are not None
269309
app.before_request_funcs[None].remove(app._before_request)
270-
app.teardown_request_funcs[None].remove(_teardown_request)
310+
app.teardown_request_funcs[None].remove(app._teardown_request)
271311
del app._original_wsgi_app
272312
app._is_instrumented_by_opentelemetry = False
273313
else:

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

+20
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,23 @@ def test_uninstrument(self):
5959
self.assertEqual([b"Hello: 123"], list(resp.response))
6060
span_list = self.memory_exporter.get_finished_spans()
6161
self.assertEqual(len(span_list), 1)
62+
63+
def test_exluded_urls_explicit(self):
64+
FlaskInstrumentor().uninstrument()
65+
FlaskInstrumentor().instrument(excluded_urls="/hello/456")
66+
67+
self.app = flask.Flask(__name__)
68+
self.app.route("/hello/<int:helloid>")(self._hello_endpoint)
69+
client = Client(self.app, BaseResponse)
70+
71+
resp = client.get("/hello/123")
72+
self.assertEqual(200, resp.status_code)
73+
self.assertEqual([b"Hello: 123"], list(resp.response))
74+
span_list = self.memory_exporter.get_finished_spans()
75+
self.assertEqual(len(span_list), 1)
76+
77+
resp = client.get("/hello/456")
78+
self.assertEqual(200, resp.status_code)
79+
self.assertEqual([b"Hello: 456"], list(resp.response))
80+
span_list = self.memory_exporter.get_finished_spans()
81+
self.assertEqual(len(span_list), 1)

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)