scout-apm 3.3.0__cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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-313-aarch64-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 +94 -0
- scout_apm-3.3.0.dist-info/RECORD +65 -0
- scout_apm-3.3.0.dist-info/WHEEL +7 -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
|
+
)
|