scout-apm 3.3.0__cp312-cp312-macosx_10_13_x86_64.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. scout_apm/__init__.py +0 -0
  2. scout_apm/api/__init__.py +197 -0
  3. scout_apm/async_/__init__.py +1 -0
  4. scout_apm/async_/api.py +41 -0
  5. scout_apm/async_/instruments/__init__.py +0 -0
  6. scout_apm/async_/instruments/jinja2.py +13 -0
  7. scout_apm/async_/starlette.py +101 -0
  8. scout_apm/bottle.py +86 -0
  9. scout_apm/celery.py +153 -0
  10. scout_apm/compat.py +104 -0
  11. scout_apm/core/__init__.py +99 -0
  12. scout_apm/core/_objtrace.cpython-312-darwin.so +0 -0
  13. scout_apm/core/agent/__init__.py +0 -0
  14. scout_apm/core/agent/commands.py +250 -0
  15. scout_apm/core/agent/manager.py +319 -0
  16. scout_apm/core/agent/socket.py +211 -0
  17. scout_apm/core/backtrace.py +116 -0
  18. scout_apm/core/cli/__init__.py +0 -0
  19. scout_apm/core/cli/core_agent_manager.py +32 -0
  20. scout_apm/core/config.py +404 -0
  21. scout_apm/core/context.py +140 -0
  22. scout_apm/core/error.py +95 -0
  23. scout_apm/core/error_service.py +167 -0
  24. scout_apm/core/metadata.py +66 -0
  25. scout_apm/core/n_plus_one_tracker.py +41 -0
  26. scout_apm/core/objtrace.py +24 -0
  27. scout_apm/core/platform_detection.py +66 -0
  28. scout_apm/core/queue_time.py +99 -0
  29. scout_apm/core/sampler.py +149 -0
  30. scout_apm/core/samplers/__init__.py +0 -0
  31. scout_apm/core/samplers/cpu.py +76 -0
  32. scout_apm/core/samplers/memory.py +23 -0
  33. scout_apm/core/samplers/thread.py +41 -0
  34. scout_apm/core/stacktracer.py +30 -0
  35. scout_apm/core/threading.py +56 -0
  36. scout_apm/core/tracked_request.py +328 -0
  37. scout_apm/core/web_requests.py +167 -0
  38. scout_apm/django/__init__.py +7 -0
  39. scout_apm/django/apps.py +137 -0
  40. scout_apm/django/instruments/__init__.py +0 -0
  41. scout_apm/django/instruments/huey.py +30 -0
  42. scout_apm/django/instruments/sql.py +140 -0
  43. scout_apm/django/instruments/template.py +35 -0
  44. scout_apm/django/middleware.py +211 -0
  45. scout_apm/django/request.py +144 -0
  46. scout_apm/dramatiq.py +42 -0
  47. scout_apm/falcon.py +142 -0
  48. scout_apm/flask/__init__.py +118 -0
  49. scout_apm/flask/sqlalchemy.py +28 -0
  50. scout_apm/huey.py +54 -0
  51. scout_apm/hug.py +40 -0
  52. scout_apm/instruments/__init__.py +21 -0
  53. scout_apm/instruments/elasticsearch.py +263 -0
  54. scout_apm/instruments/jinja2.py +127 -0
  55. scout_apm/instruments/pymongo.py +105 -0
  56. scout_apm/instruments/redis.py +77 -0
  57. scout_apm/instruments/urllib3.py +80 -0
  58. scout_apm/rq.py +85 -0
  59. scout_apm/sqlalchemy.py +38 -0
  60. scout_apm-3.3.0.dist-info/LICENSE +21 -0
  61. scout_apm-3.3.0.dist-info/METADATA +94 -0
  62. scout_apm-3.3.0.dist-info/RECORD +65 -0
  63. scout_apm-3.3.0.dist-info/WHEEL +5 -0
  64. scout_apm-3.3.0.dist-info/entry_points.txt +2 -0
  65. scout_apm-3.3.0.dist-info/top_level.txt +1 -0
