scout-apm 3.3.0__cp38-cp38-musllinux_1_2_i686.whl

Sign up to get free protection for your applications and to get access to all the features.
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"