Skip to content

Commit 4637912

Browse files
matthewgrossmanshalevrocelotl
authored
Use request_ctx to determine whether or not _teardown_request should end flask span (#1692)
Co-authored-by: Shalev Roda <[email protected]> Co-authored-by: Diego Hurtado <[email protected]>
1 parent fc54787 commit 4637912

File tree

7 files changed

+104
-19
lines changed

7 files changed

+104
-19
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4444
([#1738](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1738))
4545
- Fix `None does not implement middleware` error when there are no middlewares registered
4646
([#1766](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1766))
47+
- Fix Flask instrumentation to only close the span if it was created by the same request context.
48+
([#1692](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1692))
4749

4850
## Version 1.17.0/0.38b0 (2023-03-22)
4951

dev-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ bleach==4.1.0 # transient dependency for readme-renderer
1414
grpcio-tools==1.29.0
1515
mypy-protobuf>=1.23
1616
protobuf~=3.13
17-
markupsafe==2.0.1
17+
markupsafe>=2.0.1
1818
codespell==2.1.0
1919
requests==2.28.1
2020
ruamel.yaml==0.17.21

instrumentation/opentelemetry-instrumentation-flask/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies = [
3030
"opentelemetry-instrumentation-wsgi == 0.40b0.dev",
3131
"opentelemetry-semantic-conventions == 0.40b0.dev",
3232
"opentelemetry-util-http == 0.40b0.dev",
33+
"packaging >= 21.0",
3334
]
3435

3536
[project.optional-dependencies]
@@ -38,7 +39,7 @@ instruments = [
3839
]
3940
test = [
4041
"opentelemetry-instrumentation-flask[instruments]",
41-
"markupsafe==2.0.1",
42+
"markupsafe==2.1.2",
4243
"opentelemetry-test-utils == 0.40b0.dev",
4344
]
4445

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -238,13 +238,14 @@ def response_hook(span: Span, status: str, response_headers: List):
238238
API
239239
---
240240
"""
241+
import weakref
241242
from logging import getLogger
242-
from threading import get_ident
243243
from time import time_ns
244244
from timeit import default_timer
245245
from typing import Collection
246246

247247
import flask
248+
from packaging import version as package_version
248249

249250
import opentelemetry.instrumentation.wsgi as otel_wsgi
250251
from opentelemetry import context, trace
@@ -265,11 +266,21 @@ def response_hook(span: Span, status: str, response_headers: List):
265266
_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
266267
_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key"
267268
_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
268-
_ENVIRON_THREAD_ID_KEY = "opentelemetry-flask.thread_id_key"
269+
_ENVIRON_REQCTX_REF_KEY = "opentelemetry-flask.reqctx_ref_key"
269270
_ENVIRON_TOKEN = "opentelemetry-flask.token"
270271

271272
_excluded_urls_from_env = get_excluded_urls("FLASK")
272273

274+
if package_version.parse(flask.__version__) >= package_version.parse("2.2.0"):
275+
276+
def _request_ctx_ref() -> weakref.ReferenceType:
277+
return weakref.ref(flask.globals.request_ctx._get_current_object())
278+
279+
else:
280+
281+
def _request_ctx_ref() -> weakref.ReferenceType:
282+
return weakref.ref(flask._request_ctx_stack.top)
283+
273284

274285
def get_default_span_name():
275286
try:
@@ -399,7 +410,7 @@ def _before_request():
399410
activation = trace.use_span(span, end_on_exit=True)
400411
activation.__enter__() # pylint: disable=E1101
401412
flask_request_environ[_ENVIRON_ACTIVATION_KEY] = activation
402-
flask_request_environ[_ENVIRON_THREAD_ID_KEY] = get_ident()
413+
flask_request_environ[_ENVIRON_REQCTX_REF_KEY] = _request_ctx_ref()
403414
flask_request_environ[_ENVIRON_SPAN_KEY] = span
404415
flask_request_environ[_ENVIRON_TOKEN] = token
405416

@@ -439,17 +450,22 @@ def _teardown_request(exc):
439450
return
440451

441452
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
442-
thread_id = flask.request.environ.get(_ENVIRON_THREAD_ID_KEY)
443-
if not activation or thread_id != get_ident():
453+
454+
original_reqctx_ref = flask.request.environ.get(
455+
_ENVIRON_REQCTX_REF_KEY
456+
)
457+
current_reqctx_ref = _request_ctx_ref()
458+
if not activation or original_reqctx_ref != current_reqctx_ref:
444459
# This request didn't start a span, maybe because it was created in
445460
# a way that doesn't run `before_request`, like when it is created
446461
# with `app.test_request_context`.
447462
#
448-
# Similarly, check the thread_id against the current thread to ensure
449-
# tear down only happens on the original thread. This situation can
450-
# arise if the original thread handling the request spawn children
451-
# threads and then uses something like copy_current_request_context
452-
# to copy the request context.
463+
# Similarly, check that the request_ctx that created the span
464+
# matches the current request_ctx, and only tear down if they match.
465+
# This situation can arise if the original request_ctx handling
466+
# the request calls functions that push new request_ctx's,
467+
# like any decorated with `flask.copy_current_request_context`.
468+
453469
return
454470
if exc is None:
455471
activation.__exit__(None, None, None)

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from werkzeug.test import Client
2020
from werkzeug.wrappers import Response
2121

22-
from opentelemetry import context
22+
from opentelemetry import context, trace
2323

2424

2525
class InstrumentationTest:
@@ -37,6 +37,21 @@ def _sqlcommenter_endpoint():
3737
)
3838
return sqlcommenter_flask_values
3939

40+
@staticmethod
41+
def _copy_context_endpoint():
42+
@flask.copy_current_request_context
43+
def _extract_header():
44+
return flask.request.headers["x-req"]
45+
46+
# Despite `_extract_header` copying the request context,
47+
# calling it shouldn't detach the parent Flask span's contextvar
48+
request_header = _extract_header()
49+
50+
return {
51+
"span_name": trace.get_current_span().name,
52+
"request_header": request_header,
53+
}
54+
4055
@staticmethod
4156
def _multithreaded_endpoint(count):
4257
def do_random_stuff():
@@ -84,6 +99,7 @@ def excluded2_endpoint():
8499
self.app.route("/hello/<int:helloid>")(self._hello_endpoint)
85100
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
86101
self.app.route("/multithreaded")(self._multithreaded_endpoint)
102+
self.app.route("/copy_context")(self._copy_context_endpoint)
87103
self.app.route("/excluded/<int:helloid>")(self._hello_endpoint)
88104
self.app.route("/excluded")(excluded_endpoint)
89105
self.app.route("/excluded2")(excluded2_endpoint)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import flask
15+
from werkzeug.test import Client
16+
from werkzeug.wrappers import Response
17+
18+
from opentelemetry.instrumentation.flask import FlaskInstrumentor
19+
from opentelemetry.test.wsgitestutil import WsgiTestBase
20+
21+
from .base_test import InstrumentationTest
22+
23+
24+
class TestCopyContext(InstrumentationTest, WsgiTestBase):
25+
def setUp(self):
26+
super().setUp()
27+
FlaskInstrumentor().instrument()
28+
self.app = flask.Flask(__name__)
29+
self._common_initialization()
30+
31+
def tearDown(self):
32+
super().tearDown()
33+
with self.disable_logging():
34+
FlaskInstrumentor().uninstrument()
35+
36+
def test_copycontext(self):
37+
"""Test that instrumentation tear down does not blow up
38+
when the request calls functions where the context has been
39+
copied via `flask.copy_current_request_context`
40+
"""
41+
self.app = flask.Flask(__name__)
42+
self.app.route("/copy_context")(self._copy_context_endpoint)
43+
client = Client(self.app, Response)
44+
resp = client.get("/copy_context", headers={"x-req": "a-header"})
45+
46+
self.assertEqual(200, resp.status_code)
47+
self.assertEqual("/copy_context", resp.json["span_name"])
48+
self.assertEqual("a-header", resp.json["request_header"])

tox.ini

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ envlist =
8484
pypy3-test-instrumentation-fastapi
8585

8686
; opentelemetry-instrumentation-flask
87-
py3{7,8,9,10,11}-test-instrumentation-flask
88-
pypy3-test-instrumentation-flask
87+
py3{7,8,9,10,11}-test-instrumentation-flask{213,220}
88+
pypy3-test-instrumentation-flask{213,220}
8989

9090
; opentelemetry-instrumentation-urllib
9191
py3{7,8,9,10,11}-test-instrumentation-urllib
@@ -258,6 +258,8 @@ deps =
258258
falcon1: falcon ==1.4.1
259259
falcon2: falcon >=2.0.0,<3.0.0
260260
falcon3: falcon >=3.0.0,<4.0.0
261+
flask213: Flask ==2.1.3
262+
flask220: Flask >=2.2.0
261263
grpc: pytest-asyncio
262264
sqlalchemy11: sqlalchemy>=1.1,<1.2
263265
sqlalchemy14: aiosqlite
@@ -304,7 +306,7 @@ changedir =
304306
test-instrumentation-elasticsearch{2,5,6}: instrumentation/opentelemetry-instrumentation-elasticsearch/tests
305307
test-instrumentation-falcon{1,2,3}: instrumentation/opentelemetry-instrumentation-falcon/tests
306308
test-instrumentation-fastapi: instrumentation/opentelemetry-instrumentation-fastapi/tests
307-
test-instrumentation-flask: instrumentation/opentelemetry-instrumentation-flask/tests
309+
test-instrumentation-flask{213,220}: instrumentation/opentelemetry-instrumentation-flask/tests
308310
test-instrumentation-urllib: instrumentation/opentelemetry-instrumentation-urllib/tests
309311
test-instrumentation-urllib3: instrumentation/opentelemetry-instrumentation-urllib3/tests
310312
test-instrumentation-grpc: instrumentation/opentelemetry-instrumentation-grpc/tests
@@ -365,8 +367,8 @@ commands_pre =
365367

366368
grpc: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test]
367369

368-
falcon{1,2,3},flask,django{1,2,3,4},pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,urllib3,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
369-
wsgi,falcon{1,2,3},flask,django{1,2,3,4},pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
370+
falcon{1,2,3},flask{213,220},django{1,2,3,4},pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,urllib3,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
371+
wsgi,falcon{1,2,3},flask{213,220},django{1,2,3,4},pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
370372
asgi,django{3,4},starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test]
371373

372374
asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test]
@@ -380,7 +382,7 @@ commands_pre =
380382

381383
falcon{1,2,3}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test]
382384

383-
flask: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test]
385+
flask{213,220}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test]
384386

385387
urllib: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib[test]
386388

0 commit comments

Comments
 (0)