scout-apm 3.3.0__cp310-cp310-musllinux_1_2_i686.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-310-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 +94 -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
|