Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use request_ctx to determine whether or not _teardown_request should end flask span #1692

Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d11ff88
Use `id(flask.request)` instead of `thread.get_ident()`
matthewgrossman Feb 24, 2023
fb9161d
use reqctx instead?
matthewgrossman Feb 24, 2023
45198d7
try request ctx stack
matthewgrossman Mar 14, 2023
19f4c3c
get a functional test
matthewgrossman Mar 14, 2023
b173c05
add conditional based on package_version
matthewgrossman Mar 14, 2023
df2085d
loosen constraints
matthewgrossman Mar 14, 2023
fe1bd89
weakref, not id()
matthewgrossman Mar 14, 2023
6f7835a
lets pivot
matthewgrossman Mar 14, 2023
4bcc1df
clean up
matthewgrossman Mar 14, 2023
8e6905a
weakref version, attempting other branch now
matthewgrossman Mar 14, 2023
8d5a391
fix comment
matthewgrossman Apr 7, 2023
6b06283
fix type
matthewgrossman Apr 7, 2023
d82185e
fix comment
matthewgrossman Apr 7, 2023
282da72
add packaging to requirements
matthewgrossman Apr 7, 2023
3e1711c
Merge branch 'main' into mg-use-request-id-instead-of-thread-id
matthewgrossman Apr 7, 2023
1b8ab68
Add to tox.ini
matthewgrossman Apr 7, 2023
41a2eb7
push up again
matthewgrossman Apr 7, 2023
d599ac5
loosen dev requirements
matthewgrossman Apr 7, 2023
e573fea
Add to changelog
matthewgrossman Apr 7, 2023
7be29f3
lint
matthewgrossman Apr 7, 2023
1579401
Merge branch 'main' into mg-use-request-id-instead-of-thread-id
shalevr Apr 19, 2023
774c34c
Merge branch 'main' into mg-use-request-id-instead-of-thread-id
matthewgrossman May 24, 2023
7fdf90c
Merge branch 'mg-use-request-id-instead-of-thread-id' of github.com:m…
matthewgrossman May 24, 2023
72136d6
format
matthewgrossman May 24, 2023
103871b
format
matthewgrossman May 24, 2023
10b49de
isort
matthewgrossman May 24, 2023
76934fa
fix
matthewgrossman May 24, 2023
d5da755
fix markupsafe
matthewgrossman May 24, 2023
356cf20
black
matthewgrossman May 25, 2023
3098165
black
matthewgrossman May 25, 2023
5aa045c
Merge branch 'main' into mg-use-request-id-instead-of-thread-id
ocelotl Jun 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ instruments = [
]
test = [
"opentelemetry-instrumentation-flask[instruments]",
"markupsafe==2.0.1",
"flask >= 2.2.0",
matthewgrossman marked this conversation as resolved.
Show resolved Hide resolved
"markupsafe>=2.0.1",
"opentelemetry-test-utils == 0.38b0.dev",
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,11 @@ def response_hook(span: Span, status: str, response_headers: List):
---
"""
from logging import getLogger
from threading import get_ident
from packaging import version as package_version
from time import time_ns
from timeit import default_timer
from typing import Collection
import weakref

import flask

Expand All @@ -265,11 +266,17 @@ def response_hook(span: Span, status: str, response_headers: List):
_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key"
_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
_ENVIRON_THREAD_ID_KEY = "opentelemetry-flask.thread_id_key"
_ENVIRON_REQCTX_REF_KEY = "opentelemetry-flask.reqctx_ref_key"
_ENVIRON_TOKEN = "opentelemetry-flask.token"

_excluded_urls_from_env = get_excluded_urls("FLASK")

if package_version.parse(flask.__version__) >= package_version.parse("2.2.0"):
matthewgrossman marked this conversation as resolved.
Show resolved Hide resolved
def _request_ctx_ref() -> weakref.ReferenceType:
return weakref.ref(flask.globals.request_ctx._get_current_object())
matthewgrossman marked this conversation as resolved.
Show resolved Hide resolved
else:
def _request_ctx_ref() -> int:
return weakref.ref(flask._request_ctx_stack.top)

def get_default_span_name():
try:
Expand Down Expand Up @@ -399,7 +406,7 @@ def _before_request():
activation = trace.use_span(span, end_on_exit=True)
activation.__enter__() # pylint: disable=E1101
flask_request_environ[_ENVIRON_ACTIVATION_KEY] = activation
flask_request_environ[_ENVIRON_THREAD_ID_KEY] = get_ident()
flask_request_environ[_ENVIRON_REQCTX_REF_KEY] = _request_ctx_ref()
flask_request_environ[_ENVIRON_SPAN_KEY] = span
flask_request_environ[_ENVIRON_TOKEN] = token

Expand Down Expand Up @@ -439,17 +446,20 @@ def _teardown_request(exc):
return

activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
thread_id = flask.request.environ.get(_ENVIRON_THREAD_ID_KEY)
if not activation or thread_id != get_ident():

original_reqctx_ref = flask.request.environ.get(_ENVIRON_REQCTX_REF_KEY)
current_reqctx_ref = _request_ctx_ref()
if not activation or original_reqctx_ref != current_reqctx_ref:
# This request didn't start a span, maybe because it was created in
# a way that doesn't run `before_request`, like when it is created
# with `app.test_request_context`.
#
# Similarly, check the thread_id against the current thread to ensure
# tear down only happens on the original thread. This situation can
# arise if the original thread handling the request spawn children
# threads and then uses something like copy_current_request_context
# to copy the request context.
# Similarly, check that the request_ctx that created the span
# matches the current request_ctx, and only tear down if they match.
# This situation can arise if the original request_ctx handling
# the request calls functions that push new request_ctx's,
# like any decorated with `flask.copy_current_request_context`.

return
if exc is None:
activation.__exit__(None, None, None)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from werkzeug.wrappers import Response

from opentelemetry import context
from opentelemetry import trace


class InstrumentationTest:
Expand All @@ -37,6 +38,22 @@ def _sqlcommenter_endpoint():
)
return sqlcommenter_flask_values

@staticmethod
def _copy_context_endpoint():

@flask.copy_current_request_context
def _extract_header():
return flask.request.headers['x-req']

# Despite `_extract_header` copying the request context,
# calling it shouldn't detach the parent Flask span's contextvar
request_header = _extract_header()

return {
'span_name': trace.get_current_span().name,
'request_header': request_header
}

@staticmethod
def _multithreaded_endpoint(count):
def do_random_stuff():
Expand Down Expand Up @@ -84,6 +101,7 @@ def excluded2_endpoint():
self.app.route("/hello/<int:helloid>")(self._hello_endpoint)
self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint)
self.app.route("/multithreaded")(self._multithreaded_endpoint)
self.app.route("/copy_context")(self._copy_context_endpoint)
self.app.route("/excluded/<int:helloid>")(self._hello_endpoint)
self.app.route("/excluded")(excluded_endpoint)
self.app.route("/excluded2")(excluded2_endpoint)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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 flask
from werkzeug.test import Client
from werkzeug.wrappers import Response

from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.test.wsgitestutil import WsgiTestBase

# pylint: disable=import-error
from .base_test import InstrumentationTest


class TestCopyContext(InstrumentationTest, WsgiTestBase):
def setUp(self):
super().setUp()
FlaskInstrumentor().instrument()
self.app = flask.Flask(__name__)
self._common_initialization()

def tearDown(self):
super().tearDown()
with self.disable_logging():
FlaskInstrumentor().uninstrument()

def test_copycontext(self):
"""Test that instrumentation tear down does not blow up
when the request thread spawn children threads and the request
context is copied to the children threads
"""
self.app = flask.Flask(__name__)
self.app.route("/copy_context")(
self._copy_context_endpoint
)
client = Client(self.app, Response)
resp = client.get("/copy_context", headers={'x-req': 'a-header'})

self.assertEqual(200, resp.status_code)
self.assertEqual('/copy_context', resp.json['span_name'])
self.assertEqual('a-header', resp.json['request_header'])