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/__init__.py
ADDED
File without changes
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import sys
|
4
|
+
|
5
|
+
import scout_apm.core
|
6
|
+
from scout_apm.compat import ContextDecorator, text
|
7
|
+
from scout_apm.core.config import ScoutConfig
|
8
|
+
from scout_apm.core.error import ErrorMonitor
|
9
|
+
from scout_apm.core.tracked_request import TrackedRequest
|
10
|
+
|
11
|
+
# The async_ module can only be shipped on Python 3.6+
|
12
|
+
try:
|
13
|
+
from scout_apm.async_.api import AsyncDecoratorMixin
|
14
|
+
except ImportError:
|
15
|
+
|
16
|
+
class AsyncDecoratorMixin(object):
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
__all__ = [
|
21
|
+
"BackgroundTransaction",
|
22
|
+
"Config",
|
23
|
+
"Context",
|
24
|
+
"Error",
|
25
|
+
"WebTransaction",
|
26
|
+
"install",
|
27
|
+
"instrument",
|
28
|
+
]
|
29
|
+
|
30
|
+
|
31
|
+
class Context(object):
|
32
|
+
@classmethod
|
33
|
+
def add(self, key, value):
|
34
|
+
"""Adds context to the currently executing request.
|
35
|
+
|
36
|
+
:key: Any String identifying the request context.
|
37
|
+
Example: "user_ip", "plan", "alert_count"
|
38
|
+
:value: Any json-serializable type.
|
39
|
+
Example: "1.1.1.1", "free", 100
|
40
|
+
:returns: nothing.
|
41
|
+
"""
|
42
|
+
TrackedRequest.instance().tag(key, value)
|
43
|
+
|
44
|
+
|
45
|
+
class Config(ScoutConfig):
|
46
|
+
pass
|
47
|
+
|
48
|
+
|
49
|
+
install = scout_apm.core.install
|
50
|
+
|
51
|
+
|
52
|
+
def ignore_transaction():
|
53
|
+
TrackedRequest.instance().tag("ignore_transaction", True)
|
54
|
+
|
55
|
+
|
56
|
+
class instrument(AsyncDecoratorMixin, ContextDecorator):
|
57
|
+
def __init__(self, operation, kind="Custom", tags=None):
|
58
|
+
self.operation = text(kind) + "/" + text(operation)
|
59
|
+
if tags is None:
|
60
|
+
self.tags = {}
|
61
|
+
else:
|
62
|
+
self.tags = tags
|
63
|
+
|
64
|
+
def __enter__(self):
|
65
|
+
tracked_request = TrackedRequest.instance()
|
66
|
+
self.span = tracked_request.start_span(operation=self.operation)
|
67
|
+
for key, value in self.tags.items():
|
68
|
+
self.tag(key, value)
|
69
|
+
return self
|
70
|
+
|
71
|
+
def __exit__(self, *exc):
|
72
|
+
tracked_request = TrackedRequest.instance()
|
73
|
+
tracked_request.stop_span()
|
74
|
+
return False
|
75
|
+
|
76
|
+
def tag(self, key, value):
|
77
|
+
if self.span is not None:
|
78
|
+
self.span.tag(key, value)
|
79
|
+
|
80
|
+
|
81
|
+
class Transaction(AsyncDecoratorMixin, ContextDecorator):
|
82
|
+
"""
|
83
|
+
This Class is not meant to be used directly.
|
84
|
+
Use one of the subclasses
|
85
|
+
(WebTransaction or BackgroundTransaction)
|
86
|
+
"""
|
87
|
+
|
88
|
+
def __init__(self, name, tags=None):
|
89
|
+
self.name = text(name)
|
90
|
+
if tags is None:
|
91
|
+
self.tags = {}
|
92
|
+
else:
|
93
|
+
self.tags = tags
|
94
|
+
|
95
|
+
@classmethod
|
96
|
+
def start(cls, kind, name, tags=None):
|
97
|
+
operation = text(kind) + "/" + text(name)
|
98
|
+
|
99
|
+
tracked_request = TrackedRequest.instance()
|
100
|
+
tracked_request.operation = operation
|
101
|
+
tracked_request.is_real_request = True
|
102
|
+
span = tracked_request.start_span(
|
103
|
+
operation=operation, should_capture_backtrace=False
|
104
|
+
)
|
105
|
+
if tags is not None:
|
106
|
+
for key, value in tags.items():
|
107
|
+
tracked_request.tag(key, value)
|
108
|
+
return span
|
109
|
+
|
110
|
+
@classmethod
|
111
|
+
def stop(cls):
|
112
|
+
tracked_request = TrackedRequest.instance()
|
113
|
+
tracked_request.stop_span()
|
114
|
+
return True
|
115
|
+
|
116
|
+
# __enter__ must be defined by child classes.
|
117
|
+
|
118
|
+
# *exc is any exception raised. Ignore that
|
119
|
+
def __exit__(self, *exc):
|
120
|
+
WebTransaction.stop()
|
121
|
+
return False
|
122
|
+
|
123
|
+
def tag(self, key, value):
|
124
|
+
if self.span is not None:
|
125
|
+
self.span.tag(key, value)
|
126
|
+
|
127
|
+
|
128
|
+
class WebTransaction(Transaction):
|
129
|
+
@classmethod
|
130
|
+
def start(cls, name, tags=None):
|
131
|
+
super(WebTransaction, cls).start("Controller", text(name), tags)
|
132
|
+
|
133
|
+
def __enter__(self):
|
134
|
+
super(WebTransaction, self).start("Controller", self.name, self.tags)
|
135
|
+
|
136
|
+
|
137
|
+
class BackgroundTransaction(Transaction):
|
138
|
+
@classmethod
|
139
|
+
def start(cls, name, tags=None):
|
140
|
+
super(BackgroundTransaction, cls).start("Job", text(name), tags)
|
141
|
+
|
142
|
+
def __enter__(self):
|
143
|
+
super(BackgroundTransaction, self).start("Job", self.name, self.tags)
|
144
|
+
|
145
|
+
|
146
|
+
def rename_transaction(name):
|
147
|
+
if name is not None:
|
148
|
+
tracked_request = TrackedRequest.instance()
|
149
|
+
tracked_request.tag("transaction.name", name)
|
150
|
+
|
151
|
+
|
152
|
+
class Error(object):
|
153
|
+
@classmethod
|
154
|
+
def capture(
|
155
|
+
cls,
|
156
|
+
exception,
|
157
|
+
request_path=None,
|
158
|
+
request_params=None,
|
159
|
+
session=None,
|
160
|
+
custom_controller=None,
|
161
|
+
custom_params=None,
|
162
|
+
):
|
163
|
+
"""
|
164
|
+
Capture the exception manually.
|
165
|
+
|
166
|
+
Utilizes sys.exc_info to gather the traceback. This has the side
|
167
|
+
effect that if another exception is raised before calling
|
168
|
+
``Error.capture``, the traceback will match the most recently
|
169
|
+
raised exception.
|
170
|
+
|
171
|
+
Includes any context added for the TrackedRequest.
|
172
|
+
|
173
|
+
:exception: Any exception.
|
174
|
+
:request_path: Any String identifying the relative path of the request.
|
175
|
+
Example: "/hello-world/"
|
176
|
+
:request_params: Any json-serializable dict representing the
|
177
|
+
querystring parameters.
|
178
|
+
Example: {"page": 1}
|
179
|
+
:session: Any json-serializable dict representing the
|
180
|
+
request session.
|
181
|
+
Example: {"step": 0}
|
182
|
+
:custom_controller: Any String identifying the controller or job.
|
183
|
+
Example: "send_email"
|
184
|
+
:custom_params: Any json-serializable dict.
|
185
|
+
Example: {"to": "scout@test.com", "from": "no-reply@test.com"}
|
186
|
+
:returns: nothing.
|
187
|
+
"""
|
188
|
+
if isinstance(exception, Exception):
|
189
|
+
exc_info = (exception.__class__, exception, sys.exc_info()[2])
|
190
|
+
ErrorMonitor.send(
|
191
|
+
exc_info,
|
192
|
+
request_path=request_path,
|
193
|
+
request_params=request_params,
|
194
|
+
session=session,
|
195
|
+
custom_controller=custom_controller,
|
196
|
+
custom_params=custom_params,
|
197
|
+
)
|
@@ -0,0 +1 @@
|
|
1
|
+
# coding=utf-8
|
scout_apm/async_/api.py
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
from functools import wraps
|
4
|
+
|
5
|
+
|
6
|
+
class AsyncDecoratorMixin(object):
|
7
|
+
"""Provide the ability to decorate both sync and async functions."""
|
8
|
+
|
9
|
+
is_async = False
|
10
|
+
|
11
|
+
@classmethod
|
12
|
+
def async_(cls, operation, tags=None, **kwargs):
|
13
|
+
"""
|
14
|
+
Instrument an async function via a decorator.
|
15
|
+
|
16
|
+
This will return an awaitable which must be awaited.
|
17
|
+
Using this on a synchronous function will raise a
|
18
|
+
RuntimeError.
|
19
|
+
|
20
|
+
``
|
21
|
+
@instrument.async_("Foo")
|
22
|
+
async def foo():
|
23
|
+
...
|
24
|
+
``
|
25
|
+
"""
|
26
|
+
instance = cls(operation, tags=tags, **kwargs)
|
27
|
+
instance.is_async = True
|
28
|
+
return instance
|
29
|
+
|
30
|
+
def __call__(self, func):
|
31
|
+
if self.is_async:
|
32
|
+
# Until https://bugs.python.org/issue37398 has a resolution,
|
33
|
+
# manually wrap the async function
|
34
|
+
@wraps(func)
|
35
|
+
async def decorated(*args, **kwds):
|
36
|
+
with self._recreate_cm():
|
37
|
+
return await func(*args, **kwds)
|
38
|
+
|
39
|
+
return decorated
|
40
|
+
else:
|
41
|
+
return super().__call__(func)
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import wrapt
|
4
|
+
|
5
|
+
from scout_apm.core.tracked_request import TrackedRequest
|
6
|
+
|
7
|
+
|
8
|
+
@wrapt.decorator
|
9
|
+
async def wrapped_render_async(wrapped, instance, args, kwargs):
|
10
|
+
tracked_request = TrackedRequest.instance()
|
11
|
+
with tracked_request.span(operation="Template/Render") as span:
|
12
|
+
span.tag("name", instance.name)
|
13
|
+
return await wrapped(*args, **kwargs)
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import wrapt
|
4
|
+
from starlette.background import BackgroundTask
|
5
|
+
|
6
|
+
import scout_apm.core
|
7
|
+
from scout_apm.core.tracked_request import TrackedRequest
|
8
|
+
from scout_apm.core.web_requests import asgi_track_request_data
|
9
|
+
|
10
|
+
|
11
|
+
class ScoutMiddleware:
|
12
|
+
def __init__(self, app):
|
13
|
+
self.app = app
|
14
|
+
installed = scout_apm.core.install()
|
15
|
+
self._do_nothing = not installed
|
16
|
+
if installed:
|
17
|
+
install_background_instrumentation()
|
18
|
+
|
19
|
+
async def __call__(self, scope, receive, send):
|
20
|
+
if self._do_nothing or scope["type"] != "http":
|
21
|
+
return await self.app(scope, receive, send)
|
22
|
+
|
23
|
+
tracked_request = TrackedRequest.instance()
|
24
|
+
# Assume the request is real until we determine it's not. This is useful
|
25
|
+
# when the asyncio instrumentation is determining if a new Task should
|
26
|
+
# reuse the existing tracked request.
|
27
|
+
tracked_request.is_real_request = True
|
28
|
+
# Can't name controller until post-routing - see final clause
|
29
|
+
controller_span = tracked_request.start_span(operation="Controller/Unknown")
|
30
|
+
|
31
|
+
asgi_track_request_data(scope, tracked_request)
|
32
|
+
|
33
|
+
def grab_extra_data():
|
34
|
+
if "endpoint" in scope:
|
35
|
+
# Rename top span
|
36
|
+
endpoint = scope["endpoint"]
|
37
|
+
if not hasattr(endpoint, "__qualname__"):
|
38
|
+
endpoint = endpoint.__class__
|
39
|
+
controller_span.operation = "Controller/{}.{}".format(
|
40
|
+
endpoint.__module__,
|
41
|
+
endpoint.__qualname__,
|
42
|
+
)
|
43
|
+
tracked_request.operation = controller_span.operation
|
44
|
+
else:
|
45
|
+
# Mark the request as not real
|
46
|
+
tracked_request.is_real_request = False
|
47
|
+
|
48
|
+
# From AuthenticationMiddleware - bypass request.user because it
|
49
|
+
# throws AssertionError if 'user' is not in Scope, and we need a
|
50
|
+
# try/except already
|
51
|
+
try:
|
52
|
+
username = scope["user"].display_name
|
53
|
+
except (KeyError, AttributeError):
|
54
|
+
pass
|
55
|
+
else:
|
56
|
+
tracked_request.tag("username", username)
|
57
|
+
|
58
|
+
async def wrapped_send(data):
|
59
|
+
type_ = data.get("type", None)
|
60
|
+
if type_ == "http.response.start" and 500 <= data.get("status", 200) <= 599:
|
61
|
+
tracked_request.tag("error", "true")
|
62
|
+
elif type_ == "http.response.body" and not data.get("more_body", False):
|
63
|
+
# Finish HTTP span when body finishes sending, not later (e.g.
|
64
|
+
# after background tasks)
|
65
|
+
grab_extra_data()
|
66
|
+
tracked_request.stop_span()
|
67
|
+
return await send(data)
|
68
|
+
|
69
|
+
try:
|
70
|
+
await self.app(scope, receive, wrapped_send)
|
71
|
+
except Exception as exc:
|
72
|
+
tracked_request.tag("error", "true")
|
73
|
+
raise exc
|
74
|
+
finally:
|
75
|
+
if tracked_request.end_time is None:
|
76
|
+
grab_extra_data()
|
77
|
+
tracked_request.stop_span()
|
78
|
+
|
79
|
+
|
80
|
+
background_instrumentation_installed = False
|
81
|
+
|
82
|
+
|
83
|
+
def install_background_instrumentation():
|
84
|
+
global background_instrumentation_installed
|
85
|
+
if background_instrumentation_installed:
|
86
|
+
return
|
87
|
+
background_instrumentation_installed = True
|
88
|
+
|
89
|
+
@wrapt.decorator
|
90
|
+
async def wrapped_background_call(wrapped, instance, args, kwargs):
|
91
|
+
tracked_request = TrackedRequest.instance()
|
92
|
+
tracked_request.is_real_request = True
|
93
|
+
|
94
|
+
operation = "Job/{}.{}".format(
|
95
|
+
instance.func.__module__, instance.func.__qualname__
|
96
|
+
)
|
97
|
+
tracked_request.operation = operation
|
98
|
+
with tracked_request.span(operation=operation):
|
99
|
+
return await wrapped(*args, **kwargs)
|
100
|
+
|
101
|
+
BackgroundTask.__call__ = wrapped_background_call(BackgroundTask.__call__)
|
scout_apm/bottle.py
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import wrapt
|
4
|
+
from bottle import request, response
|
5
|
+
|
6
|
+
import scout_apm.core
|
7
|
+
from scout_apm.core.config import scout_config
|
8
|
+
from scout_apm.core.queue_time import track_request_queue_time
|
9
|
+
from scout_apm.core.tracked_request import TrackedRequest
|
10
|
+
from scout_apm.core.web_requests import create_filtered_path, ignore_path
|
11
|
+
|
12
|
+
|
13
|
+
class ScoutPlugin(object):
|
14
|
+
def __init__(self):
|
15
|
+
self.name = "scout"
|
16
|
+
self.api = 2
|
17
|
+
|
18
|
+
def set_config_from_bottle(self, app):
|
19
|
+
bottle_configs = {}
|
20
|
+
prefix = "scout."
|
21
|
+
prefix_len = len(prefix)
|
22
|
+
for key, value in app.config.items():
|
23
|
+
if key.startswith(prefix) and len(key) > prefix_len:
|
24
|
+
scout_key = key[prefix_len:]
|
25
|
+
bottle_configs[scout_key] = value
|
26
|
+
scout_config.set(**bottle_configs)
|
27
|
+
|
28
|
+
def setup(self, app):
|
29
|
+
self.set_config_from_bottle(app)
|
30
|
+
installed = scout_apm.core.install()
|
31
|
+
self._do_nothing = not installed
|
32
|
+
|
33
|
+
def apply(self, callback, context):
|
34
|
+
if self._do_nothing:
|
35
|
+
return callback
|
36
|
+
return wrap_callback(callback)
|
37
|
+
|
38
|
+
|
39
|
+
@wrapt.decorator
|
40
|
+
def wrap_callback(wrapped, instance, args, kwargs):
|
41
|
+
tracked_request = TrackedRequest.instance()
|
42
|
+
tracked_request.is_real_request = True
|
43
|
+
|
44
|
+
path = request.path
|
45
|
+
# allitems() is an undocumented bottle internal
|
46
|
+
tracked_request.tag("path", create_filtered_path(path, request.query.allitems()))
|
47
|
+
if ignore_path(path):
|
48
|
+
tracked_request.tag("ignore_transaction", True)
|
49
|
+
|
50
|
+
if request.route.name is not None:
|
51
|
+
controller_name = request.route.name
|
52
|
+
else:
|
53
|
+
controller_name = request.route.rule
|
54
|
+
if controller_name == "/":
|
55
|
+
controller_name = "/home"
|
56
|
+
if not controller_name.startswith("/"):
|
57
|
+
controller_name = "/" + controller_name
|
58
|
+
|
59
|
+
if scout_config.value("collect_remote_ip"):
|
60
|
+
# Determine a remote IP to associate with the request. The
|
61
|
+
# value is spoofable by the requester so this is not suitable
|
62
|
+
# to use in any security sensitive context.
|
63
|
+
user_ip = (
|
64
|
+
request.headers.get("x-forwarded-for", "").split(",")[0]
|
65
|
+
or request.headers.get("client-ip", "").split(",")[0]
|
66
|
+
or request.environ.get("REMOTE_ADDR")
|
67
|
+
)
|
68
|
+
tracked_request.tag("user_ip", user_ip)
|
69
|
+
|
70
|
+
queue_time = request.headers.get("x-queue-start", "") or request.headers.get(
|
71
|
+
"x-request-start", ""
|
72
|
+
)
|
73
|
+
track_request_queue_time(queue_time, tracked_request)
|
74
|
+
operation = "Controller{}".format(controller_name)
|
75
|
+
|
76
|
+
with tracked_request.span(operation=operation):
|
77
|
+
tracked_request.operation = operation
|
78
|
+
try:
|
79
|
+
value = wrapped(*args, **kwargs)
|
80
|
+
except Exception:
|
81
|
+
tracked_request.tag("error", "true")
|
82
|
+
raise
|
83
|
+
else:
|
84
|
+
if 500 <= response.status_code <= 599:
|
85
|
+
tracked_request.tag("error", "true")
|
86
|
+
return value
|
scout_apm/celery.py
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import datetime as dt
|
4
|
+
import logging
|
5
|
+
|
6
|
+
from celery.signals import before_task_publish, task_failure, task_postrun, task_prerun
|
7
|
+
|
8
|
+
from scout_apm.core.queue_time import track_job_queue_time
|
9
|
+
|
10
|
+
try:
|
11
|
+
import django
|
12
|
+
from django.views.debug import SafeExceptionReporterFilter
|
13
|
+
|
14
|
+
def get_safe_settings():
|
15
|
+
return SafeExceptionReporterFilter().get_safe_settings()
|
16
|
+
|
17
|
+
except ImportError:
|
18
|
+
# Django not installed
|
19
|
+
get_safe_settings = None
|
20
|
+
|
21
|
+
import scout_apm.core
|
22
|
+
from scout_apm.compat import datetime_to_timestamp
|
23
|
+
from scout_apm.core.config import scout_config
|
24
|
+
from scout_apm.core.error import ErrorMonitor
|
25
|
+
from scout_apm.core.tracked_request import TrackedRequest
|
26
|
+
|
27
|
+
logger = logging.getLogger(__name__)
|
28
|
+
|
29
|
+
|
30
|
+
def before_task_publish_callback(headers=None, properties=None, **kwargs):
|
31
|
+
if "scout_task_start" not in headers:
|
32
|
+
headers["scout_task_start"] = datetime_to_timestamp(
|
33
|
+
dt.datetime.now(dt.timezone.utc)
|
34
|
+
)
|
35
|
+
|
36
|
+
|
37
|
+
def task_prerun_callback(task=None, **kwargs):
|
38
|
+
tracked_request = TrackedRequest.instance()
|
39
|
+
tracked_request.is_real_request = True
|
40
|
+
|
41
|
+
start_time_header = getattr(task.request, "scout_task_start", None)
|
42
|
+
track_job_queue_time(start_time_header, tracked_request)
|
43
|
+
|
44
|
+
task_id = getattr(task.request, "id", None)
|
45
|
+
if task_id:
|
46
|
+
tracked_request.tag("task_id", task_id)
|
47
|
+
parent_task_id = getattr(task.request, "parent_id", None)
|
48
|
+
if parent_task_id:
|
49
|
+
tracked_request.tag("parent_task_id", parent_task_id)
|
50
|
+
|
51
|
+
delivery_info = getattr(task.request, "delivery_info", None)
|
52
|
+
if delivery_info:
|
53
|
+
tracked_request.tag("is_eager", delivery_info.get("is_eager", False))
|
54
|
+
tracked_request.tag("exchange", delivery_info.get("exchange", "unknown"))
|
55
|
+
tracked_request.tag("priority", delivery_info.get("priority", "unknown"))
|
56
|
+
tracked_request.tag("routing_key", delivery_info.get("routing_key", "unknown"))
|
57
|
+
tracked_request.tag("queue", delivery_info.get("queue", "unknown"))
|
58
|
+
|
59
|
+
operation = "Job/" + task.name
|
60
|
+
tracked_request.start_span(operation=operation)
|
61
|
+
tracked_request.operation = operation
|
62
|
+
|
63
|
+
|
64
|
+
def task_postrun_callback(task=None, **kwargs):
|
65
|
+
tracked_request = TrackedRequest.instance()
|
66
|
+
tracked_request.stop_span()
|
67
|
+
|
68
|
+
|
69
|
+
def task_failure_callback(
|
70
|
+
sender,
|
71
|
+
task_id=None,
|
72
|
+
exception=None,
|
73
|
+
args=None,
|
74
|
+
kwargs=None,
|
75
|
+
traceback=None,
|
76
|
+
einfo=None,
|
77
|
+
**remaining,
|
78
|
+
):
|
79
|
+
tracked_request = TrackedRequest.instance()
|
80
|
+
tracked_request.tag("error", "true")
|
81
|
+
|
82
|
+
custom_controller = sender.name
|
83
|
+
custom_params = {
|
84
|
+
"celery": {
|
85
|
+
"task_id": task_id,
|
86
|
+
"args": args,
|
87
|
+
"kwargs": kwargs,
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
# Look up the django settings if populated.
|
92
|
+
environment = None
|
93
|
+
if get_safe_settings:
|
94
|
+
try:
|
95
|
+
environment = get_safe_settings()
|
96
|
+
except django.core.exceptions.ImproperlyConfigured as exc:
|
97
|
+
# Django not setup correctly
|
98
|
+
logger.debug(
|
99
|
+
"Celery integration does not have django configured properly: %r", exc
|
100
|
+
)
|
101
|
+
pass
|
102
|
+
except Exception as exc:
|
103
|
+
logger.debug(
|
104
|
+
"Celery task_failure callback exception: %r", exc, exc_info=exc
|
105
|
+
)
|
106
|
+
pass
|
107
|
+
|
108
|
+
# Celery occassionally will send the traceback as a string rather
|
109
|
+
# than a Stack trace object as the docs indicate. In that case,
|
110
|
+
# fall back to the billiard ExceptionInfo instance
|
111
|
+
traceback = traceback if traceback and not isinstance(traceback, str) else einfo.tb
|
112
|
+
exc_info = (exception.__class__, exception, traceback)
|
113
|
+
ErrorMonitor.send(
|
114
|
+
exc_info,
|
115
|
+
environment=environment,
|
116
|
+
custom_params=custom_params,
|
117
|
+
custom_controller=custom_controller,
|
118
|
+
)
|
119
|
+
|
120
|
+
|
121
|
+
def install(app=None):
|
122
|
+
if app is not None:
|
123
|
+
copy_configuration(app)
|
124
|
+
|
125
|
+
installed = scout_apm.core.install()
|
126
|
+
if not installed:
|
127
|
+
return
|
128
|
+
|
129
|
+
before_task_publish.connect(before_task_publish_callback)
|
130
|
+
task_prerun.connect(task_prerun_callback)
|
131
|
+
task_failure.connect(task_failure_callback)
|
132
|
+
task_postrun.connect(task_postrun_callback)
|
133
|
+
|
134
|
+
|
135
|
+
def copy_configuration(app):
|
136
|
+
prefix = "scout_"
|
137
|
+
prefix_len = len(prefix)
|
138
|
+
|
139
|
+
to_set = {}
|
140
|
+
for key, value in app.conf.items():
|
141
|
+
key_lower = key.lower()
|
142
|
+
if key_lower.startswith(prefix) and len(key_lower) > prefix_len:
|
143
|
+
scout_key = key_lower[prefix_len:]
|
144
|
+
to_set[scout_key] = value
|
145
|
+
|
146
|
+
scout_config.set(**to_set)
|
147
|
+
|
148
|
+
|
149
|
+
def uninstall():
|
150
|
+
before_task_publish.disconnect(before_task_publish_callback)
|
151
|
+
task_prerun.disconnect(task_prerun_callback)
|
152
|
+
task_postrun.disconnect(task_postrun_callback)
|
153
|
+
task_failure.disconnect(task_failure_callback)
|
scout_apm/compat.py
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import datetime as dt
|
4
|
+
import gzip
|
5
|
+
import inspect
|
6
|
+
import queue
|
7
|
+
from contextlib import ContextDecorator
|
8
|
+
from functools import wraps
|
9
|
+
from html import escape
|
10
|
+
from urllib.parse import parse_qsl, urlencode, urljoin
|
11
|
+
|
12
|
+
import certifi
|
13
|
+
import urllib3
|
14
|
+
|
15
|
+
|
16
|
+
def iteritems(dictionary):
|
17
|
+
return dictionary.items()
|
18
|
+
|
19
|
+
|
20
|
+
# datetime_to_timestamp converts a naive UTC datetime to a unix timestamp
|
21
|
+
def datetime_to_timestamp(datetime_obj):
|
22
|
+
return datetime_obj.replace(tzinfo=dt.timezone.utc).timestamp()
|
23
|
+
|
24
|
+
|
25
|
+
def text(value, encoding="utf-8", errors="strict"):
|
26
|
+
"""
|
27
|
+
Convert a value to str on Python 3 and unicode on Python 2.
|
28
|
+
"""
|
29
|
+
if isinstance(value, str):
|
30
|
+
return value
|
31
|
+
elif isinstance(value, bytes):
|
32
|
+
return str(value, encoding, errors)
|
33
|
+
else:
|
34
|
+
return str(value)
|
35
|
+
|
36
|
+
|
37
|
+
def get_pos_args(func):
|
38
|
+
return inspect.getfullargspec(func).args
|
39
|
+
|
40
|
+
|
41
|
+
def unwrap_decorators(func):
|
42
|
+
unwrapped = func
|
43
|
+
while True:
|
44
|
+
# N.B. only some decorators set __wrapped__ on Python 2.7
|
45
|
+
try:
|
46
|
+
unwrapped = unwrapped.__wrapped__
|
47
|
+
except AttributeError:
|
48
|
+
break
|
49
|
+
return unwrapped
|
50
|
+
|
51
|
+
|
52
|
+
def kwargs_only(func):
|
53
|
+
"""
|
54
|
+
Source: https://pypi.org/project/kwargs-only/
|
55
|
+
Make a function only accept keyword arguments.
|
56
|
+
This can be dropped in Python 3 in lieu of:
|
57
|
+
def foo(*, bar=default):
|
58
|
+
Source: https://pypi.org/project/kwargs-only/
|
59
|
+
"""
|
60
|
+
if hasattr(inspect, "signature"): # pragma: no cover
|
61
|
+
# Python 3
|
62
|
+
signature = inspect.signature(func)
|
63
|
+
arg_names = list(signature.parameters.keys())
|
64
|
+
else: # pragma: no cover
|
65
|
+
# Python 2
|
66
|
+
signature = inspect.getargspec(func)
|
67
|
+
arg_names = signature.args
|
68
|
+
|
69
|
+
if len(arg_names) > 0 and arg_names[0] in ("self", "cls"):
|
70
|
+
allowable_args = 1
|
71
|
+
else:
|
72
|
+
allowable_args = 0
|
73
|
+
|
74
|
+
@wraps(func)
|
75
|
+
def wrapper(*args, **kwargs):
|
76
|
+
if len(args) > allowable_args:
|
77
|
+
raise TypeError(
|
78
|
+
"{} should only be called with keyword args".format(func.__name__)
|
79
|
+
)
|
80
|
+
return func(*args, **kwargs)
|
81
|
+
|
82
|
+
return wrapper
|
83
|
+
|
84
|
+
|
85
|
+
def urllib3_cert_pool_manager(**kwargs):
|
86
|
+
return urllib3.PoolManager(cert_reqs="CERT_REQUIRED", ca_certs=certifi.where())
|
87
|
+
|
88
|
+
|
89
|
+
def gzip_compress(data):
|
90
|
+
return gzip.compress(data)
|
91
|
+
|
92
|
+
|
93
|
+
__all__ = [
|
94
|
+
"ContextDecorator",
|
95
|
+
"datetime_to_timestamp",
|
96
|
+
"escape",
|
97
|
+
"gzip_compress",
|
98
|
+
"kwargs_only",
|
99
|
+
"parse_qsl",
|
100
|
+
"queue",
|
101
|
+
"text",
|
102
|
+
"urlencode",
|
103
|
+
"urljoin",
|
104
|
+
]
|