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.
Files changed (65) hide show
  1. scout_apm/__init__.py +0 -0
  2. scout_apm/api/__init__.py +197 -0
  3. scout_apm/async_/__init__.py +1 -0
  4. scout_apm/async_/api.py +41 -0
  5. scout_apm/async_/instruments/__init__.py +0 -0
  6. scout_apm/async_/instruments/jinja2.py +13 -0
  7. scout_apm/async_/starlette.py +101 -0
  8. scout_apm/bottle.py +86 -0
  9. scout_apm/celery.py +153 -0
  10. scout_apm/compat.py +104 -0
  11. scout_apm/core/__init__.py +99 -0
  12. scout_apm/core/_objtrace.cpython-38-i386-linux-gnu.so +0 -0
  13. scout_apm/core/agent/__init__.py +0 -0
  14. scout_apm/core/agent/commands.py +250 -0
  15. scout_apm/core/agent/manager.py +319 -0
  16. scout_apm/core/agent/socket.py +211 -0
  17. scout_apm/core/backtrace.py +116 -0
  18. scout_apm/core/cli/__init__.py +0 -0
  19. scout_apm/core/cli/core_agent_manager.py +32 -0
  20. scout_apm/core/config.py +404 -0
  21. scout_apm/core/context.py +140 -0
  22. scout_apm/core/error.py +95 -0
  23. scout_apm/core/error_service.py +167 -0
  24. scout_apm/core/metadata.py +66 -0
  25. scout_apm/core/n_plus_one_tracker.py +41 -0
  26. scout_apm/core/objtrace.py +24 -0
  27. scout_apm/core/platform_detection.py +66 -0
  28. scout_apm/core/queue_time.py +99 -0
  29. scout_apm/core/sampler.py +149 -0
  30. scout_apm/core/samplers/__init__.py +0 -0
  31. scout_apm/core/samplers/cpu.py +76 -0
  32. scout_apm/core/samplers/memory.py +23 -0
  33. scout_apm/core/samplers/thread.py +41 -0
  34. scout_apm/core/stacktracer.py +30 -0
  35. scout_apm/core/threading.py +56 -0
  36. scout_apm/core/tracked_request.py +328 -0
  37. scout_apm/core/web_requests.py +167 -0
  38. scout_apm/django/__init__.py +7 -0
  39. scout_apm/django/apps.py +137 -0
  40. scout_apm/django/instruments/__init__.py +0 -0
  41. scout_apm/django/instruments/huey.py +30 -0
  42. scout_apm/django/instruments/sql.py +140 -0
  43. scout_apm/django/instruments/template.py +35 -0
  44. scout_apm/django/middleware.py +211 -0
  45. scout_apm/django/request.py +144 -0
  46. scout_apm/dramatiq.py +42 -0
  47. scout_apm/falcon.py +142 -0
  48. scout_apm/flask/__init__.py +118 -0
  49. scout_apm/flask/sqlalchemy.py +28 -0
  50. scout_apm/huey.py +54 -0
  51. scout_apm/hug.py +40 -0
  52. scout_apm/instruments/__init__.py +21 -0
  53. scout_apm/instruments/elasticsearch.py +263 -0
  54. scout_apm/instruments/jinja2.py +127 -0
  55. scout_apm/instruments/pymongo.py +105 -0
  56. scout_apm/instruments/redis.py +77 -0
  57. scout_apm/instruments/urllib3.py +80 -0
  58. scout_apm/rq.py +85 -0
  59. scout_apm/sqlalchemy.py +38 -0
  60. scout_apm-3.3.0.dist-info/LICENSE +21 -0
  61. scout_apm-3.3.0.dist-info/METADATA +82 -0
  62. scout_apm-3.3.0.dist-info/RECORD +65 -0
  63. scout_apm-3.3.0.dist-info/WHEEL +5 -0
  64. scout_apm-3.3.0.dist-info/entry_points.txt +2 -0
  65. 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
@@ -0,0 +1,7 @@
1
+ # coding=utf-8
2
+
3
+ import django
4
+
5
+ if django.VERSION < (3, 2, 0):
6
+ # Only define default_app_config when using a version earlier than 3.2
7
+ default_app_config = "scout_apm.django.apps.ScoutApmDjangoConfig"