scout-apm 3.3.0__cp38-cp38-musllinux_1_2_x86_64.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-38-x86_64-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
|
+
]
|