Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 28 additions & 0 deletions src/instana/instrumentation/werkzeug.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,34 @@ def run_simple_with_instana(

return wrapped(*args, **kwargs)

@wrapt.patch_function_wrapper("werkzeug.serving", "BaseWSGIServer.__init__")
def base_wsgi_server_init_with_instana(
wrapped: Callable,
instance: Any,
args: tuple,
kwargs: dict[str, Any],
) -> Any:
"""
Patch werkzeug.serving.BaseWSGIServer.__init__ to wrap WSGI applications.

Covers frameworks like Odoo that instantiate BaseWSGIServer (or its
subclasses such as ThreadedWSGIServer) directly without going through
run_simple. The app is wrapped after super().__init__ so that any
subclass setup that reads self.app also sees the instrumented version.
"""
wrapped(*args, **kwargs)
try:
if _is_flask_app(instance.app):
logger.debug("Skipping BaseWSGIServer instrumentation for Flask app")
return
if not isinstance(instance.app, InstanaWSGIMiddleware):
instance.app = InstanaWSGIMiddleware(
instance.app, status_as_string=False
)
logger.debug("BaseWSGIServer app wrapped")
except Exception:
logger.debug("Failed to wrap BaseWSGIServer app", exc_info=True)

logger.debug("Instrumenting werkzeug")

except ImportError:
Expand Down
62 changes: 62 additions & 0 deletions tests/frameworks/test_werkzeug.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,68 @@ def __call__(self, environ, start_response):
assert call_args[2] is flask_app
assert not isinstance(call_args[2], InstanaWSGIMiddleware)

def test_base_wsgi_server_direct_instantiation(self) -> None:
"""Test instrumentation when BaseWSGIServer is instantiated directly (e.g. Odoo).

Odoo's ThreadedWSGIServerReloadable extends werkzeug.serving.ThreadedWSGIServer
which extends BaseWSGIServer, bypassing run_simple entirely. This test verifies
that the BaseWSGIServer.__init__ patch wraps the app in that case.
"""
import socket
from werkzeug.serving import BaseWSGIServer

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]

server = BaseWSGIServer("127.0.0.1", port, simple_wsgi_app)
try:
assert isinstance(server.app, InstanaWSGIMiddleware)
assert server.app.app is simple_wsgi_app
finally:
server.server_close()

def test_base_wsgi_server_skips_flask_app(self) -> None:
"""Test that BaseWSGIServer patch skips Flask apps."""
import socket
from werkzeug.serving import BaseWSGIServer

class Flask:
def __call__(self, environ, start_response):
return simple_wsgi_app(environ, start_response)

Flask.__module__ = "flask.app"
flask_app = Flask()

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]

server = BaseWSGIServer("127.0.0.1", port, flask_app)
try:
assert server.app is flask_app
assert not isinstance(server.app, InstanaWSGIMiddleware)
finally:
server.server_close()

def test_base_wsgi_server_not_double_wrapped(self) -> None:
"""Test that an already-wrapped app is not wrapped again."""
import socket
from werkzeug.serving import BaseWSGIServer

pre_wrapped = InstanaWSGIMiddleware(simple_wsgi_app, status_as_string=False)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]

server = BaseWSGIServer("127.0.0.1", port, pre_wrapped)
try:
assert server.app is pre_wrapped
assert not isinstance(server.app.app, InstanaWSGIMiddleware)
finally:
server.server_close()


def test_parse_status_code_handles_valid_and_invalid_values() -> None:
"""Test safe parsing of WSGI status strings."""
Expand Down
Loading