diff --git a/README.md b/README.md index cf23b2c4..4239a4c3 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,14 @@ Python 2.7 or 3.4+. Scout APM has integrations for the following frameworks: * Bottle 0.12+ -* CherryPy 18.0.0+ * Celery 3.1+ -* Django 1.8+ +* Django 3.2+ * Dramatiq 1.0+ * Falcon 2.0+ * Flask 0.10+ * Huey 2.0+ * Hug 2.5.1+ * Nameko 2.0+ -* Pyramid 1.8+ * RQ 1.0+ * Starlette 0.12+ diff --git a/src/scout_apm/cherrypy.py b/src/scout_apm/cherrypy.py deleted file mode 100644 index b1a6e9f0..00000000 --- a/src/scout_apm/cherrypy.py +++ /dev/null @@ -1,130 +0,0 @@ -# coding=utf-8 -from __future__ import absolute_import, division, print_function, unicode_literals - -import cherrypy -from cherrypy.lib.encoding import ResponseEncoder -from cherrypy.process import plugins - -import scout_apm.core -from scout_apm.compat import parse_qsl -from scout_apm.core.config import scout_config -from scout_apm.core.tracked_request import TrackedRequest -from scout_apm.core.web_requests import ( - create_filtered_path, - ignore_path, - track_request_queue_time, -) - - -class ScoutPlugin(plugins.SimplePlugin): - def __init__(self, bus): - super(ScoutPlugin, self).__init__(bus) - installed = scout_apm.core.install() - self._do_nothing = not installed - - def before_request(self): - if self._do_nothing: - return - request = cherrypy.request - tracked_request = TrackedRequest.instance() - tracked_request.is_real_request = True - request._scout_tracked_request = tracked_request - - # Can't name operation until after request, when routing has been done - request._scout_controller_span = tracked_request.start_span( - "Controller/Unknown" - ) - - def after_request(self): - if self._do_nothing: - return - request = cherrypy.request - tracked_request = getattr(request, "_scout_tracked_request", None) - if tracked_request is None: - return - - # Rename controller span now routing has been done - operation_name = get_operation_name(request) - if operation_name is not None: - request._scout_controller_span.operation = operation_name - - # Grab general request data now it has been parsed - path = request.path_info - # Parse params ourselves because we want only GET params but CherryPy - # parses POST params (nearly always sensitive) into the same dict. - params = parse_qsl(request.query_string) - tracked_request.tag("path", create_filtered_path(path, params)) - if ignore_path(path): - tracked_request.tag("ignore_transaction", True) - - if scout_config.value("collect_remote_ip"): - # Determine a remote IP to associate with the request. The value is - # spoofable by the requester so this is not suitable to use in any - # security sensitive context. - user_ip = ( - request.headers.get("x-forwarded-for", "").split(",")[0] - or request.headers.get("client-ip", "").split(",")[0] - or (request.remote.ip or None) - ) - tracked_request.tag("user_ip", user_ip) - - queue_time = request.headers.get("x-queue-start", "") or request.headers.get( - "x-request-start", "" - ) - track_request_queue_time(queue_time, tracked_request) - - response = cherrypy.response - status = response.status - if isinstance(status, int): - status_int = status - else: - status_first = status.split(" ", 1)[0] - try: - status_int = int(status_first) - except ValueError: - # Assume OK - status_int = 200 - if 500 <= status_int <= 599: - tracked_request.tag("error", "true") - elif status_int == 404: - tracked_request.is_real_request = False - - tracked_request.stop_span() - - -def get_operation_name(request): - handler = request.handler - if handler is None: - return None - - if isinstance(handler, ResponseEncoder): - real_handler = handler.oldhandler - else: - real_handler = handler - - # Unwrap HandlerWrapperTool classes - while hasattr(real_handler, "callable"): - real_handler = real_handler.callable - - # Looks like it's from HandlerTool - if getattr(real_handler, "__name__", "") == "handle_func": - try: - wrapped_tool = real_handler.__closure__[2].cell_contents.callable - except (AttributeError, IndexError): - pass - else: - try: - return "Controller/{}".format(wrapped_tool.__name__) - except AttributeError: - pass - - # Not a method? Not from an exposed view then - if not hasattr(real_handler, "__self__"): - return None - - real_handler_cls = real_handler.__self__.__class__ - return "Controller/{}.{}.{}".format( - real_handler_cls.__module__, - real_handler_cls.__name__, - real_handler.__name__, - ) diff --git a/src/scout_apm/pyramid.py b/src/scout_apm/pyramid.py deleted file mode 100644 index d76d2772..00000000 --- a/src/scout_apm/pyramid.py +++ /dev/null @@ -1,78 +0,0 @@ -# coding=utf-8 -from __future__ import absolute_import, division, print_function, unicode_literals - -import scout_apm.core -from scout_apm.core.config import scout_config -from scout_apm.core.tracked_request import TrackedRequest -from scout_apm.core.web_requests import ( - create_filtered_path, - ignore_path, - track_request_queue_time, -) - - -def includeme(config): - configs = {} - pyramid_config = config.get_settings() - for name in pyramid_config: - if name.startswith("SCOUT_"): - value = pyramid_config[name] - clean_name = name.replace("SCOUT_", "").lower() - configs[clean_name] = value - scout_config.set(**configs) - - if scout_apm.core.install(): - config.add_tween("scout_apm.pyramid.instruments") - - -def instruments(handler, registry): - def scout_tween(request): - tracked_request = TrackedRequest.instance() - - with tracked_request.span( - operation="Controller/Pyramid", should_capture_backtrace=False - ) as span: - path = request.path - # mixed() returns values as *either* single items or lists - url_params = [ - (k, v) for k, vs in request.GET.dict_of_lists().items() for v in vs - ] - tracked_request.tag("path", create_filtered_path(path, url_params)) - if ignore_path(path): - tracked_request.tag("ignore_transaction", True) - - if scout_config.value("collect_remote_ip"): - # Determine a remote IP to associate with the request. The value is - # spoofable by the requester so this is not suitable to use in any - # security sensitive context. - user_ip = ( - request.headers.get("x-forwarded-for", default="").split(",")[0] - or request.headers.get("client-ip", default="").split(",")[0] - or request.remote_addr - ) - tracked_request.tag("user_ip", user_ip) - - queue_time = request.headers.get( - "x-queue-start", default="" - ) or request.headers.get("x-request-start", default="") - track_request_queue_time(queue_time, tracked_request) - - try: - try: - response = handler(request) - finally: - # Routing lives further down the call chain. So time it - # starting above, but only set the name if it gets a name - if request.matched_route is not None: - tracked_request.is_real_request = True - span.operation = "Controller/" + request.matched_route.name - except Exception: - tracked_request.tag("error", "true") - raise - - if 500 <= response.status_code <= 599: - tracked_request.tag("error", "true") - - return response - - return scout_tween diff --git a/tests/integration/app.py b/tests/integration/app.py index 6a4c7caa..d4743303 100755 --- a/tests/integration/app.py +++ b/tests/integration/app.py @@ -20,7 +20,7 @@ import logging import scout_apm.api -from tests.integration import test_bottle, test_django, test_flask, test_pyramid +from tests.integration import test_bottle, test_django, test_flask logger = logging.getLogger(__name__) @@ -36,8 +36,6 @@ SUB_APPS["/django"] = app with test_flask.app_with_scout() as app: SUB_APPS["/flask"] = app -with test_pyramid.app_with_scout() as app: - SUB_APPS["/pyramid"] = app def app(environ, start_response): @@ -86,14 +84,6 @@ def app(environ, start_response):
  • Crash
  • -
    -

    Pyramid

    - -
    diff --git a/tests/integration/test_cherrypy.py b/tests/integration/test_cherrypy.py deleted file mode 100644 index 48ff4f8d..00000000 --- a/tests/integration/test_cherrypy.py +++ /dev/null @@ -1,213 +0,0 @@ -# coding=utf-8 -from __future__ import absolute_import, division, print_function, unicode_literals - -import datetime as dt -from contextlib import contextmanager - -import cherrypy -from webtest import TestApp - -from scout_apm.api import Config -from scout_apm.cherrypy import ScoutPlugin -from scout_apm.compat import datetime_to_timestamp, kwargs_only -from tests.integration.util import ( - parametrize_filtered_params, - parametrize_queue_time_header_name, - parametrize_user_ip_headers, -) - - -@contextmanager -@kwargs_only -def app_with_scout(scout_config=None): - """ - Context manager that configures and installs the Scout plugin for CherryPy. - """ - if scout_config is None: - scout_config = {} - - scout_config["core_agent_launch"] = False - scout_config.setdefault("monitor", True) - Config.set(**scout_config) - - class Views(object): - @cherrypy.expose - def index(self, **params): # Take all params so CherryPy doesn't 404 - return "Welcome home." - - @cherrypy.expose - def hello(self): - return "Hello World!" - - @cherrypy.expose - def crash(self): - raise ValueError("BØØM!") # non-ASCII - - @cherrypy.expose - def return_error(self): - cherrypy.response.status = 503 - return "Something went wrong" - - # Serve this source file statically - static_file = cherrypy.tools.staticfile.handler(__file__) - - app = cherrypy.Application(Views(), "/", config=None) - - # Setup according to https://docs.scoutapm.com/#cherrypy - plugin = ScoutPlugin(cherrypy.engine) - plugin.subscribe() - - try: - yield app - finally: - plugin.unsubscribe() - Config.reset_all() - - -def test_home(tracked_requests): - with app_with_scout() as app: - response = TestApp(app).get("/") - - assert response.status_int == 200 - assert response.text == "Welcome home." - assert len(tracked_requests) == 1 - tracked_request = tracked_requests[0] - assert len(tracked_request.complete_spans) == 1 - assert tracked_request.tags["path"] == "/" - span = tracked_request.complete_spans[0] - assert span.operation == "Controller/tests.integration.test_cherrypy.Views.index" - - -def test_home_ignored(tracked_requests): - with app_with_scout(scout_config={"ignore": "/"}) as app: - response = TestApp(app).get("/") - - assert response.status_int == 200 - assert response.text == "Welcome home." - assert tracked_requests == [] - - -@parametrize_filtered_params -def test_filtered_params(params, expected_path, tracked_requests): - with app_with_scout() as app: - TestApp(app).get("/", params=params) - - assert tracked_requests[0].tags["path"] == expected_path - - -@parametrize_user_ip_headers -def test_user_ip(headers, client_address, expected, tracked_requests): - with app_with_scout() as app: - TestApp(app).get( - "/", - headers=headers, - extra_environ=( - {str("REMOTE_ADDR"): client_address} - if client_address is not None - else {} - ), - ) - - tracked_request = tracked_requests[0] - assert tracked_request.tags["user_ip"] == expected - - -def test_user_ip_collection_disabled(tracked_requests): - with app_with_scout(scout_config={"collect_remote_ip": False}) as app: - TestApp(app).get( - "/", - extra_environ={str("REMOTE_ADDR"): str("1.1.1.1")}, - ) - - tracked_request = tracked_requests[0] - assert "user_ip" not in tracked_request.tags - - -@parametrize_queue_time_header_name -def test_queue_time(header_name, tracked_requests): - # Not testing floats due to Python 2/3 rounding differences - queue_start = int(datetime_to_timestamp(dt.datetime.utcnow())) - 2 - with app_with_scout() as app: - response = TestApp(app).get( - "/", headers={header_name: str("t=") + str(queue_start)} - ) - - assert response.status_int == 200 - assert len(tracked_requests) == 1 - queue_time_ns = tracked_requests[0].tags["scout.queue_time_ns"] - assert isinstance(queue_time_ns, int) and queue_time_ns > 0 - - -def test_hello(tracked_requests): - with app_with_scout() as app: - response = TestApp(app).get("/hello/") - - assert response.status_int == 200 - assert response.text == "Hello World!" - assert len(tracked_requests) == 1 - tracked_request = tracked_requests[0] - assert len(tracked_request.complete_spans) == 1 - assert tracked_request.tags["path"] == "/hello/" - span = tracked_request.complete_spans[0] - assert span.operation == "Controller/tests.integration.test_cherrypy.Views.hello" - - -def test_server_error(tracked_requests): - with app_with_scout() as app: - response = TestApp(app).get("/crash/", expect_errors=True) - - assert response.status_int == 500 - assert len(tracked_requests) == 1 - tracked_request = tracked_requests[0] - assert tracked_request.tags["path"] == "/crash/" - assert tracked_request.tags["error"] == "true" - assert len(tracked_request.complete_spans) == 1 - span = tracked_request.complete_spans[0] - assert span.operation == "Controller/tests.integration.test_cherrypy.Views.crash" - - -def test_return_error(tracked_requests): - with app_with_scout() as app: - response = TestApp(app).get("/return-error/", expect_errors=True) - - assert response.status_int == 503 - assert len(tracked_requests) == 1 - tracked_request = tracked_requests[0] - assert tracked_request.tags["path"] == "/return-error/" - assert tracked_request.tags["error"] == "true" - assert len(tracked_request.complete_spans) == 1 - span = tracked_request.complete_spans[0] - assert ( - span.operation - == "Controller/tests.integration.test_cherrypy.Views.return_error" - ) - - -def test_static_file(tracked_requests): - with app_with_scout() as app: - # CherryPy serves its built-in favicon by default - response = TestApp(app).get("/static-file/") - - assert response.status_int == 200 - assert response.headers["content-type"] == "text/x-python;charset=utf-8" - assert len(tracked_requests) == 1 - tracked_request = tracked_requests[0] - assert len(tracked_request.complete_spans) == 1 - span = tracked_request.complete_spans[0] - assert span.operation == "Controller/staticfile" - - -def test_not_found(tracked_requests): - with app_with_scout() as app: - response = TestApp(app).get("/not-found/", expect_errors=True) - - assert response.status_int == 404 - assert tracked_requests == [] - - -def test_no_monitor(tracked_requests): - with app_with_scout(scout_config={"monitor": False}) as app: - response = TestApp(app).get("/") - - assert response.status_int == 200 - assert tracked_requests == [] diff --git a/tests/integration/test_pyramid.py b/tests/integration/test_pyramid.py deleted file mode 100644 index f1009855..00000000 --- a/tests/integration/test_pyramid.py +++ /dev/null @@ -1,201 +0,0 @@ -# coding=utf-8 -from __future__ import absolute_import, division, print_function, unicode_literals - -import datetime as dt -from contextlib import contextmanager - -import pytest -from pyramid.config import Configurator -from pyramid.response import Response -from webtest import TestApp - -from scout_apm.api import Config -from scout_apm.compat import datetime_to_timestamp, kwargs_only -from tests.integration.util import ( - parametrize_filtered_params, - parametrize_queue_time_header_name, - parametrize_user_ip_headers, -) - - -@contextmanager -@kwargs_only -def app_with_scout(config=None): - """ - Context manager that configures and installs the Scout plugin for Bottle. - - """ - # Enable Scout by default in tests. - if config is None: - config = {} - - config.setdefault("SCOUT_MONITOR", True) - # Disable running the agent. - config["SCOUT_CORE_AGENT_LAUNCH"] = False - - def home(request): - return Response("Welcome home.") - - def hello(request): - return Response("Hello World!") - - def crash(request): - raise ValueError("BØØM!") # non-ASCII - - def return_error(request): - return Response("Something went wrong", status=503) - - with Configurator() as configurator: - configurator.add_route("home", "/") - configurator.add_view(home, route_name="home", request_method="GET") - configurator.add_route("hello", "/hello/") - configurator.add_view(hello, route_name="hello") - configurator.add_route("crash", "/crash/") - configurator.add_view(crash, route_name="crash") - configurator.add_route("return_error", "/return-error/") - configurator.add_view(return_error, route_name="return_error") - - # Setup according to https://docs.scoutapm.com/#pyramid - configurator.add_settings(**config) - configurator.include("scout_apm.pyramid") - app = configurator.make_wsgi_app() - - try: - yield app - finally: - # Reset Scout configuration. - Config.reset_all() - - -def test_home(tracked_requests): - with app_with_scout() as app: - response = TestApp(app).get("/", expect_errors=True) - - assert response.status_int == 200 - assert response.text == "Welcome home." - assert len(tracked_requests) == 1 - tracked_request = tracked_requests[0] - assert tracked_request.tags["path"] == "/" - assert tracked_request.tags["user_ip"] is None - assert len(tracked_request.complete_spans) == 1 - span = tracked_request.complete_spans[0] - assert span.operation == "Controller/home" - - -@parametrize_filtered_params -def test_filtered_params(params, expected_path, tracked_requests): - with app_with_scout() as app: - TestApp(app).get("/", params=params) - - assert tracked_requests[0].tags["path"] == expected_path - - -@parametrize_user_ip_headers -def test_user_ip(headers, client_address, expected, tracked_requests): - with app_with_scout() as app: - TestApp(app).get( - "/", - headers=headers, - extra_environ=( - {str("REMOTE_ADDR"): client_address} - if client_address is not None - else {} - ), - ) - - tracked_request = tracked_requests[0] - assert tracked_request.tags["user_ip"] == expected - - -def test_user_ip_collection_disabled(tracked_requests): - with app_with_scout(config={"SCOUT_COLLECT_REMOTE_IP": False}) as app: - TestApp(app).get( - "/", - extra_environ={str("REMOTE_ADDR"): str("1.1.1.1")}, - ) - - tracked_request = tracked_requests[0] - assert "user_ip" not in tracked_request.tags - - -@parametrize_queue_time_header_name -def test_queue_time(header_name, tracked_requests): - # Not testing floats due to Python 2/3 rounding differences - queue_start = int(datetime_to_timestamp(dt.datetime.utcnow())) - 2 - with app_with_scout() as app: - response = TestApp(app).get( - "/", headers={header_name: str("t=") + str(queue_start)} - ) - - assert response.status_int == 200 - assert len(tracked_requests) == 1 - queue_time_ns = tracked_requests[0].tags["scout.queue_time_ns"] - assert isinstance(queue_time_ns, int) and queue_time_ns > 0 - - -def test_home_ignored(tracked_requests): - with app_with_scout(config={"SCOUT_IGNORE": ["/"]}) as app: - response = TestApp(app).get("/") - - assert response.status_int == 200 - assert response.text == "Welcome home." - assert tracked_requests == [] - - -def test_hello(tracked_requests): - with app_with_scout() as app: - response = TestApp(app).get("/hello/") - - assert response.status_int == 200 - assert response.text == "Hello World!" - assert len(tracked_requests) == 1 - tracked_request = tracked_requests[0] - assert tracked_request.tags["path"] == "/hello/" - assert len(tracked_request.complete_spans) == 1 - span = tracked_request.complete_spans[0] - assert span.operation == "Controller/hello" - - -def test_not_found(tracked_requests): - with app_with_scout() as app: - response = TestApp(app).get("/not-found/", expect_errors=True) - - assert response.status_int == 404 - assert tracked_requests == [] - - -def test_server_error(tracked_requests): - # Unlike most other frameworks, Pyramid doesn't catch all exceptions. - with app_with_scout() as app, pytest.raises(ValueError): - TestApp(app).get("/crash/", expect_errors=True) - - assert len(tracked_requests) == 1 - tracked_request = tracked_requests[0] - assert tracked_request.tags["path"] == "/crash/" - assert tracked_request.tags["error"] == "true" - assert len(tracked_request.complete_spans) == 1 - span = tracked_request.complete_spans[0] - assert span.operation == "Controller/crash" - - -def test_return_error(tracked_requests): - with app_with_scout() as app: - response = TestApp(app).get("/return-error/", expect_errors=True) - - assert response.status_int == 503 - assert response.text == "Something went wrong" - assert len(tracked_requests) == 1 - tracked_request = tracked_requests[0] - assert tracked_request.tags["path"] == "/return-error/" - assert tracked_request.tags["error"] == "true" - assert len(tracked_request.complete_spans) == 1 - span = tracked_request.complete_spans[0] - assert span.operation == "Controller/return_error" - - -def test_no_monitor(tracked_requests): - with app_with_scout(config={"SCOUT_MONITOR": False}) as app: - response = TestApp(app).get("/hello/") - - assert response.status_int == 200 - assert tracked_requests == []