scout-apm 3.3.0__cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.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-313-aarch64-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 +6 -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
scout_apm/core/config.py
ADDED
@@ -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()
|
scout_apm/core/error.py
ADDED
@@ -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)
|