scout_apm/dramatiq.py ADDED
@@ -0,0 +1,42 @@
1
+ # coding=utf-8
2
+
3
+ import dramatiq
4
+
5
+ import scout_apm.core
6
+ from scout_apm.core.tracked_request import TrackedRequest
7
+
8
+
9
+ class ScoutMiddleware(dramatiq.Middleware):
10
+ def __init__(self):
11
+ installed = scout_apm.core.install()
12
+ self._do_nothing = not installed
13
+
14
+ def before_process_message(self, broker, message):
15
+ if self._do_nothing:
16
+ return
17
+ tracked_request = TrackedRequest.instance()
18
+ tracked_request.tag("queue", message.queue_name)
19
+ tracked_request.tag("message_id", message.message_id)
20
+ operation = "Job/" + message.actor_name
21
+ tracked_request.start_span(operation=operation)
22
+ tracked_request.operation = operation
23
+
24
+ def after_process_message(self, broker, message, result=None, exception=None):
25
+ if self._do_nothing:
26
+ return
27
+ tracked_request = TrackedRequest.instance()
28
+ tracked_request.is_real_request = True
29
+ if exception:
30
+ tracked_request.tag("error", "true")
31
+ tracked_request.stop_span()
32
+
33
+ def after_skip_message(self, broker, message):
34
+ """
35
+ The message was skipped by another middleware raising SkipMessage.
36
+ Stop the span and thus the request, it won't have been marked as real
37
+ so that's alright.
38
+ """
39
+ if self._do_nothing:
40
+ return
41
+ tracked_request = TrackedRequest.instance()
42
+ tracked_request.stop_span()
scout_apm/falcon.py ADDED
@@ -0,0 +1,142 @@
1
+ # coding=utf-8
2
+
3
+ import logging
4
+ import warnings
5
+
6
+ import falcon
7
+
8
+ from scout_apm.api import install
9
+ from scout_apm.core.config import scout_config
10
+ from scout_apm.core.queue_time import track_request_queue_time
11
+ from scout_apm.core.tracked_request import TrackedRequest
12
+ from scout_apm.core.web_requests import create_filtered_path, ignore_path
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Falcon Middleware docs:
17
+ # https://falcon.readthedocs.io/en/stable/api/middleware.html
18
+
19
+
20
+ class ScoutMiddleware(object):
21
+ """
22
+ Falcon Middleware for integration with Scout APM.
23
+ """
24
+
25
+ def __init__(self, config, hug_http_interface=None):
26
+ self.api = None
27
+ self.hug_http_interface = hug_http_interface
28
+ installed = install(config=config)
29
+ self._do_nothing = not installed
30
+
31
+ def set_api(self, api):
32
+ if not isinstance(api, falcon.API):
33
+ raise ValueError("api should be an instance of falcon.API")
34
+ self.api = api
35
+
36
+ def process_request(self, req, resp):
37
+ if self._do_nothing:
38
+ return
39
+ if self.api is None and self.hug_http_interface is not None:
40
+ self.api = self.hug_http_interface.falcon
41
+ tracked_request = TrackedRequest.instance()
42
+ tracked_request.is_real_request = True
43
+ req.context.scout_tracked_request = tracked_request
44
+ tracked_request.start_span(
45
+ operation="Middleware", should_capture_backtrace=False
46
+ )
47
+
48
+ path = req.path
49
+ # Falcon URL parameter values are *either* single items or lists
50
+ url_params = [
51
+ (k, v)
52
+ for k, vs in req.params.items()
53
+ for v in (vs if isinstance(vs, list) else [vs])
54
+ ]
55
+ tracked_request.tag("path", create_filtered_path(path, url_params))
56
+ if ignore_path(path):
57
+ tracked_request.tag("ignore_transaction", True)
58
+
59
+ if scout_config.value("collect_remote_ip"):
60
+ # Determine a remote IP to associate with the request. The value is
61
+ # spoofable by the requester so this is not suitable to use in any
62
+ # security sensitive context.
63
+ user_ip = (
64
+ req.get_header("x-forwarded-for", default="").split(",")[0]
65
+ or req.get_header("client-ip", default="").split(",")[0]
66
+ or req.remote_addr
67
+ )
68
+ tracked_request.tag("user_ip", user_ip)
69
+
70
+ queue_time = req.get_header("x-queue-start", default="") or req.get_header(
71
+ "x-request-start", default=""
72
+ )
73
+ track_request_queue_time(queue_time, tracked_request)
74
+
75
+ def process_resource(self, req, resp, resource, params):
76
+ if self._do_nothing:
77
+ return
78
+
79
+ tracked_request = getattr(req.context, "scout_tracked_request", None)
80
+ if tracked_request is None:
81
+ # Somehow we didn't start a request - this might occur in
82
+ # combination with a pretty adversarial application, so guard
83
+ # against it, although if a request was started and the context was
84
+ # lost, other problems might occur.
85
+ return
86
+
87
+ if self.api is None:
88
+ warnings.warn(
89
+ (
90
+ "{}.set_api() should be called before requests begin for"
91
+ + " more detail."
92
+ ).format(self.__class__.__name__),
93
+ RuntimeWarning,
94
+ stacklevel=2,
95
+ )
96
+ operation = "Controller/{}.{}.{}".format(
97
+ resource.__module__, resource.__class__.__name__, req.method
98
+ )
99
+ else:
100
+ # Find the current responder's name. Falcon passes middleware the
101
+ # current resource but unfortunately not the method being called, hence
102
+ # we have to go through routing again.
103
+ responder, _params, _resource, _uri_template = self.api._get_responder(req)
104
+ operation = self._name_operation(req, responder, resource)
105
+
106
+ span = tracked_request.start_span(
107
+ operation=operation, should_capture_backtrace=False
108
+ )
109
+ tracked_request.operation = operation
110
+ req.context.scout_resource_span = span
111
+
112
+ def _name_operation(self, req, responder, resource):
113
+ try:
114
+ last_part = responder.__name__
115
+ except AttributeError:
116
+ last_part = req.method
117
+ return "Controller/{}.{}.{}".format(
118
+ resource.__module__, resource.__class__.__name__, last_part
119
+ )
120
+
121
+ def process_response(self, req, resp, resource, req_succeeded):
122
+ tracked_request = getattr(req.context, "scout_tracked_request", None)
123
+ if tracked_request is None:
124
+ # Somehow we didn't start a request
125
+ return
126
+
127
+ # Falcon only stores the response status line, we have to parse it
128
+ try:
129
+ status_code = int(resp.status.split(" ")[0])
130
+ except ValueError:
131
+ # Bad status line - force it to be tagged as an error because
132
+ # client will experience it as one
133
+ status_code = 500
134
+
135
+ if not req_succeeded or 500 <= status_code <= 599:
136
+ tracked_request.tag("error", "true")
137
+
138
+ span = getattr(req.context, "scout_resource_span", None)
139
+ if span is not None:
140
+ tracked_request.stop_span()
141
+ # Stop Middleware span
142
+ tracked_request.stop_span()
@@ -0,0 +1,118 @@
1
+ # coding=utf-8
2
+
3
+ import logging
4
+ import sys
5
+
6
+ import wrapt
7
+ from flask import current_app
8
+ from flask.globals import request, session
9
+
10
+ import scout_apm.core
11
+ from scout_apm.core.config import scout_config
12
+ from scout_apm.core.error import ErrorMonitor
13
+ from scout_apm.core.tracked_request import TrackedRequest
14
+ from scout_apm.core.web_requests import RequestComponents, werkzeug_track_request_data
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ScoutApm(object):
20
+ def __init__(self, app):
21
+ self.app = app
22
+ self._attempted_install = False
23
+ app.full_dispatch_request = self.wrapped_full_dispatch_request(
24
+ app.full_dispatch_request
25
+ )
26
+ app.preprocess_request = self.wrapped_preprocess_request(app.preprocess_request)
27
+
28
+ @wrapt.decorator
29
+ def wrapped_full_dispatch_request(self, wrapped, instance, args, kwargs):
30
+ if not self._attempted_install:
31
+ self.extract_flask_settings()
32
+ installed = scout_apm.core.install()
33
+ self._do_nothing = not installed
34
+ self._attempted_install = True
35
+
36
+ if self._do_nothing:
37
+ return wrapped(*args, **kwargs)
38
+
39
+ # Pass on routing exceptions (normally 404's)
40
+ if request.routing_exception is not None:
41
+ return wrapped(*args, **kwargs)
42
+
43
+ request_components = get_request_components(self.app, request)
44
+ operation = "Controller/{}.{}".format(
45
+ request_components.module, request_components.controller
46
+ )
47
+
48
+ tracked_request = TrackedRequest.instance()
49
+ tracked_request.is_real_request = True
50
+ tracked_request.operation = operation
51
+ request._scout_tracked_request = tracked_request
52
+
53
+ werkzeug_track_request_data(request, tracked_request)
54
+
55
+ with tracked_request.span(
56
+ operation=operation, should_capture_backtrace=False
57
+ ) as span:
58
+ request._scout_view_span = span
59
+
60
+ try:
61
+ response = wrapped(*args, **kwargs)
62
+ except Exception as exc:
63
+ tracked_request.tag("error", "true")
64
+ if scout_config.value("errors_enabled"):
65
+ ErrorMonitor.send(
66
+ sys.exc_info(),
67
+ request_components=get_request_components(self.app, request),
68
+ request_path=request.path,
69
+ request_params=dict(request.args.lists()),
70
+ session=dict(session.items()),
71
+ environment=self.app.config,
72
+ )
73
+ raise exc
74
+ else:
75
+ if 500 <= response.status_code <= 599:
76
+ tracked_request.tag("error", "true")
77
+ return response
78
+
79
+ @wrapt.decorator
80
+ def wrapped_preprocess_request(self, wrapped, instance, args, kwargs):
81
+ tracked_request = getattr(request, "_scout_tracked_request", None)
82
+ if tracked_request is None:
83
+ return wrapped(*args, **kwargs)
84
+
85
+ # Unlike middleware in other frameworks, using request preprocessors is
86
+ # less common in Flask, so only add a span if there is any in use
87
+ have_before_request_funcs = (
88
+ None in instance.before_request_funcs
89
+ or request.blueprint in instance.before_request_funcs
90
+ )
91
+ if not have_before_request_funcs:
92
+ return wrapped(*args, **kwargs)
93
+
94
+ with tracked_request.span("PreprocessRequest", should_capture_backtrace=False):
95
+ return wrapped(*args, **kwargs)
96
+
97
+ def extract_flask_settings(self):
98
+ """
99
+ Copies SCOUT_* settings in the app into Scout's config lookup
100
+ """
101
+ configs = {}
102
+ configs["application_root"] = self.app.instance_path
103
+ for name in current_app.config:
104
+ if name.startswith("SCOUT_"):
105
+ value = current_app.config[name]
106
+ clean_name = name.replace("SCOUT_", "").lower()
107
+ configs[clean_name] = value
108
+ scout_config.set(**configs)
109
+
110
+
111
+ def get_request_components(app, request):
112
+ view_func = app.view_functions[request.endpoint]
113
+ request_components = RequestComponents(
114
+ module=view_func.__module__,
115
+ controller=view_func.__name__,
116
+ action=request.method,
117
+ )
118
+ return request_components
@@ -0,0 +1,28 @@
1
+ # coding=utf-8
2
+
3
+ import wrapt
4
+ from flask_sqlalchemy import SQLAlchemy
5
+
6
+ import scout_apm.sqlalchemy
7
+
8
+
9
+ def instrument_sqlalchemy(db):
10
+ # Version 3 of flask_sqlalchemy changed how engines are created
11
+ if hasattr(db, "_make_engine"):
12
+ db._make_engine = wrapped_make_engine(db._make_engine)
13
+ else:
14
+ SQLAlchemy.get_engine = wrapped_get_engine(SQLAlchemy.get_engine)
15
+
16
+
17
+ @wrapt.decorator
18
+ def wrapped_get_engine(wrapped, instance, args, kwargs):
19
+ engine = wrapped(*args, **kwargs)
20
+ scout_apm.sqlalchemy.instrument_sqlalchemy(engine)
21
+ return engine
22
+
23
+
24
+ @wrapt.decorator
25
+ def wrapped_make_engine(wrapped, instance, args, kwargs):
26
+ engine = wrapped(*args, **kwargs)
27
+ scout_apm.sqlalchemy.instrument_sqlalchemy(engine)
28
+ return engine
scout_apm/huey.py ADDED
@@ -0,0 +1,54 @@
1
+ # coding=utf-8
2
+
3
+ from huey.exceptions import RetryTask, TaskLockedException
4
+ from huey.signals import SIGNAL_CANCELED
5
+
6
+ import scout_apm.core
7
+ from scout_apm.core.tracked_request import TrackedRequest
8
+
9
+ # Because neither hooks nor signals are called in *all* cases, we need to use
10
+ # both in order to capture every case. See source:
11
+ # https://github.com/coleifer/huey/blob/e6710bd6a9f581ebc728e24f5923d26eb0047750/huey/api.py#L331 # noqa
12
+
13
+
14
+ def attach_scout(huey):
15
+ installed = scout_apm.core.install()
16
+ if installed:
17
+ attach_scout_handlers(huey)
18
+
19
+
20
+ def attach_scout_handlers(huey):
21
+ huey.pre_execute()(scout_on_pre_execute)
22
+ huey.post_execute()(scout_on_post_execute)
23
+ huey.signal(SIGNAL_CANCELED)(scout_on_cancelled)
24
+
25
+
26
+ def scout_on_pre_execute(task):
27
+ tracked_request = TrackedRequest.instance()
28
+
29
+ tracked_request.tag("task_id", task.id)
30
+
31
+ operation = "Job/{}.{}".format(task.__module__, task.__class__.__name__)
32
+ tracked_request.start_span(operation=operation)
33
+ tracked_request.operation = operation
34
+
35
+
36
+ def scout_on_post_execute(task, task_value, exception):
37
+ tracked_request = TrackedRequest.instance()
38
+ if exception is None:
39
+ tracked_request.is_real_request = True
40
+ elif isinstance(exception, TaskLockedException):
41
+ pass
42
+ elif isinstance(exception, RetryTask):
43
+ tracked_request.is_real_request = True
44
+ tracked_request.tag("retrying", True)
45
+ else:
46
+ tracked_request.is_real_request = True
47
+ tracked_request.tag("error", "true")
48
+ tracked_request.stop_span()
49
+
50
+
51
+ def scout_on_cancelled(signal, task, exc=None):
52
+ # In the case of a cancelled signal, Huey doesn't run the post_execute
53
+ # handler, so we need to tidy up
54
+ TrackedRequest.instance().stop_span()
scout_apm/hug.py ADDED
@@ -0,0 +1,40 @@
1
+ # coding=utf-8
2
+
3
+ import hug
4
+ from hug.interface import HTTP
5
+
6
+ from scout_apm.falcon import ScoutMiddleware as FalconMiddleware
7
+
8
+
9
+ class ScoutMiddleware(FalconMiddleware):
10
+ """
11
+ Hug's HTTP interface is based on Falcon. Therefore we use a subclass of our
12
+ Falcon integration with Hug specific extras.
13
+ """
14
+
15
+ def __init__(self, config, hug_http_interface):
16
+ super(ScoutMiddleware, self).__init__(config)
17
+ self.hug_http_interface = hug_http_interface
18
+
19
+ def process_request(self, req, resp):
20
+ if not self._do_nothing and self.api is None:
21
+ self.api = self.hug_http_interface.falcon
22
+ return super(ScoutMiddleware, self).process_request(req, resp)
23
+
24
+ def _name_operation(self, req, responder, resource):
25
+ if isinstance(responder, HTTP):
26
+ # Hug doesn't use functions but its custom callable classes
27
+ return "Controller/{}.{}".format(
28
+ responder.interface._function.__module__,
29
+ responder.interface._function.__name__,
30
+ )
31
+ return super(ScoutMiddleware, self)._name_operation(req, responder, resource)
32
+
33
+
34
+ def integrate_scout(hug_module_name, config):
35
+ http_interface = hug.API(hug_module_name).http
36
+ scout_middleware = ScoutMiddleware(
37
+ config=config,
38
+ hug_http_interface=http_interface,
39
+ )
40
+ http_interface.add_middleware(scout_middleware)
@@ -0,0 +1,21 @@
1
+ # coding=utf-8
2
+
3
+ import importlib
4
+ import logging
5
+
6
+ from scout_apm.core.config import scout_config
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ instrument_names = ["elasticsearch", "jinja2", "pymongo", "redis", "urllib3"]
11
+
12
+
13
+ def ensure_all_installed():
14
+ disabled_instruments = scout_config.value("disabled_instruments")
15
+ for instrument_name in instrument_names:
16
+ if instrument_name in disabled_instruments:
17
+ logger.info("%s instrument is disabled. Skipping.", instrument_name)
18
+ continue
19
+
20
+ module = importlib.import_module("{}.{}".format(__name__, instrument_name))
21
+ module.ensure_installed()