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,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)