scout-apm 3.3.0__cp310-cp310-musllinux_1_2_x86_64.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-310-x86_64-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 +94 -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,404 @@
1
+ # coding=utf-8
2
+
3
+ import logging
4
+ import os
5
+ import re
6
+ import warnings
7
+ from typing import Any, Dict, List, Optional, Union
8
+
9
+ from scout_apm.core import platform_detection
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ key_regex = re.compile(r"[a-zA-Z0-9]{16}")
14
+
15
+
16
+ class ScoutConfig(object):
17
+ """
18
+ Configuration object for the ScoutApm agent.
19
+
20
+ Contains a list of configuration "layers". When a configuration key is
21
+ looked up, each layer is asked in turn if it knows the value. The first one
22
+ to answer affirmatively returns the value.
23
+ """
24
+
25
+ def __init__(self):
26
+ self.layers = [
27
+ Env(),
28
+ Python(),
29
+ Derived(self),
30
+ Defaults(),
31
+ Null(),
32
+ ]
33
+
34
+ def value(self, key: str) -> Any:
35
+ value = self.locate_layer_for_key(key).value(key)
36
+ if key in CONVERSIONS:
37
+ return CONVERSIONS[key](value)
38
+ return value
39
+
40
+ def locate_layer_for_key(self, key: str) -> Any:
41
+ for layer in self.layers:
42
+ if layer.has_config(key):
43
+ return layer
44
+
45
+ # Should be unreachable because Null returns None for all keys.
46
+ raise ValueError("key {!r} not found in any layer".format(key))
47
+
48
+ def log(self) -> None:
49
+ logger.debug("Configuration Loaded:")
50
+ for key in self.known_keys:
51
+ if key in self.secret_keys:
52
+ continue
53
+
54
+ layer = self.locate_layer_for_key(key)
55
+ logger.debug(
56
+ "%-9s: %s = %s",
57
+ layer.__class__.__name__,
58
+ key,
59
+ layer.value(key),
60
+ )
61
+
62
+ known_keys = [
63
+ "app_server",
64
+ "application_root",
65
+ "collect_remote_ip",
66
+ "core_agent_config_file",
67
+ "core_agent_dir",
68
+ "core_agent_download",
69
+ "core_agent_launch",
70
+ "core_agent_log_file",
71
+ "core_agent_log_level",
72
+ "core_agent_permissions",
73
+ "core_agent_socket_path",
74
+ "core_agent_version",
75
+ "disabled_instruments",
76
+ "download_url",
77
+ "framework",
78
+ "framework_version",
79
+ "hostname",
80
+ "ignore", # Deprecated in favor of ignore_endpoints
81
+ "ignore_endpoints",
82
+ "ignore_jobs",
83
+ "key",
84
+ "log_level",
85
+ "log_payload_content",
86
+ "monitor",
87
+ "name",
88
+ "revision_sha",
89
+ "sample_rate",
90
+ "endpoint_sample_rate",
91
+ "sample_endpoints",
92
+ "sample_jobs",
93
+ "job_sample_rate",
94
+ "scm_subdirectory",
95
+ "shutdown_message_enabled",
96
+ "shutdown_timeout_seconds",
97
+ ]
98
+
99
+ secret_keys = {"key"}
100
+
101
+ def core_agent_permissions(self) -> int:
102
+ try:
103
+ return int(str(self.value("core_agent_permissions")), 8)
104
+ except ValueError:
105
+ logger.exception(
106
+ "Invalid core_agent_permissions value, using default of 0o700"
107
+ )
108
+ return 0o700
109
+
110
+ @classmethod
111
+ def set(cls, **kwargs: Any) -> None:
112
+ """
113
+ Sets a configuration value for the Scout agent. Values set here will
114
+ not override values set in ENV.
115
+ """
116
+ for key, value in kwargs.items():
117
+ SCOUT_PYTHON_VALUES[key] = value
118
+
119
+ @classmethod
120
+ def unset(cls, *keys: str) -> None:
121
+ """
122
+ Removes a configuration value for the Scout agent.
123
+ """
124
+ for key in keys:
125
+ SCOUT_PYTHON_VALUES.pop(key, None)
126
+
127
+ @classmethod
128
+ def reset_all(cls) -> None:
129
+ """
130
+ Remove all configuration settings set via `ScoutConfig.set(...)`.
131
+
132
+ This is meant for use in testing.
133
+ """
134
+ SCOUT_PYTHON_VALUES.clear()
135
+
136
+
137
+ # Module-level data, the ScoutConfig.set(key="value") adds to this
138
+ SCOUT_PYTHON_VALUES = {}
139
+
140
+
141
+ class Python(object):
142
+ """
143
+ A configuration overlay that lets other parts of python set values.
144
+ """
145
+
146
+ def has_config(self, key: str) -> bool:
147
+ return key in SCOUT_PYTHON_VALUES
148
+
149
+ def value(self, key: str) -> Any:
150
+ return SCOUT_PYTHON_VALUES[key]
151
+
152
+
153
+ class Env(object):
154
+ """
155
+ Reads configuration from environment by prefixing the key
156
+ requested with "SCOUT_"
157
+
158
+ Example: the `key` config looks for SCOUT_KEY
159
+ environment variable
160
+ """
161
+
162
+ def has_config(self, key: str) -> bool:
163
+ env_key = self.modify_key(key)
164
+ return env_key in os.environ
165
+
166
+ def value(self, key: str) -> Any:
167
+ env_key = self.modify_key(key)
168
+ return os.environ[env_key]
169
+
170
+ def modify_key(self, key: str) -> str:
171
+ env_key = ("SCOUT_" + key).upper()
172
+ return env_key
173
+
174
+
175
+ class Derived(object):
176
+ """
177
+ A configuration overlay that calculates from other values.
178
+ """
179
+
180
+ def __init__(self, config: ScoutConfig):
181
+ """
182
+ config argument is the overall ScoutConfig var, so we can lookup the
183
+ components of the derived info.
184
+ """
185
+ self.config = config
186
+
187
+ def has_config(self, key: str) -> bool:
188
+ return self.lookup_func(key) is not None
189
+
190
+ def value(self, key: str) -> Any:
191
+ return self.lookup_func(key)()
192
+
193
+ def lookup_func(self, key: str) -> Optional[Any]:
194
+ """
195
+ Returns the derive_#{key} function, or None if it isn't defined
196
+ """
197
+ func_name = "derive_" + key
198
+ return getattr(self, func_name, None)
199
+
200
+ def derive_core_agent_full_name(self) -> str:
201
+ triple = self.config.value("core_agent_triple")
202
+ if not platform_detection.is_valid_triple(triple):
203
+ warnings.warn(
204
+ "Invalid value for core_agent_triple: {}".format(triple), stacklevel=2
205
+ )
206
+ return "{name}-{version}-{triple}".format(
207
+ name="scout_apm_core",
208
+ version=self.config.value("core_agent_version"),
209
+ triple=triple,
210
+ )
211
+
212
+ def derive_core_agent_triple(self) -> str:
213
+ return platform_detection.get_triple()
214
+
215
+
216
+ class Defaults(object):
217
+ """
218
+ Provides default values for important configurations
219
+ """
220
+
221
+ def __init__(self):
222
+ self.defaults = {
223
+ "app_server": "",
224
+ "application_root": os.getcwd(),
225
+ "collect_remote_ip": True,
226
+ "core_agent_dir": "/tmp/scout_apm_core",
227
+ "core_agent_download": True,
228
+ "core_agent_launch": True,
229
+ "core_agent_log_level": "info",
230
+ "core_agent_permissions": 700,
231
+ "core_agent_socket_path": "tcp://127.0.0.1:6590",
232
+ "core_agent_version": "v1.5.0", # can be an exact tag name, or 'latest'
233
+ "disabled_instruments": [],
234
+ "download_url": (
235
+ "https://s3-us-west-1.amazonaws.com/scout-public-downloads/"
236
+ "apm_core_agent/release"
237
+ ), # noqa: B950
238
+ "errors_batch_size": 5,
239
+ "errors_enabled": True,
240
+ "errors_ignored_exceptions": (),
241
+ "errors_host": "https://errors.scoutapm.com",
242
+ "framework": "",
243
+ "framework_version": "",
244
+ "hostname": None,
245
+ "ignore": [],
246
+ "ignore_endpoints": [],
247
+ "ignore_jobs": [],
248
+ "key": "",
249
+ "log_payload_content": False,
250
+ "monitor": False,
251
+ "name": "Python App",
252
+ "revision_sha": self._git_revision_sha(),
253
+ "sample_rate": 100,
254
+ "sample_endpoints": [],
255
+ "endpoint_sample_rate": None,
256
+ "sample_jobs": [],
257
+ "job_sample_rate": None,
258
+ "scm_subdirectory": "",
259
+ "shutdown_message_enabled": True,
260
+ "shutdown_timeout_seconds": 2.0,
261
+ "uri_reporting": "filtered_params",
262
+ }
263
+
264
+ def _git_revision_sha(self) -> str:
265
+ # N.B. The environment variable SCOUT_REVISION_SHA may also be used,
266
+ # but that will be picked up by Env
267
+ return os.environ.get("HEROKU_SLUG_COMMIT", "")
268
+
269
+ def has_config(self, key: str) -> bool:
270
+ return key in self.defaults
271
+
272
+ def value(self, key: str) -> Any:
273
+ return self.defaults[key]
274
+
275
+
276
+ class Null(object):
277
+ """
278
+ Always answers that a key is present, but the value is None
279
+
280
+ Used as the last step of the layered configuration.
281
+ """
282
+
283
+ def has_config(self, key: str) -> bool:
284
+ return True
285
+
286
+ def value(self, key: str) -> None:
287
+ return None
288
+
289
+
290
+ def _strip_leading_slash(path: str) -> str:
291
+ return path.lstrip(" /").strip()
292
+
293
+
294
+ def convert_to_bool(value: Any) -> bool:
295
+ if isinstance(value, bool):
296
+ return value
297
+ if isinstance(value, str):
298
+ return value.lower() in ("yes", "true", "t", "1")
299
+ # Unknown type - default to false?
300
+ return False
301
+
302
+
303
+ def convert_to_float(value: Any) -> float:
304
+ try:
305
+ return float(value)
306
+ except ValueError:
307
+ return 0.0
308
+
309
+
310
+ def convert_sample_rate(value: Any) -> Optional[int]:
311
+ """
312
+ Converts sample rate to integer, ensuring it's between 0 and 100.
313
+ Allows None as a valid value.
314
+ """
315
+ if value is None:
316
+ return None
317
+ try:
318
+ rate = int(value)
319
+ if not (0 <= rate <= 100):
320
+ logger.warning(
321
+ f"Invalid sample rate {rate}. Must be between 0 and 100. "
322
+ "Defaulting to 100."
323
+ )
324
+ return 100
325
+ return rate
326
+ except (TypeError, ValueError):
327
+ logger.warning(
328
+ f"Invalid sample rate {value}. Must be a number between 0 and 100. "
329
+ "Defaulting to 100."
330
+ )
331
+ return 100
332
+
333
+
334
+ def convert_to_list(value: Any) -> List[Any]:
335
+ if isinstance(value, list):
336
+ return value
337
+ if isinstance(value, tuple):
338
+ return list(value)
339
+ if isinstance(value, str):
340
+ # Split on commas
341
+ return [item.strip() for item in value.split(",") if item]
342
+ # Unknown type - default to empty?
343
+ return []
344
+
345
+
346
+ def convert_ignore_paths(value: Any) -> List[str]:
347
+ """
348
+ Removes leading slashes from paths and returns a list of strings.
349
+ """
350
+ raw_paths = convert_to_list(value)
351
+ return [_strip_leading_slash(path) for path in raw_paths]
352
+
353
+
354
+ def convert_endpoint_sampling(value: Union[str, Dict[str, Any]]) -> Dict[str, int]:
355
+ """
356
+ Converts endpoint sampling configuration from string or dict format
357
+ to a normalized dict.
358
+ Example: '/endpoint:40,/test:0' -> {'/endpoint': 40, '/test': 0}
359
+ """
360
+ if isinstance(value, dict):
361
+ return {_strip_leading_slash(k): int(v) for k, v in value.items()}
362
+ if isinstance(value, str):
363
+ if not value.strip():
364
+ return {}
365
+ result = {}
366
+ pairs = [pair.strip() for pair in value.split(",")]
367
+ for pair in pairs:
368
+ try:
369
+ endpoint, rate = pair.split(":")
370
+ rate_int = int(rate)
371
+ if not (0 <= rate_int <= 100):
372
+ logger.warning(
373
+ f"Invalid sampling rate {rate} for endpoint {endpoint}. "
374
+ "Must be between 0 and 100."
375
+ )
376
+ continue
377
+ result[_strip_leading_slash(endpoint)] = rate_int
378
+ except ValueError:
379
+ logger.warning(f"Invalid sampling configuration: {pair}")
380
+ continue
381
+ return result
382
+ return {}
383
+
384
+
385
+ CONVERSIONS = {
386
+ "collect_remote_ip": convert_to_bool,
387
+ "core_agent_download": convert_to_bool,
388
+ "core_agent_launch": convert_to_bool,
389
+ "disabled_instruments": convert_to_list,
390
+ "ignore": convert_ignore_paths,
391
+ "ignore_endpoints": convert_ignore_paths,
392
+ "ignore_jobs": convert_ignore_paths,
393
+ "monitor": convert_to_bool,
394
+ "sample_rate": convert_sample_rate,
395
+ "sample_endpoints": convert_endpoint_sampling,
396
+ "endpoint_sample_rate": convert_sample_rate,
397
+ "sample_jobs": convert_endpoint_sampling,
398
+ "job_sample_rate": convert_sample_rate,
399
+ "shutdown_message_enabled": convert_to_bool,
400
+ "shutdown_timeout_seconds": convert_to_float,
401
+ }
402
+
403
+
404
+ scout_config = ScoutConfig()
@@ -0,0 +1,140 @@
1
+ # coding=utf-8
2
+
3
+ import threading
4
+ import time
5
+ from threading import local as ThreadLocal
6
+
7
+ from scout_apm.core.tracked_request import TrackedRequest
8
+
9
+ try:
10
+ from asgiref.local import Local as AsgiRefLocal
11
+ except ImportError:
12
+ # Old versions of Python or asgiref < 3.1
13
+ AsgiRefLocal = None
14
+
15
+ try:
16
+ import asyncio
17
+ except ImportError:
18
+ asyncio = None
19
+
20
+ try:
21
+ from contextvars import ContextVar
22
+
23
+ scout_context_var = ContextVar("__scout_trackedrequest")
24
+ except ImportError:
25
+ scout_context_var = None
26
+
27
+
28
+ SCOUT_REQUEST_ATTR = "__scout_trackedrequest"
29
+
30
+
31
+ def get_current_asyncio_task():
32
+ """
33
+ Cross-version implementation of asyncio.current_task()
34
+ Returns None if there is no task.
35
+ """
36
+ if asyncio:
37
+ try:
38
+ if hasattr(asyncio, "current_task"):
39
+ # Python 3.7 and up
40
+ return asyncio.current_task()
41
+ else:
42
+ # Python 3.6
43
+ return asyncio.Task.current_task()
44
+ except RuntimeError:
45
+ return None
46
+
47
+
48
+ class SimplifiedAsgirefLocal:
49
+ """
50
+ A copy of asgiref 3.1+'s Local class without the sync_to_async /
51
+ async_to_sync compatibility.
52
+ """
53
+
54
+ CLEANUP_INTERVAL = 60 # seconds
55
+
56
+ def __init__(self):
57
+ self._storage = {}
58
+ self._last_cleanup = time.time()
59
+ self._clean_lock = threading.Lock()
60
+
61
+ def _get_context_id(self):
62
+ """
63
+ Get the ID we should use for looking up variables
64
+ """
65
+ # First, pull the current task if we can
66
+ context_id = get_current_asyncio_task()
67
+ # OK, let's try for a thread ID
68
+ if context_id is None:
69
+ context_id = threading.current_thread()
70
+ return context_id
71
+
72
+ def _cleanup(self):
73
+ """
74
+ Cleans up any references to dead threads or tasks
75
+ """
76
+ for key in list(self._storage.keys()):
77
+ if isinstance(key, threading.Thread):
78
+ if not key.is_alive():
79
+ del self._storage[key]
80
+ elif isinstance(key, asyncio.Task):
81
+ if key.done():
82
+ del self._storage[key]
83
+ self._last_cleanup = time.time()
84
+
85
+ def _maybe_cleanup(self):
86
+ """
87
+ Cleans up if enough time has passed
88
+ """
89
+ if time.time() - self._last_cleanup > self.CLEANUP_INTERVAL:
90
+ with self._clean_lock:
91
+ self._cleanup()
92
+
93
+ def __getattr__(self, key):
94
+ context_id = self._get_context_id()
95
+ if key in self._storage.get(context_id, {}):
96
+ return self._storage[context_id][key]
97
+ else:
98
+ raise AttributeError("%r object has no attribute %r" % (self, key))
99
+
100
+ def __setattr__(self, key, value):
101
+ if key in ("_storage", "_last_cleanup", "_clean_lock", "_thread_critical"):
102
+ return super().__setattr__(key, value)
103
+ self._maybe_cleanup()
104
+ self._storage.setdefault(self._get_context_id(), {})[key] = value
105
+
106
+ def __delattr__(self, key):
107
+ context_id = self._get_context_id()
108
+ if key in self._storage.get(context_id, {}):
109
+ del self._storage[context_id][key]
110
+ else:
111
+ raise AttributeError("%r object has no attribute %r" % (self, key))
112
+
113
+
114
+ class LocalContext(object):
115
+ def __init__(self):
116
+ if AsgiRefLocal is not None:
117
+ self._local = AsgiRefLocal()
118
+ elif asyncio is not None:
119
+ self._local = SimplifiedAsgirefLocal()
120
+ else:
121
+ self._local = ThreadLocal()
122
+ self.use_context_var = scout_context_var is not None
123
+
124
+ def get_tracked_request(self):
125
+ if scout_context_var:
126
+ if not scout_context_var.get(None):
127
+ scout_context_var.set(TrackedRequest())
128
+ return scout_context_var.get()
129
+ if not hasattr(self._local, "tracked_request"):
130
+ self._local.tracked_request = TrackedRequest()
131
+ return self._local.tracked_request
132
+
133
+ def clear_tracked_request(self, instance):
134
+ if getattr(self._local, "tracked_request", None) is instance:
135
+ del self._local.tracked_request
136
+ if scout_context_var and scout_context_var.get(None) is instance:
137
+ scout_context_var.set(None)
138
+
139
+
140
+ context = LocalContext()
@@ -0,0 +1,95 @@
1
+ # coding=utf-8
2
+
3
+ import logging
4
+ import os
5
+
6
+ from scout_apm.core.backtrace import capture_stacktrace
7
+ from scout_apm.core.config import scout_config
8
+ from scout_apm.core.error_service import ErrorServiceThread
9
+ from scout_apm.core.tracked_request import TrackedRequest
10
+ from scout_apm.core.web_requests import RequestComponents, filter_element
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class ErrorMonitor(object):
16
+ @classmethod
17
+ def send(
18
+ cls,
19
+ exc_info,
20
+ request_components=None,
21
+ request_path=None,
22
+ request_params=None,
23
+ session=None,
24
+ environment=None,
25
+ custom_controller=None,
26
+ custom_params=None,
27
+ ):
28
+ if not scout_config.value("errors_enabled"):
29
+ return
30
+
31
+ exc_class, exc_value, traceback = exc_info
32
+
33
+ ignore_exceptions = scout_config.value("errors_ignored_exceptions")
34
+ if ignore_exceptions and isinstance(exc_value, tuple(ignore_exceptions)):
35
+ return
36
+
37
+ tracked_request = TrackedRequest.instance()
38
+
39
+ context = {}
40
+ context.update(tracked_request.tags)
41
+
42
+ if custom_params:
43
+ context["custom_params"] = custom_params
44
+
45
+ if custom_controller:
46
+ if request_components:
47
+ request_components.controller = custom_controller
48
+ else:
49
+ request_components = RequestComponents(
50
+ module=None, controller=custom_controller, action=None
51
+ )
52
+
53
+ scm_subdirectory = scout_config.value("scm_subdirectory")
54
+ error = {
55
+ "exception_class": exc_class.__name__,
56
+ "message": str(exc_value),
57
+ "request_id": tracked_request.request_id,
58
+ "request_uri": request_path,
59
+ "request_params": filter_element("", request_params)
60
+ if request_params
61
+ else None,
62
+ "request_session": filter_element("", session) if session else None,
63
+ "environment": filter_element("", environment) if environment else None,
64
+ "trace": [
65
+ "{file}:{line}:in {function}".format(
66
+ file=os.path.join(scm_subdirectory, frame["file"])
67
+ if scm_subdirectory
68
+ else frame["file"],
69
+ line=frame["line"],
70
+ function=frame["function"],
71
+ )
72
+ for frame in capture_stacktrace(traceback)
73
+ ],
74
+ "request_components": {
75
+ "module": request_components.module,
76
+ "controller": request_components.controller,
77
+ "action": request_components.action,
78
+ }
79
+ if request_components
80
+ else None,
81
+ "context": context,
82
+ "host": scout_config.value("hostname"),
83
+ "revision_sha": scout_config.value("revision_sha"),
84
+ }
85
+
86
+ if scout_config.value("log_payload_content"):
87
+ logger.debug(
88
+ "Sending error for request: %s. Payload: %r",
89
+ tracked_request.request_id,
90
+ error,
91
+ )
92
+ else:
93
+ logger.debug("Sending error for request: %s.", tracked_request.request_id)
94
+
95
+ ErrorServiceThread.send(error=error)