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
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import logging
|
4
|
+
|
5
|
+
import psutil
|
6
|
+
|
7
|
+
logger = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
|
10
|
+
def get_rss_in_mb():
|
11
|
+
rss_in_bytes = psutil.Process().memory_info().rss
|
12
|
+
return rss_in_bytes / (1024 * 1024)
|
13
|
+
|
14
|
+
|
15
|
+
class Memory(object):
|
16
|
+
metric_type = "Memory"
|
17
|
+
metric_name = "Physical"
|
18
|
+
human_name = "Process Memory"
|
19
|
+
|
20
|
+
def run(self):
|
21
|
+
value = get_rss_in_mb()
|
22
|
+
logger.debug("%s: #%s", self.human_name, value)
|
23
|
+
return value
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import datetime as dt
|
4
|
+
import logging
|
5
|
+
import os
|
6
|
+
import threading
|
7
|
+
|
8
|
+
from scout_apm.core.agent.commands import ApplicationEvent
|
9
|
+
from scout_apm.core.agent.socket import CoreAgentSocketThread
|
10
|
+
from scout_apm.core.samplers.cpu import Cpu
|
11
|
+
from scout_apm.core.samplers.memory import Memory
|
12
|
+
from scout_apm.core.threading import SingletonThread
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class SamplersThread(SingletonThread):
|
18
|
+
_instance_lock = threading.Lock()
|
19
|
+
_stop_event = threading.Event()
|
20
|
+
|
21
|
+
def run(self):
|
22
|
+
logger.debug("Starting Samplers.")
|
23
|
+
instances = [Cpu(), Memory()]
|
24
|
+
|
25
|
+
while True:
|
26
|
+
for instance in instances:
|
27
|
+
event_value = instance.run()
|
28
|
+
if event_value is not None:
|
29
|
+
event_type = instance.metric_type + "/" + instance.metric_name
|
30
|
+
event = ApplicationEvent(
|
31
|
+
event_value=event_value,
|
32
|
+
event_type=event_type,
|
33
|
+
timestamp=dt.datetime.now(dt.timezone.utc),
|
34
|
+
source="Pid: " + str(os.getpid()),
|
35
|
+
)
|
36
|
+
CoreAgentSocketThread.send(event)
|
37
|
+
|
38
|
+
should_stop = self._stop_event.wait(timeout=60)
|
39
|
+
if should_stop:
|
40
|
+
logger.debug("Stopping Samplers.")
|
41
|
+
break
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import wrapt
|
4
|
+
|
5
|
+
from scout_apm.core.tracked_request import TrackedRequest
|
6
|
+
|
7
|
+
|
8
|
+
def trace_method(cls, method_name=None):
|
9
|
+
def decorator(info_func):
|
10
|
+
method_to_patch = method_name or info_func.__name__
|
11
|
+
|
12
|
+
@wrapt.decorator
|
13
|
+
def wrapper(wrapped, instance, args, kwargs):
|
14
|
+
entry_type, detail = info_func(instance, *args, **kwargs)
|
15
|
+
operation = entry_type
|
16
|
+
if detail["name"] is not None:
|
17
|
+
operation = operation + "/" + detail["name"]
|
18
|
+
|
19
|
+
tracked_request = TrackedRequest.instance()
|
20
|
+
with tracked_request.span(operation=operation) as span:
|
21
|
+
for key, value in detail.items():
|
22
|
+
span.tag(key, value)
|
23
|
+
|
24
|
+
return wrapped(*args, **kwargs)
|
25
|
+
|
26
|
+
setattr(cls, method_to_patch, wrapper(getattr(cls, method_to_patch)))
|
27
|
+
|
28
|
+
return wrapper
|
29
|
+
|
30
|
+
return decorator
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import threading
|
4
|
+
|
5
|
+
|
6
|
+
class SingletonThread(threading.Thread):
|
7
|
+
_instance = None
|
8
|
+
# Copy these variables into subclasses to avoid sharing:
|
9
|
+
# (Would use __init_subclass__() but Python 2 doesn't support it and using
|
10
|
+
# metaclasses to achieve the same is a lot of faff.)
|
11
|
+
# _instance_lock = threading.Lock()
|
12
|
+
# _stop_event = threading.Event()
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
def ensure_started(cls):
|
16
|
+
instance = cls._instance
|
17
|
+
if instance is not None and instance.is_alive():
|
18
|
+
# No need to grab the lock
|
19
|
+
return
|
20
|
+
with cls._instance_lock:
|
21
|
+
if cls._instance is None or not cls._instance.is_alive():
|
22
|
+
cls._instance = cls()
|
23
|
+
cls._instance.start()
|
24
|
+
|
25
|
+
@classmethod
|
26
|
+
def ensure_stopped(cls):
|
27
|
+
if cls._instance is None:
|
28
|
+
# No need to grab the lock
|
29
|
+
return
|
30
|
+
with cls._instance_lock:
|
31
|
+
if cls._instance is None:
|
32
|
+
# Nothing to stop
|
33
|
+
return
|
34
|
+
elif not cls._instance.is_alive():
|
35
|
+
# Thread died
|
36
|
+
cls._instance = None
|
37
|
+
return
|
38
|
+
|
39
|
+
# Signal stopping
|
40
|
+
cls._stop_event.set()
|
41
|
+
cls._on_stop()
|
42
|
+
cls._instance.join()
|
43
|
+
|
44
|
+
cls._instance = None
|
45
|
+
cls._stop_event.clear()
|
46
|
+
|
47
|
+
@classmethod
|
48
|
+
def _on_stop(cls):
|
49
|
+
"""
|
50
|
+
Hook to allow subclasses to add extra behaviour to stopping.
|
51
|
+
"""
|
52
|
+
pass
|
53
|
+
|
54
|
+
def __init__(self, *args, **kwargs):
|
55
|
+
super(SingletonThread, self).__init__(*args, **kwargs)
|
56
|
+
self.daemon = True
|
@@ -0,0 +1,328 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import datetime as dt
|
4
|
+
import logging
|
5
|
+
from contextlib import contextmanager
|
6
|
+
from uuid import uuid4
|
7
|
+
|
8
|
+
from scout_apm.core import backtrace, objtrace
|
9
|
+
from scout_apm.core.agent.commands import BatchCommand
|
10
|
+
from scout_apm.core.agent.socket import CoreAgentSocketThread
|
11
|
+
from scout_apm.core.config import scout_config
|
12
|
+
from scout_apm.core.n_plus_one_tracker import NPlusOneTracker
|
13
|
+
from scout_apm.core.sampler import Sampler
|
14
|
+
from scout_apm.core.samplers.memory import get_rss_in_mb
|
15
|
+
from scout_apm.core.samplers.thread import SamplersThread
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class TrackedRequest(object):
|
21
|
+
"""
|
22
|
+
This is a container which keeps track of all module instances for a single
|
23
|
+
request. For convenience they are made available as attributes based on
|
24
|
+
their keyname
|
25
|
+
"""
|
26
|
+
|
27
|
+
_sampler = None
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def get_sampler(cls):
|
31
|
+
if cls._sampler is None:
|
32
|
+
cls._sampler = Sampler(scout_config)
|
33
|
+
return cls._sampler
|
34
|
+
|
35
|
+
__slots__ = (
|
36
|
+
"sampler",
|
37
|
+
"request_id",
|
38
|
+
"start_time",
|
39
|
+
"end_time",
|
40
|
+
"active_spans",
|
41
|
+
"complete_spans",
|
42
|
+
"tags",
|
43
|
+
"is_real_request",
|
44
|
+
"_memory_start",
|
45
|
+
"n_plus_one_tracker",
|
46
|
+
"hit_max",
|
47
|
+
"sent",
|
48
|
+
"operation",
|
49
|
+
)
|
50
|
+
|
51
|
+
# Stop adding new spans at this point, to avoid exhausting memory
|
52
|
+
MAX_COMPLETE_SPANS = 1500
|
53
|
+
|
54
|
+
@classmethod
|
55
|
+
def instance(cls):
|
56
|
+
from scout_apm.core.context import context
|
57
|
+
|
58
|
+
return context.get_tracked_request()
|
59
|
+
|
60
|
+
def __init__(self):
|
61
|
+
self.request_id = "req-" + str(uuid4())
|
62
|
+
self.start_time = dt.datetime.now(dt.timezone.utc)
|
63
|
+
self.end_time = None
|
64
|
+
self.active_spans = []
|
65
|
+
self.complete_spans = []
|
66
|
+
self.tags = {}
|
67
|
+
self.is_real_request = False
|
68
|
+
self._memory_start = get_rss_in_mb()
|
69
|
+
self.n_plus_one_tracker = NPlusOneTracker()
|
70
|
+
self.hit_max = False
|
71
|
+
self.sent = False
|
72
|
+
self.operation = None
|
73
|
+
logger.debug("Starting request: %s", self.request_id)
|
74
|
+
|
75
|
+
def __repr__(self):
|
76
|
+
# Incomplete to avoid TMI
|
77
|
+
return "<TrackedRequest(request_id={}, tags={})>".format(
|
78
|
+
repr(self.request_id), repr(self.tags)
|
79
|
+
)
|
80
|
+
|
81
|
+
def tag(self, key, value):
|
82
|
+
if key in self.tags:
|
83
|
+
logger.debug(
|
84
|
+
"Overwriting previously set tag for request %s: %s",
|
85
|
+
self.request_id,
|
86
|
+
key,
|
87
|
+
)
|
88
|
+
self.tags[key] = value
|
89
|
+
|
90
|
+
def start_span(
|
91
|
+
self,
|
92
|
+
operation,
|
93
|
+
ignore=False,
|
94
|
+
ignore_children=False,
|
95
|
+
should_capture_backtrace=True,
|
96
|
+
):
|
97
|
+
parent = self.current_span()
|
98
|
+
if parent is not None:
|
99
|
+
parent_id = parent.span_id
|
100
|
+
if parent.ignore_children:
|
101
|
+
ignore = True
|
102
|
+
ignore_children = True
|
103
|
+
else:
|
104
|
+
parent_id = None
|
105
|
+
|
106
|
+
if len(self.complete_spans) >= self.MAX_COMPLETE_SPANS:
|
107
|
+
if not self.hit_max:
|
108
|
+
logger.debug(
|
109
|
+
"Hit the maximum number of spans, this trace will be incomplete."
|
110
|
+
)
|
111
|
+
self.hit_max = True
|
112
|
+
ignore = True
|
113
|
+
ignore_children = True
|
114
|
+
|
115
|
+
new_span = Span(
|
116
|
+
request_id=self.request_id,
|
117
|
+
operation=operation,
|
118
|
+
ignore=ignore,
|
119
|
+
ignore_children=ignore_children,
|
120
|
+
parent=parent_id,
|
121
|
+
should_capture_backtrace=should_capture_backtrace,
|
122
|
+
)
|
123
|
+
self.active_spans.append(new_span)
|
124
|
+
return new_span
|
125
|
+
|
126
|
+
def stop_span(self):
|
127
|
+
try:
|
128
|
+
stopping_span = self.active_spans.pop()
|
129
|
+
except IndexError as exc:
|
130
|
+
logger.debug("Exception when stopping span", exc_info=exc)
|
131
|
+
else:
|
132
|
+
stopping_span.stop()
|
133
|
+
if not stopping_span.ignore:
|
134
|
+
stopping_span.annotate()
|
135
|
+
self.complete_spans.append(stopping_span)
|
136
|
+
|
137
|
+
if len(self.active_spans) == 0:
|
138
|
+
self.finish()
|
139
|
+
|
140
|
+
@contextmanager
|
141
|
+
def span(self, *args, **kwargs):
|
142
|
+
span = self.start_span(*args, **kwargs)
|
143
|
+
try:
|
144
|
+
yield span
|
145
|
+
finally:
|
146
|
+
self.stop_span()
|
147
|
+
|
148
|
+
def current_span(self):
|
149
|
+
if self.active_spans:
|
150
|
+
return self.active_spans[-1]
|
151
|
+
else:
|
152
|
+
return None
|
153
|
+
|
154
|
+
# Request is done, release any info we have about it.
|
155
|
+
def finish(self):
|
156
|
+
from scout_apm.core.context import context
|
157
|
+
|
158
|
+
logger.debug("Stopping request: %s", self.request_id)
|
159
|
+
if self.end_time is None:
|
160
|
+
self.end_time = dt.datetime.now(dt.timezone.utc)
|
161
|
+
|
162
|
+
if self.is_real_request:
|
163
|
+
if not self.sent and self.get_sampler().should_sample(
|
164
|
+
self.operation, self.is_ignored()
|
165
|
+
):
|
166
|
+
self.tag("mem_delta", self._get_mem_delta())
|
167
|
+
self.sent = True
|
168
|
+
batch_command = BatchCommand.from_tracked_request(self)
|
169
|
+
if scout_config.value("log_payload_content"):
|
170
|
+
logger.debug(
|
171
|
+
"Sending request: %s. Payload: %s",
|
172
|
+
self.request_id,
|
173
|
+
batch_command.message(),
|
174
|
+
)
|
175
|
+
else:
|
176
|
+
logger.debug("Sending request: %s.", self.request_id)
|
177
|
+
CoreAgentSocketThread.send(batch_command)
|
178
|
+
SamplersThread.ensure_started()
|
179
|
+
|
180
|
+
details = " ".join(
|
181
|
+
"{}={}".format(key, value)
|
182
|
+
for key, value in [
|
183
|
+
("start_time", self.start_time),
|
184
|
+
("end_time", self.end_time),
|
185
|
+
("duration", (self.end_time - self.start_time).total_seconds()),
|
186
|
+
("active_spans", len(self.active_spans)),
|
187
|
+
("complete_spans", len(self.complete_spans)),
|
188
|
+
("tags", len(self.tags)),
|
189
|
+
("hit_max", self.hit_max),
|
190
|
+
("is_real_request", self.is_real_request),
|
191
|
+
("sent", self.sent),
|
192
|
+
]
|
193
|
+
)
|
194
|
+
logger.debug("Request %s %s", self.request_id, details)
|
195
|
+
context.clear_tracked_request(self)
|
196
|
+
|
197
|
+
def _get_mem_delta(self):
|
198
|
+
current_mem = get_rss_in_mb()
|
199
|
+
if current_mem > self._memory_start:
|
200
|
+
return current_mem - self._memory_start
|
201
|
+
return 0.0
|
202
|
+
|
203
|
+
# A request is ignored if the tag "ignore_transaction" is set to True
|
204
|
+
def is_ignored(self):
|
205
|
+
return self.tags.get("ignore_transaction", False)
|
206
|
+
|
207
|
+
|
208
|
+
class Span(object):
|
209
|
+
__slots__ = (
|
210
|
+
"span_id",
|
211
|
+
"start_time",
|
212
|
+
"end_time",
|
213
|
+
"request_id",
|
214
|
+
"operation",
|
215
|
+
"ignore",
|
216
|
+
"ignore_children",
|
217
|
+
"parent",
|
218
|
+
"tags",
|
219
|
+
"start_objtrace_counts",
|
220
|
+
"end_objtrace_counts",
|
221
|
+
"should_capture_backtrace",
|
222
|
+
)
|
223
|
+
|
224
|
+
def __init__(
|
225
|
+
self,
|
226
|
+
request_id=None,
|
227
|
+
operation=None,
|
228
|
+
ignore=False,
|
229
|
+
ignore_children=False,
|
230
|
+
parent=None,
|
231
|
+
should_capture_backtrace=True,
|
232
|
+
):
|
233
|
+
self.span_id = "span-" + str(uuid4())
|
234
|
+
self.start_time = dt.datetime.now(dt.timezone.utc)
|
235
|
+
self.end_time = None
|
236
|
+
self.request_id = request_id
|
237
|
+
self.operation = operation
|
238
|
+
self.ignore = ignore
|
239
|
+
self.ignore_children = ignore_children
|
240
|
+
self.parent = parent
|
241
|
+
self.tags = {}
|
242
|
+
self.start_objtrace_counts = objtrace.get_counts()
|
243
|
+
self.end_objtrace_counts = (0, 0, 0, 0)
|
244
|
+
self.should_capture_backtrace = should_capture_backtrace
|
245
|
+
|
246
|
+
def __repr__(self):
|
247
|
+
# Incomplete to avoid TMI
|
248
|
+
return "<Span(span_id={}, operation={}, ignore={}, tags={})>".format(
|
249
|
+
repr(self.span_id), repr(self.operation), repr(self.ignore), repr(self.tags)
|
250
|
+
)
|
251
|
+
|
252
|
+
def stop(self):
|
253
|
+
self.end_time = dt.datetime.now(dt.timezone.utc)
|
254
|
+
self.end_objtrace_counts = objtrace.get_counts()
|
255
|
+
|
256
|
+
def tag(self, key, value):
|
257
|
+
if key in self.tags:
|
258
|
+
logger.debug(
|
259
|
+
"Overwriting previously set tag for span %s: %s", self.span_id, key
|
260
|
+
)
|
261
|
+
self.tags[key] = value
|
262
|
+
|
263
|
+
# In seconds
|
264
|
+
def duration(self):
|
265
|
+
if self.end_time is not None:
|
266
|
+
return (self.end_time - self.start_time).total_seconds()
|
267
|
+
else:
|
268
|
+
# Current, running duration
|
269
|
+
return (
|
270
|
+
dt.datetime.now(tz=dt.timezone.utc) - self.start_time
|
271
|
+
).total_seconds()
|
272
|
+
|
273
|
+
# Add any interesting annotations to the span. Assumes that we are in the
|
274
|
+
# process of stopping this span.
|
275
|
+
def annotate(self):
|
276
|
+
self.add_allocation_tags()
|
277
|
+
if not self.should_capture_backtrace:
|
278
|
+
return
|
279
|
+
slow_threshold = 0.5
|
280
|
+
if self.duration() > slow_threshold:
|
281
|
+
self.capture_backtrace()
|
282
|
+
|
283
|
+
def add_allocation_tags(self):
|
284
|
+
if not objtrace.is_extension:
|
285
|
+
logger.debug("Not adding allocation tags: extension not loaded")
|
286
|
+
return
|
287
|
+
|
288
|
+
start_allocs = (
|
289
|
+
self.start_objtrace_counts[0]
|
290
|
+
+ self.start_objtrace_counts[1]
|
291
|
+
+ self.start_objtrace_counts[2]
|
292
|
+
)
|
293
|
+
end_allocs = (
|
294
|
+
self.end_objtrace_counts[0]
|
295
|
+
+ self.end_objtrace_counts[1]
|
296
|
+
+ self.end_objtrace_counts[2]
|
297
|
+
)
|
298
|
+
|
299
|
+
# If even one of the counters rolled over, we're pretty much
|
300
|
+
# guaranteed to have end_allocs be less than start_allocs.
|
301
|
+
# This should rarely happen. Max Unsigned Long Long is a big number
|
302
|
+
if end_allocs - start_allocs < 0:
|
303
|
+
logger.debug(
|
304
|
+
"End allocation count smaller than start allocation "
|
305
|
+
"count for span %s: start = %d, end = %d",
|
306
|
+
self.span_id,
|
307
|
+
start_allocs,
|
308
|
+
end_allocs,
|
309
|
+
)
|
310
|
+
return
|
311
|
+
|
312
|
+
self.tag("allocations", end_allocs - start_allocs)
|
313
|
+
self.tag("start_allocations", start_allocs)
|
314
|
+
self.tag("stop_allocations", end_allocs)
|
315
|
+
|
316
|
+
def capture_backtrace(self):
|
317
|
+
# The core-agent will trim the full_path as necessary.
|
318
|
+
self.tag(
|
319
|
+
"stack",
|
320
|
+
[
|
321
|
+
{
|
322
|
+
"file": frame["full_path"],
|
323
|
+
"line": frame["line"],
|
324
|
+
"function": frame["function"],
|
325
|
+
}
|
326
|
+
for frame in backtrace.capture_backtrace()
|
327
|
+
],
|
328
|
+
)
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
from scout_apm.compat import parse_qsl, urlencode
|
4
|
+
from scout_apm.core.config import scout_config
|
5
|
+
from scout_apm.core.queue_time import track_request_queue_time
|
6
|
+
|
7
|
+
# Originally derived from:
|
8
|
+
# 1. Rails:
|
9
|
+
# https://github.com/rails/rails/blob/0196551e6039ca864d1eee1e01819fcae12c1dc9/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt # noqa
|
10
|
+
# 2. Sentry server side scrubbing:
|
11
|
+
# https://docs.sentry.io/data-management/sensitive-data/#server-side-scrubbing
|
12
|
+
FILTER_PARAMETERS = frozenset(
|
13
|
+
[
|
14
|
+
"access",
|
15
|
+
"access_token",
|
16
|
+
"api_key",
|
17
|
+
"apikey",
|
18
|
+
"auth",
|
19
|
+
"auth_token",
|
20
|
+
"card[number]",
|
21
|
+
"certificate",
|
22
|
+
"credentials",
|
23
|
+
"crypt",
|
24
|
+
"key",
|
25
|
+
"mysql_pwd",
|
26
|
+
"otp",
|
27
|
+
"passwd",
|
28
|
+
"password",
|
29
|
+
"private",
|
30
|
+
"protected",
|
31
|
+
"salt",
|
32
|
+
"secret",
|
33
|
+
"secret_key",
|
34
|
+
"ssn",
|
35
|
+
"stripetoken",
|
36
|
+
"token",
|
37
|
+
]
|
38
|
+
)
|
39
|
+
|
40
|
+
|
41
|
+
def create_filtered_path(path, query_params):
|
42
|
+
if scout_config.value("uri_reporting") == "path":
|
43
|
+
return path
|
44
|
+
filtered_params = sorted(
|
45
|
+
[
|
46
|
+
(
|
47
|
+
str(key).encode("utf-8"),
|
48
|
+
# Apply str again to cover the None case.
|
49
|
+
str(filter_element(key, value)).encode("utf-8"),
|
50
|
+
)
|
51
|
+
for key, value in query_params
|
52
|
+
]
|
53
|
+
)
|
54
|
+
if not filtered_params:
|
55
|
+
return path
|
56
|
+
return path + "?" + urlencode(filtered_params)
|
57
|
+
|
58
|
+
|
59
|
+
def filter_element(key, value):
|
60
|
+
"""
|
61
|
+
Filter an individual key/value element of sensitive content. If the
|
62
|
+
value is a dictionary, recursively filter the keys in that dictionary.
|
63
|
+
|
64
|
+
Can be used recursively on a dict with:
|
65
|
+
|
66
|
+
filter_element('', {"foo": "bar"})
|
67
|
+
"""
|
68
|
+
is_sensitive = str(key).lower() in FILTER_PARAMETERS
|
69
|
+
|
70
|
+
if is_sensitive:
|
71
|
+
filtered = "[FILTERED]"
|
72
|
+
elif isinstance(value, dict):
|
73
|
+
# Python 2 unicode compatibility: force all keys and values to bytes
|
74
|
+
#
|
75
|
+
# We expect query_params to have keys and values both of strings, because
|
76
|
+
# that's how frameworks build it. However sometimes application code
|
77
|
+
# mutates this structure to use incorrect types, or we are filtering a
|
78
|
+
# different collection, so we have to cautiously make everything a string
|
79
|
+
# again. Ignoring the possibilities of bytes or objects with bad __str__
|
80
|
+
# methods because they seem very unlikely.
|
81
|
+
filtered = {str(k): filter_element(k, v) for k, v in value.items()}
|
82
|
+
elif isinstance(value, list):
|
83
|
+
filtered = [filter_element("", v) for v in value]
|
84
|
+
elif isinstance(value, set):
|
85
|
+
filtered = {filter_element("", v) for v in value}
|
86
|
+
elif isinstance(value, tuple):
|
87
|
+
filtered = tuple([filter_element("", v) for v in value])
|
88
|
+
elif value is None:
|
89
|
+
filtered = value
|
90
|
+
else:
|
91
|
+
filtered = str(value)
|
92
|
+
|
93
|
+
return filtered
|
94
|
+
|
95
|
+
|
96
|
+
def ignore_path(path):
|
97
|
+
ignored_paths = scout_config.value("ignore")
|
98
|
+
for ignored in ignored_paths:
|
99
|
+
if path.lstrip(" /").startswith(ignored):
|
100
|
+
return True
|
101
|
+
return False
|
102
|
+
|
103
|
+
|
104
|
+
def asgi_track_request_data(scope, tracked_request):
|
105
|
+
"""
|
106
|
+
Track request data from an ASGI HTTP or Websocket scope.
|
107
|
+
"""
|
108
|
+
path = scope.get("root_path", "") + scope["path"]
|
109
|
+
query_params = parse_qsl(scope.get("query_string", b"").decode("utf-8"))
|
110
|
+
tracked_request.tag("path", create_filtered_path(path, query_params))
|
111
|
+
if ignore_path(path):
|
112
|
+
tracked_request.tag("ignore_transaction", True)
|
113
|
+
|
114
|
+
# We only care about the last values of headers so don't care that we use
|
115
|
+
# a plain dict rather than a multi-value dict
|
116
|
+
headers = {k.lower(): v for k, v in scope.get("headers", ())}
|
117
|
+
|
118
|
+
if scout_config.value("collect_remote_ip"):
|
119
|
+
user_ip = (
|
120
|
+
headers.get(b"x-forwarded-for", b"").decode("latin1").split(",")[0]
|
121
|
+
or headers.get(b"client-ip", b"").decode("latin1").split(",")[0]
|
122
|
+
or scope.get("client", ("",))[0]
|
123
|
+
)
|
124
|
+
tracked_request.tag("user_ip", user_ip)
|
125
|
+
|
126
|
+
queue_time = headers.get(b"x-queue-start", b"") or headers.get(
|
127
|
+
b"x-request-start", b""
|
128
|
+
)
|
129
|
+
track_request_queue_time(queue_time.decode("latin1"), tracked_request)
|
130
|
+
|
131
|
+
|
132
|
+
def werkzeug_track_request_data(werkzeug_request, tracked_request):
|
133
|
+
"""
|
134
|
+
Several integrations use Werkzeug requests, so share the code for
|
135
|
+
extracting common data here.
|
136
|
+
"""
|
137
|
+
path = werkzeug_request.path
|
138
|
+
tracked_request.tag(
|
139
|
+
"path", create_filtered_path(path, werkzeug_request.args.items(multi=True))
|
140
|
+
)
|
141
|
+
if ignore_path(path):
|
142
|
+
tracked_request.tag("ignore_transaction", True)
|
143
|
+
|
144
|
+
if scout_config.value("collect_remote_ip"):
|
145
|
+
# Determine a remote IP to associate with the request. The value is
|
146
|
+
# spoofable by the requester so this is not suitable to use in any
|
147
|
+
# security sensitive context.
|
148
|
+
user_ip = (
|
149
|
+
werkzeug_request.headers.get("x-forwarded-for", default="").split(",")[0]
|
150
|
+
or werkzeug_request.headers.get("client-ip", default="").split(",")[0]
|
151
|
+
or werkzeug_request.remote_addr
|
152
|
+
)
|
153
|
+
tracked_request.tag("user_ip", user_ip)
|
154
|
+
|
155
|
+
queue_time = werkzeug_request.headers.get(
|
156
|
+
"x-queue-start", default=""
|
157
|
+
) or werkzeug_request.headers.get("x-request-start", default="")
|
158
|
+
track_request_queue_time(queue_time, tracked_request)
|
159
|
+
|
160
|
+
|
161
|
+
class RequestComponents(object):
|
162
|
+
__slots__ = ("module", "controller", "action")
|
163
|
+
|
164
|
+
def __init__(self, module, controller, action):
|
165
|
+
self.module = module
|
166
|
+
self.controller = controller
|
167
|
+
self.action = action
|