scout-apm 3.3.0__cp38-cp38-musllinux_1_2_i686.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scout_apm/__init__.py +0 -0
- scout_apm/api/__init__.py +197 -0
- scout_apm/async_/__init__.py +1 -0
- scout_apm/async_/api.py +41 -0
- scout_apm/async_/instruments/__init__.py +0 -0
- scout_apm/async_/instruments/jinja2.py +13 -0
- scout_apm/async_/starlette.py +101 -0
- scout_apm/bottle.py +86 -0
- scout_apm/celery.py +153 -0
- scout_apm/compat.py +104 -0
- scout_apm/core/__init__.py +99 -0
- scout_apm/core/_objtrace.cpython-38-i386-linux-gnu.so +0 -0
- scout_apm/core/agent/__init__.py +0 -0
- scout_apm/core/agent/commands.py +250 -0
- scout_apm/core/agent/manager.py +319 -0
- scout_apm/core/agent/socket.py +211 -0
- scout_apm/core/backtrace.py +116 -0
- scout_apm/core/cli/__init__.py +0 -0
- scout_apm/core/cli/core_agent_manager.py +32 -0
- scout_apm/core/config.py +404 -0
- scout_apm/core/context.py +140 -0
- scout_apm/core/error.py +95 -0
- scout_apm/core/error_service.py +167 -0
- scout_apm/core/metadata.py +66 -0
- scout_apm/core/n_plus_one_tracker.py +41 -0
- scout_apm/core/objtrace.py +24 -0
- scout_apm/core/platform_detection.py +66 -0
- scout_apm/core/queue_time.py +99 -0
- scout_apm/core/sampler.py +149 -0
- scout_apm/core/samplers/__init__.py +0 -0
- scout_apm/core/samplers/cpu.py +76 -0
- scout_apm/core/samplers/memory.py +23 -0
- scout_apm/core/samplers/thread.py +41 -0
- scout_apm/core/stacktracer.py +30 -0
- scout_apm/core/threading.py +56 -0
- scout_apm/core/tracked_request.py +328 -0
- scout_apm/core/web_requests.py +167 -0
- scout_apm/django/__init__.py +7 -0
- scout_apm/django/apps.py +137 -0
- scout_apm/django/instruments/__init__.py +0 -0
- scout_apm/django/instruments/huey.py +30 -0
- scout_apm/django/instruments/sql.py +140 -0
- scout_apm/django/instruments/template.py +35 -0
- scout_apm/django/middleware.py +211 -0
- scout_apm/django/request.py +144 -0
- scout_apm/dramatiq.py +42 -0
- scout_apm/falcon.py +142 -0
- scout_apm/flask/__init__.py +118 -0
- scout_apm/flask/sqlalchemy.py +28 -0
- scout_apm/huey.py +54 -0
- scout_apm/hug.py +40 -0
- scout_apm/instruments/__init__.py +21 -0
- scout_apm/instruments/elasticsearch.py +263 -0
- scout_apm/instruments/jinja2.py +127 -0
- scout_apm/instruments/pymongo.py +105 -0
- scout_apm/instruments/redis.py +77 -0
- scout_apm/instruments/urllib3.py +80 -0
- scout_apm/rq.py +85 -0
- scout_apm/sqlalchemy.py +38 -0
- scout_apm-3.3.0.dist-info/LICENSE +21 -0
- scout_apm-3.3.0.dist-info/METADATA +82 -0
- scout_apm-3.3.0.dist-info/RECORD +65 -0
- scout_apm-3.3.0.dist-info/WHEEL +5 -0
- scout_apm-3.3.0.dist-info/entry_points.txt +2 -0
- scout_apm-3.3.0.dist-info/top_level.txt +1 -0
scout_apm/django/apps.py
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import sys
|
4
|
+
|
5
|
+
import django
|
6
|
+
from django.apps import AppConfig
|
7
|
+
from django.conf import settings
|
8
|
+
from django.core.signals import got_request_exception
|
9
|
+
from django.test.signals import setting_changed
|
10
|
+
|
11
|
+
if django.VERSION < (3, 1):
|
12
|
+
from django.views.debug import get_safe_settings
|
13
|
+
else:
|
14
|
+
from django.views.debug import SafeExceptionReporterFilter
|
15
|
+
|
16
|
+
def get_safe_settings():
|
17
|
+
return SafeExceptionReporterFilter().get_safe_settings()
|
18
|
+
|
19
|
+
|
20
|
+
import scout_apm.core
|
21
|
+
from scout_apm.core.config import scout_config
|
22
|
+
from scout_apm.core.error import ErrorMonitor
|
23
|
+
from scout_apm.django.instruments.huey import ensure_huey_instrumented
|
24
|
+
from scout_apm.django.instruments.sql import ensure_sql_instrumented
|
25
|
+
from scout_apm.django.instruments.template import ensure_templates_instrumented
|
26
|
+
from scout_apm.django.request import get_request_components
|
27
|
+
|
28
|
+
|
29
|
+
class ScoutApmDjangoConfig(AppConfig):
|
30
|
+
name = "scout_apm"
|
31
|
+
verbose_name = "Scout Apm (Django)"
|
32
|
+
|
33
|
+
def ready(self):
|
34
|
+
self.update_scout_config_from_django_settings()
|
35
|
+
setting_changed.connect(self.on_setting_changed)
|
36
|
+
|
37
|
+
# Finish installing the agent. If the agent isn't installed for any
|
38
|
+
# reason, return without installing instruments
|
39
|
+
installed = scout_apm.core.install()
|
40
|
+
if not installed:
|
41
|
+
return
|
42
|
+
|
43
|
+
if scout_config.value("errors_enabled"):
|
44
|
+
got_request_exception.connect(self.on_got_request_exception)
|
45
|
+
|
46
|
+
self.install_middleware()
|
47
|
+
|
48
|
+
# Setup Instruments
|
49
|
+
ensure_huey_instrumented()
|
50
|
+
ensure_sql_instrumented()
|
51
|
+
ensure_templates_instrumented()
|
52
|
+
|
53
|
+
def update_scout_config_from_django_settings(self, **kwargs):
|
54
|
+
for name in dir(settings):
|
55
|
+
self.on_setting_changed(name)
|
56
|
+
|
57
|
+
def on_got_request_exception(self, request, **kwargs):
|
58
|
+
"""
|
59
|
+
Process this exception with the error monitoring solution.
|
60
|
+
"""
|
61
|
+
ErrorMonitor.send(
|
62
|
+
sys.exc_info(),
|
63
|
+
request_components=get_request_components(request),
|
64
|
+
request_path=request.path,
|
65
|
+
request_params=dict(request.GET.lists()),
|
66
|
+
session=dict(request.session.items())
|
67
|
+
if hasattr(request, "session")
|
68
|
+
else None,
|
69
|
+
environment=get_safe_settings(),
|
70
|
+
)
|
71
|
+
|
72
|
+
def on_setting_changed(self, setting, **kwargs):
|
73
|
+
cast = None
|
74
|
+
if setting == "BASE_DIR":
|
75
|
+
scout_name = "application_root"
|
76
|
+
cast = str
|
77
|
+
elif setting.startswith("SCOUT_"):
|
78
|
+
scout_name = setting.replace("SCOUT_", "").lower()
|
79
|
+
else:
|
80
|
+
return
|
81
|
+
|
82
|
+
try:
|
83
|
+
value = getattr(settings, setting)
|
84
|
+
except AttributeError:
|
85
|
+
# It was removed
|
86
|
+
scout_config.unset(scout_name)
|
87
|
+
else:
|
88
|
+
if cast is not None:
|
89
|
+
value = cast(value)
|
90
|
+
scout_config.set(**{scout_name: value})
|
91
|
+
|
92
|
+
def install_middleware(self):
|
93
|
+
"""
|
94
|
+
Attempts to insert the ScoutApm middleware as the first middleware
|
95
|
+
(first on incoming requests, last on outgoing responses).
|
96
|
+
"""
|
97
|
+
from django.conf import settings
|
98
|
+
|
99
|
+
# If MIDDLEWARE is set, update that, with handling of tuple vs array forms
|
100
|
+
if getattr(settings, "MIDDLEWARE", None) is not None:
|
101
|
+
timing_middleware = "scout_apm.django.middleware.MiddlewareTimingMiddleware"
|
102
|
+
view_middleware = "scout_apm.django.middleware.ViewTimingMiddleware"
|
103
|
+
|
104
|
+
if isinstance(settings.MIDDLEWARE, tuple):
|
105
|
+
if timing_middleware not in settings.MIDDLEWARE:
|
106
|
+
settings.MIDDLEWARE = (timing_middleware,) + settings.MIDDLEWARE
|
107
|
+
if view_middleware not in settings.MIDDLEWARE:
|
108
|
+
settings.MIDDLEWARE = settings.MIDDLEWARE + (view_middleware,)
|
109
|
+
else:
|
110
|
+
if timing_middleware not in settings.MIDDLEWARE:
|
111
|
+
settings.MIDDLEWARE.insert(0, timing_middleware)
|
112
|
+
if view_middleware not in settings.MIDDLEWARE:
|
113
|
+
settings.MIDDLEWARE.append(view_middleware)
|
114
|
+
|
115
|
+
# Otherwise, we're doing old style middleware, do the same thing with
|
116
|
+
# the same handling of tuple vs array forms
|
117
|
+
else:
|
118
|
+
timing_middleware = (
|
119
|
+
"scout_apm.django.middleware.OldStyleMiddlewareTimingMiddleware"
|
120
|
+
)
|
121
|
+
view_middleware = "scout_apm.django.middleware.OldStyleViewMiddleware"
|
122
|
+
|
123
|
+
if isinstance(settings.MIDDLEWARE_CLASSES, tuple):
|
124
|
+
if timing_middleware not in settings.MIDDLEWARE_CLASSES:
|
125
|
+
settings.MIDDLEWARE_CLASSES = (
|
126
|
+
timing_middleware,
|
127
|
+
) + settings.MIDDLEWARE_CLASSES
|
128
|
+
|
129
|
+
if view_middleware not in settings.MIDDLEWARE_CLASSES:
|
130
|
+
settings.MIDDLEWARE_CLASSES = settings.MIDDLEWARE_CLASSES + (
|
131
|
+
view_middleware,
|
132
|
+
)
|
133
|
+
else:
|
134
|
+
if timing_middleware not in settings.MIDDLEWARE_CLASSES:
|
135
|
+
settings.MIDDLEWARE_CLASSES.insert(0, timing_middleware)
|
136
|
+
if view_middleware not in settings.MIDDLEWARE_CLASSES:
|
137
|
+
settings.MIDDLEWARE_CLASSES.append(view_middleware)
|
File without changes
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import logging
|
4
|
+
|
5
|
+
from django.conf import settings
|
6
|
+
|
7
|
+
logger = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
huey_instrumented = False
|
10
|
+
|
11
|
+
|
12
|
+
def ensure_huey_instrumented():
|
13
|
+
global huey_instrumented
|
14
|
+
if huey_instrumented:
|
15
|
+
return
|
16
|
+
huey_instrumented = True
|
17
|
+
|
18
|
+
# Avoid importing if not installed
|
19
|
+
if "huey.contrib.djhuey" not in settings.INSTALLED_APPS: # pragma: no cover
|
20
|
+
return
|
21
|
+
|
22
|
+
try:
|
23
|
+
from huey.contrib.djhuey import HUEY
|
24
|
+
except ImportError: # pragma: no cover
|
25
|
+
return
|
26
|
+
|
27
|
+
from scout_apm.huey import attach_scout_handlers
|
28
|
+
|
29
|
+
attach_scout_handlers(HUEY)
|
30
|
+
logger.debug("Instrumented huey.contrib.djhuey")
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import logging
|
4
|
+
|
5
|
+
import django
|
6
|
+
import wrapt
|
7
|
+
from django.db import connections
|
8
|
+
from django.db.backends.signals import connection_created
|
9
|
+
from django.db.backends.utils import CursorWrapper
|
10
|
+
|
11
|
+
from scout_apm.core.tracked_request import TrackedRequest
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
sql_instrumented = False
|
16
|
+
|
17
|
+
|
18
|
+
def ensure_sql_instrumented():
|
19
|
+
global sql_instrumented
|
20
|
+
if sql_instrumented:
|
21
|
+
return
|
22
|
+
sql_instrumented = True
|
23
|
+
|
24
|
+
if django.VERSION >= (2, 0):
|
25
|
+
for connection in connections.all():
|
26
|
+
install_db_execute_hook(connection=connection)
|
27
|
+
connection_created.connect(install_db_execute_hook)
|
28
|
+
logger.debug("Installed DB connection created signal handler")
|
29
|
+
else:
|
30
|
+
CursorWrapper.execute = execute_wrapper(CursorWrapper.execute)
|
31
|
+
CursorWrapper.executemany = executemany_wrapper(CursorWrapper.executemany)
|
32
|
+
|
33
|
+
logger.debug("Monkey patched SQL")
|
34
|
+
|
35
|
+
|
36
|
+
def db_execute_hook(execute, sql, params, many, context):
|
37
|
+
"""
|
38
|
+
Database instrumentation hook for Django 2.0+
|
39
|
+
https://docs.djangoproject.com/en/2.0/topics/db/instrumentation/
|
40
|
+
"""
|
41
|
+
if many:
|
42
|
+
operation = "SQL/Many"
|
43
|
+
else:
|
44
|
+
operation = "SQL/Query"
|
45
|
+
|
46
|
+
if sql is not None:
|
47
|
+
tracked_request = TrackedRequest.instance()
|
48
|
+
span = tracked_request.start_span(operation=operation)
|
49
|
+
span.tag("db.statement", sql)
|
50
|
+
|
51
|
+
try:
|
52
|
+
return execute(sql, params, many, context)
|
53
|
+
finally:
|
54
|
+
if sql is not None:
|
55
|
+
tracked_request.stop_span()
|
56
|
+
if tracked_request.n_plus_one_tracker.should_capture_backtrace(
|
57
|
+
sql=sql,
|
58
|
+
duration=span.duration(),
|
59
|
+
count=(1 if not many else len(params)),
|
60
|
+
):
|
61
|
+
span.capture_backtrace()
|
62
|
+
|
63
|
+
|
64
|
+
def install_db_execute_hook(connection, **kwargs):
|
65
|
+
"""
|
66
|
+
Install db_execute_hook on the given database connection.
|
67
|
+
|
68
|
+
Rather than use the documented API of the `execute_wrapper()` context
|
69
|
+
manager, directly insert the hook. This is done because:
|
70
|
+
1. Deleting the context manager closes it, so it's not possible to enter
|
71
|
+
it here and not exit it, unless we store it forever in some variable
|
72
|
+
2. We want to be the first hook, so we can capture every query (although
|
73
|
+
potentially later hooks will change the SQL)
|
74
|
+
3. We want to be idempotent and only install the hook once
|
75
|
+
"""
|
76
|
+
if db_execute_hook not in connection.execute_wrappers:
|
77
|
+
connection.execute_wrappers.insert(0, db_execute_hook)
|
78
|
+
|
79
|
+
|
80
|
+
@wrapt.decorator
|
81
|
+
def execute_wrapper(wrapped, instance, args, kwargs):
|
82
|
+
"""
|
83
|
+
CursorWrapper.execute() wrapper for Django < 2.0
|
84
|
+
"""
|
85
|
+
try:
|
86
|
+
sql = _extract_sql(*args, **kwargs)
|
87
|
+
except TypeError:
|
88
|
+
sql = None
|
89
|
+
|
90
|
+
if sql is not None:
|
91
|
+
tracked_request = TrackedRequest.instance()
|
92
|
+
span = tracked_request.start_span(operation="SQL/Query")
|
93
|
+
span.tag("db.statement", sql)
|
94
|
+
|
95
|
+
try:
|
96
|
+
return wrapped(*args, **kwargs)
|
97
|
+
finally:
|
98
|
+
if sql is not None:
|
99
|
+
tracked_request.stop_span()
|
100
|
+
if tracked_request.n_plus_one_tracker.should_capture_backtrace(
|
101
|
+
sql, span.duration()
|
102
|
+
):
|
103
|
+
span.capture_backtrace()
|
104
|
+
|
105
|
+
|
106
|
+
def _extract_sql(sql, *args, **kwargs):
|
107
|
+
return sql
|
108
|
+
|
109
|
+
|
110
|
+
@wrapt.decorator
|
111
|
+
def executemany_wrapper(wrapped, instance, args, kwargs):
|
112
|
+
"""
|
113
|
+
CursorWrapper.executemany() wrapper for Django < 2.0
|
114
|
+
"""
|
115
|
+
try:
|
116
|
+
sql, param_list = _extract_sql_param_list(*args, **kwargs)
|
117
|
+
except TypeError:
|
118
|
+
sql = None
|
119
|
+
param_list = None
|
120
|
+
|
121
|
+
if sql is not None:
|
122
|
+
tracked_request = TrackedRequest.instance()
|
123
|
+
span = tracked_request.start_span(operation="SQL/Many")
|
124
|
+
span.tag("db.statement", sql)
|
125
|
+
|
126
|
+
try:
|
127
|
+
return wrapped(*args, **kwargs)
|
128
|
+
finally:
|
129
|
+
if sql is not None:
|
130
|
+
tracked_request.stop_span()
|
131
|
+
if tracked_request.n_plus_one_tracker.should_capture_backtrace(
|
132
|
+
sql=sql,
|
133
|
+
duration=span.duration(),
|
134
|
+
count=len(param_list),
|
135
|
+
):
|
136
|
+
span.capture_backtrace()
|
137
|
+
|
138
|
+
|
139
|
+
def _extract_sql_param_list(sql, param_list, *args, **kwargs):
|
140
|
+
return sql, param_list
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import logging
|
4
|
+
|
5
|
+
from django.template import Template
|
6
|
+
from django.template.loader_tags import BlockNode
|
7
|
+
|
8
|
+
from scout_apm.core.stacktracer import trace_method
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
templates_instrumented = False
|
13
|
+
|
14
|
+
|
15
|
+
def ensure_templates_instrumented():
|
16
|
+
global templates_instrumented
|
17
|
+
if templates_instrumented:
|
18
|
+
return
|
19
|
+
templates_instrumented = True
|
20
|
+
|
21
|
+
@trace_method(Template)
|
22
|
+
def __init__(self, *args, **kwargs):
|
23
|
+
name = args[2] if len(args) >= 3 else "<Unknown Template>"
|
24
|
+
return ("Template/Compile", {"name": name})
|
25
|
+
|
26
|
+
@trace_method(Template)
|
27
|
+
def render(self, *args, **kwargs):
|
28
|
+
name = self.name if self.name is not None else "<Unknown Template>"
|
29
|
+
return ("Template/Render", {"name": name})
|
30
|
+
|
31
|
+
@trace_method(BlockNode, "render")
|
32
|
+
def render_block(self, *args, **kwargs):
|
33
|
+
return ("Block/Render", {"name": self.name})
|
34
|
+
|
35
|
+
logger.debug("Monkey patched Templates")
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
from django.conf import settings
|
4
|
+
from django.urls import get_urlconf
|
5
|
+
|
6
|
+
from scout_apm.core.config import scout_config
|
7
|
+
from scout_apm.core.queue_time import track_request_queue_time
|
8
|
+
from scout_apm.core.tracked_request import TrackedRequest
|
9
|
+
from scout_apm.core.web_requests import create_filtered_path, ignore_path
|
10
|
+
from scout_apm.django.request import get_controller_name
|
11
|
+
|
12
|
+
|
13
|
+
def track_request_view_data(request, tracked_request):
|
14
|
+
path = request.path
|
15
|
+
tracked_request.tag(
|
16
|
+
"path",
|
17
|
+
create_filtered_path(
|
18
|
+
path, [(k, v) for k, vs in request.GET.lists() for v in vs]
|
19
|
+
),
|
20
|
+
)
|
21
|
+
if ignore_path(path):
|
22
|
+
tracked_request.tag("ignore_transaction", True)
|
23
|
+
|
24
|
+
if scout_config.value("collect_remote_ip"):
|
25
|
+
try:
|
26
|
+
# Determine a remote IP to associate with the request. The value is
|
27
|
+
# spoofable by the requester so this is not suitable to use in any
|
28
|
+
# security sensitive context.
|
29
|
+
user_ip = (
|
30
|
+
request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")[0]
|
31
|
+
or request.META.get("HTTP_CLIENT_IP", "").split(",")[0]
|
32
|
+
or request.META.get("REMOTE_ADDR", None)
|
33
|
+
)
|
34
|
+
tracked_request.tag("user_ip", user_ip)
|
35
|
+
except Exception:
|
36
|
+
pass
|
37
|
+
|
38
|
+
# Django's request.user caches in this attribute on first access. We only
|
39
|
+
# want to track the user if the application code has touched request.user
|
40
|
+
# because touching it causes session access, which adds "Cookie" to the
|
41
|
+
# "Vary" header.
|
42
|
+
user = getattr(request, "_cached_user", None)
|
43
|
+
if user is not None:
|
44
|
+
try:
|
45
|
+
tracked_request.tag("username", user.get_username())
|
46
|
+
except Exception:
|
47
|
+
pass
|
48
|
+
|
49
|
+
tracked_request.tag("urlconf", get_urlconf(settings.ROOT_URLCONF))
|
50
|
+
|
51
|
+
|
52
|
+
class MiddlewareTimingMiddleware(object):
|
53
|
+
"""
|
54
|
+
Insert as early into the Middleware stack as possible (outermost layers),
|
55
|
+
so that other middlewares called after can be timed.
|
56
|
+
"""
|
57
|
+
|
58
|
+
def __init__(self, get_response):
|
59
|
+
self.get_response = get_response
|
60
|
+
|
61
|
+
def __call__(self, request):
|
62
|
+
if not scout_config.value("monitor"):
|
63
|
+
return self.get_response(request)
|
64
|
+
|
65
|
+
tracked_request = TrackedRequest.instance()
|
66
|
+
|
67
|
+
queue_time = request.META.get("HTTP_X_QUEUE_START") or request.META.get(
|
68
|
+
"HTTP_X_REQUEST_START", ""
|
69
|
+
)
|
70
|
+
track_request_queue_time(queue_time, tracked_request)
|
71
|
+
|
72
|
+
with tracked_request.span(
|
73
|
+
operation="Middleware",
|
74
|
+
should_capture_backtrace=False,
|
75
|
+
):
|
76
|
+
return self.get_response(request)
|
77
|
+
|
78
|
+
|
79
|
+
class ViewTimingMiddleware(object):
|
80
|
+
"""
|
81
|
+
Insert as deep into the middleware stack as possible, ideally wrapping no
|
82
|
+
other middleware. Designed to time the View itself
|
83
|
+
"""
|
84
|
+
|
85
|
+
def __init__(self, get_response):
|
86
|
+
self.get_response = get_response
|
87
|
+
|
88
|
+
def __call__(self, request):
|
89
|
+
"""
|
90
|
+
Wrap a single incoming request with start and stop calls.
|
91
|
+
This will start timing, but relies on the process_view callback to
|
92
|
+
capture more details about what view was really called, and other
|
93
|
+
similar info.
|
94
|
+
|
95
|
+
If process_view isn't called, then the request will not
|
96
|
+
be recorded. This can happen if a middleware further along the stack
|
97
|
+
doesn't call onward, and instead returns a response directly.
|
98
|
+
"""
|
99
|
+
if not scout_config.value("monitor"):
|
100
|
+
return self.get_response(request)
|
101
|
+
|
102
|
+
tracked_request = TrackedRequest.instance()
|
103
|
+
|
104
|
+
# This operation name won't be recorded unless changed later in
|
105
|
+
# process_view
|
106
|
+
with tracked_request.span(operation="Unknown", should_capture_backtrace=False):
|
107
|
+
response = self.get_response(request)
|
108
|
+
track_request_view_data(request, tracked_request)
|
109
|
+
if 500 <= response.status_code <= 599:
|
110
|
+
tracked_request.tag("error", "true")
|
111
|
+
return response
|
112
|
+
|
113
|
+
def process_view(self, request, view_func, view_args, view_kwargs):
|
114
|
+
"""
|
115
|
+
Capture details about the view_func that is about to execute
|
116
|
+
"""
|
117
|
+
if not scout_config.value("monitor"):
|
118
|
+
return
|
119
|
+
tracked_request = TrackedRequest.instance()
|
120
|
+
tracked_request.is_real_request = True
|
121
|
+
|
122
|
+
span = tracked_request.current_span()
|
123
|
+
if span is not None:
|
124
|
+
span.operation = get_controller_name(request)
|
125
|
+
tracked_request.operation = span.operation
|
126
|
+
|
127
|
+
def process_exception(self, request, exception):
|
128
|
+
"""
|
129
|
+
Mark this request as having errored out
|
130
|
+
|
131
|
+
Does not modify or catch or otherwise change the exception thrown
|
132
|
+
"""
|
133
|
+
if not scout_config.value("monitor"):
|
134
|
+
return
|
135
|
+
TrackedRequest.instance().tag("error", "true")
|
136
|
+
|
137
|
+
|
138
|
+
class OldStyleMiddlewareTimingMiddleware(object):
|
139
|
+
"""
|
140
|
+
Insert as early into the Middleware stack as possible (outermost layers),
|
141
|
+
so that other middlewares called after can be timed.
|
142
|
+
"""
|
143
|
+
|
144
|
+
def process_request(self, request):
|
145
|
+
if not scout_config.value("monitor"):
|
146
|
+
return
|
147
|
+
tracked_request = TrackedRequest.instance()
|
148
|
+
request._scout_tracked_request = tracked_request
|
149
|
+
|
150
|
+
queue_time = request.META.get("HTTP_X_QUEUE_START") or request.META.get(
|
151
|
+
"HTTP_X_REQUEST_START", ""
|
152
|
+
)
|
153
|
+
track_request_queue_time(queue_time, tracked_request)
|
154
|
+
|
155
|
+
tracked_request.start_span(
|
156
|
+
operation="Middleware", should_capture_backtrace=False
|
157
|
+
)
|
158
|
+
|
159
|
+
def process_response(self, request, response):
|
160
|
+
# Only stop span if there's a request, but presume we are balanced,
|
161
|
+
# i.e. that custom instrumentation within the application is not
|
162
|
+
# causing errors
|
163
|
+
tracked_request = getattr(request, "_scout_tracked_request", None)
|
164
|
+
if tracked_request is not None:
|
165
|
+
if 500 <= response.status_code <= 599:
|
166
|
+
tracked_request.tag("error", "true")
|
167
|
+
tracked_request.stop_span()
|
168
|
+
return response
|
169
|
+
|
170
|
+
|
171
|
+
class OldStyleViewMiddleware(object):
|
172
|
+
def process_view(self, request, view_func, view_func_args, view_func_kwargs):
|
173
|
+
tracked_request = getattr(request, "_scout_tracked_request", None)
|
174
|
+
if tracked_request is None:
|
175
|
+
# Looks like OldStyleMiddlewareTimingMiddleware didn't run, so
|
176
|
+
# don't do anything
|
177
|
+
return
|
178
|
+
|
179
|
+
tracked_request.is_real_request = True
|
180
|
+
|
181
|
+
span = tracked_request.start_span(
|
182
|
+
operation=get_controller_name(request), should_capture_backtrace=False
|
183
|
+
)
|
184
|
+
# Save the span into the request, so we can check
|
185
|
+
# if we're matched up when stopping
|
186
|
+
request._scout_view_span = span
|
187
|
+
|
188
|
+
def process_response(self, request, response):
|
189
|
+
tracked_request = getattr(request, "_scout_tracked_request", None)
|
190
|
+
if tracked_request is None:
|
191
|
+
# Looks like OldStyleMiddlewareTimingMiddleware didn't run, so
|
192
|
+
# don't do anything
|
193
|
+
return response
|
194
|
+
|
195
|
+
track_request_view_data(request, tracked_request)
|
196
|
+
|
197
|
+
# Only stop span if we started, but presume we are balanced, i.e. that
|
198
|
+
# custom instrumentation within the application is not causing errors
|
199
|
+
span = getattr(request, "_scout_view_span", None)
|
200
|
+
if span is not None:
|
201
|
+
tracked_request.stop_span()
|
202
|
+
return response
|
203
|
+
|
204
|
+
def process_exception(self, request, exception):
|
205
|
+
tracked_request = getattr(request, "_scout_tracked_request", None)
|
206
|
+
if tracked_request is None:
|
207
|
+
# Looks like OldStyleMiddlewareTimingMiddleware didn't run, so
|
208
|
+
# don't do anything
|
209
|
+
return
|
210
|
+
|
211
|
+
tracked_request.tag("error", "true")
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
from scout_apm.core.web_requests import RequestComponents
|
4
|
+
|
5
|
+
|
6
|
+
def get_controller_name(request):
|
7
|
+
view_func = request.resolver_match.func
|
8
|
+
view_name = request.resolver_match._func_path
|
9
|
+
if hasattr(view_func, "view_class"):
|
10
|
+
view_func = view_func.view_class
|
11
|
+
view_name = "{}.{}".format(view_func.__module__, view_func.__name__)
|
12
|
+
|
13
|
+
django_admin_components = _get_django_admin_components(view_func)
|
14
|
+
if django_admin_components:
|
15
|
+
view_name = "{}.{}.{}".format(
|
16
|
+
django_admin_components.module,
|
17
|
+
django_admin_components.controller,
|
18
|
+
django_admin_components.action,
|
19
|
+
)
|
20
|
+
|
21
|
+
django_rest_framework_components = _get_django_rest_framework_components(
|
22
|
+
request, view_func
|
23
|
+
)
|
24
|
+
if django_rest_framework_components is not None:
|
25
|
+
view_name = "{}.{}.{}".format(
|
26
|
+
django_rest_framework_components.module,
|
27
|
+
django_rest_framework_components.controller,
|
28
|
+
django_rest_framework_components.action,
|
29
|
+
)
|
30
|
+
|
31
|
+
# Seems to be a Tastypie Resource. Need to resort to some stack inspection
|
32
|
+
# to find a better name since its decorators don't wrap very well
|
33
|
+
if view_name == "tastypie.resources.wrapper":
|
34
|
+
tastypie_components = _get_tastypie_components(request, view_func)
|
35
|
+
if tastypie_components is not None:
|
36
|
+
view_name = "{}.{}.{}".format(
|
37
|
+
tastypie_components.module,
|
38
|
+
tastypie_components.controller,
|
39
|
+
tastypie_components.action,
|
40
|
+
)
|
41
|
+
|
42
|
+
return "Controller/{}".format(view_name)
|
43
|
+
|
44
|
+
|
45
|
+
def get_request_components(request):
|
46
|
+
if not request.resolver_match:
|
47
|
+
return None
|
48
|
+
view_func = request.resolver_match.func
|
49
|
+
view_name = request.resolver_match._func_path
|
50
|
+
if hasattr(view_func, "view_class"):
|
51
|
+
view_func = view_func.view_class
|
52
|
+
request_components = RequestComponents(
|
53
|
+
module=view_func.__module__,
|
54
|
+
controller=view_func.__name__,
|
55
|
+
action=request.method,
|
56
|
+
)
|
57
|
+
|
58
|
+
django_admin_components = _get_django_admin_components(view_func)
|
59
|
+
if django_admin_components:
|
60
|
+
request_components = django_admin_components
|
61
|
+
|
62
|
+
django_rest_framework_components = _get_django_rest_framework_components(
|
63
|
+
request, view_func
|
64
|
+
)
|
65
|
+
if django_rest_framework_components is not None:
|
66
|
+
request_components = django_rest_framework_components
|
67
|
+
|
68
|
+
# Seems to be a Tastypie Resource. Need to resort to some stack inspection
|
69
|
+
# to find a better name since its decorators don't wrap very well
|
70
|
+
if view_name == "tastypie.resources.wrapper":
|
71
|
+
tastypie_components = _get_tastypie_components(request, view_func)
|
72
|
+
if tastypie_components is not None:
|
73
|
+
request_components = tastypie_components
|
74
|
+
return request_components
|
75
|
+
|
76
|
+
|
77
|
+
def _get_django_admin_components(view_func):
|
78
|
+
if hasattr(view_func, "model_admin"):
|
79
|
+
# Seems to comes from Django admin (attribute only set on Django 1.9+)
|
80
|
+
admin_class = view_func.model_admin.__class__
|
81
|
+
return RequestComponents(
|
82
|
+
module=admin_class.__module__,
|
83
|
+
controller=admin_class.__name__,
|
84
|
+
action=view_func.__name__,
|
85
|
+
)
|
86
|
+
return None
|
87
|
+
|
88
|
+
|
89
|
+
def _get_django_rest_framework_components(request, view_func):
|
90
|
+
try:
|
91
|
+
from rest_framework.viewsets import ViewSetMixin
|
92
|
+
except ImportError:
|
93
|
+
return None
|
94
|
+
|
95
|
+
kls = getattr(view_func, "cls", None)
|
96
|
+
if isinstance(kls, type) and not issubclass(kls, ViewSetMixin):
|
97
|
+
return None
|
98
|
+
|
99
|
+
# Get 'actions' set in ViewSetMixin.as_view
|
100
|
+
actions = getattr(view_func, "actions", None)
|
101
|
+
if not actions or not isinstance(actions, dict):
|
102
|
+
return None
|
103
|
+
|
104
|
+
method_lower = request.method.lower()
|
105
|
+
if method_lower not in actions:
|
106
|
+
return None
|
107
|
+
|
108
|
+
return RequestComponents(
|
109
|
+
module=view_func.__module__,
|
110
|
+
controller=view_func.__name__,
|
111
|
+
action=actions[method_lower],
|
112
|
+
)
|
113
|
+
|
114
|
+
|
115
|
+
def _get_tastypie_components(request, view_func):
|
116
|
+
try:
|
117
|
+
from tastypie.resources import Resource
|
118
|
+
except ImportError:
|
119
|
+
return None
|
120
|
+
|
121
|
+
try:
|
122
|
+
wrapper = view_func.__wrapped__
|
123
|
+
except AttributeError:
|
124
|
+
return None
|
125
|
+
|
126
|
+
if not hasattr(wrapper, "__closure__") or len(wrapper.__closure__) != 2:
|
127
|
+
return None
|
128
|
+
|
129
|
+
instance = wrapper.__closure__[0].cell_contents
|
130
|
+
if not isinstance(instance, Resource): # pragma: no cover
|
131
|
+
return None
|
132
|
+
|
133
|
+
method_name = wrapper.__closure__[1].cell_contents
|
134
|
+
if not isinstance(method_name, str): # pragma: no cover
|
135
|
+
return None
|
136
|
+
|
137
|
+
if method_name.startswith("dispatch_"): # pragma: no cover
|
138
|
+
method_name = request.method.lower() + method_name.split("dispatch", 1)[1]
|
139
|
+
|
140
|
+
return RequestComponents(
|
141
|
+
module=instance.__module__,
|
142
|
+
controller=instance.__class__.__name__,
|
143
|
+
action=method_name,
|
144
|
+
)
|