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.
- 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-38-i386-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 +82 -0
- scout_apm-3.3.0.dist-info/RECORD +65 -0
- scout_apm-3.3.0.dist-info/WHEEL +5 -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)
|