scout-apm 3.2.1__cp310-cp310-musllinux_1_2_x86_64.whl → 3.4.0__cp310-cp310-musllinux_1_2_x86_64.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/celery.py +3 -1
- scout_apm/core/_objtrace.cpython-310-x86_64-linux-gnu.so +0 -0
- scout_apm/core/agent/commands.py +20 -7
- scout_apm/core/config.py +122 -29
- scout_apm/core/metadata.py +3 -3
- scout_apm/core/queue_time.py +1 -1
- scout_apm/core/sampler.py +149 -0
- scout_apm/core/samplers/cpu.py +2 -2
- scout_apm/core/samplers/thread.py +1 -1
- scout_apm/core/tracked_request.py +21 -7
- scout_apm/core/web_requests.py +1 -1
- scout_apm/rq.py +11 -1
- {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info}/METADATA +16 -3
- {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info}/RECORD +49 -48
- {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info}/WHEEL +1 -1
- {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info}/entry_points.txt +0 -0
- {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info/licenses}/LICENSE +0 -0
- {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info}/top_level.txt +0 -0
scout_apm/celery.py
CHANGED
@@ -29,7 +29,9 @@ logger = logging.getLogger(__name__)
|
|
29
29
|
|
30
30
|
def before_task_publish_callback(headers=None, properties=None, **kwargs):
|
31
31
|
if "scout_task_start" not in headers:
|
32
|
-
headers["scout_task_start"] = datetime_to_timestamp(
|
32
|
+
headers["scout_task_start"] = datetime_to_timestamp(
|
33
|
+
dt.datetime.now(dt.timezone.utc)
|
34
|
+
)
|
33
35
|
|
34
36
|
|
35
37
|
def task_prerun_callback(task=None, **kwargs):
|
Binary file
|
scout_apm/core/agent/commands.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# coding=utf-8
|
2
2
|
|
3
|
+
import datetime as dt
|
3
4
|
import logging
|
4
5
|
import re
|
5
6
|
|
@@ -10,6 +11,18 @@ logger = logging.getLogger(__name__)
|
|
10
11
|
key_regex = re.compile(r"^[a-zA-Z0-9]{20}$")
|
11
12
|
|
12
13
|
|
14
|
+
def format_dt_for_core_agent(event_time: dt.datetime) -> str:
|
15
|
+
"""
|
16
|
+
Returns expected format for Core Agent compatibility.
|
17
|
+
Coerce any tz-aware datetime to UTC just in case.
|
18
|
+
"""
|
19
|
+
# if we somehow got a naive datetime, convert it to UTC
|
20
|
+
if event_time.tzinfo is None:
|
21
|
+
logger.warning("Naive datetime passed to format_dt_for_core_agent")
|
22
|
+
event_time = event_time.astimezone(dt.timezone.utc)
|
23
|
+
return event_time.isoformat()
|
24
|
+
|
25
|
+
|
13
26
|
class Register(object):
|
14
27
|
__slots__ = ("app", "key", "hostname")
|
15
28
|
|
@@ -49,7 +62,7 @@ class StartSpan(object):
|
|
49
62
|
def message(self):
|
50
63
|
return {
|
51
64
|
"StartSpan": {
|
52
|
-
"timestamp": self.timestamp
|
65
|
+
"timestamp": format_dt_for_core_agent(self.timestamp),
|
53
66
|
"request_id": self.request_id,
|
54
67
|
"span_id": self.span_id,
|
55
68
|
"parent_id": self.parent,
|
@@ -69,7 +82,7 @@ class StopSpan(object):
|
|
69
82
|
def message(self):
|
70
83
|
return {
|
71
84
|
"StopSpan": {
|
72
|
-
"timestamp": self.timestamp
|
85
|
+
"timestamp": format_dt_for_core_agent(self.timestamp),
|
73
86
|
"request_id": self.request_id,
|
74
87
|
"span_id": self.span_id,
|
75
88
|
}
|
@@ -86,7 +99,7 @@ class StartRequest(object):
|
|
86
99
|
def message(self):
|
87
100
|
return {
|
88
101
|
"StartRequest": {
|
89
|
-
"timestamp": self.timestamp
|
102
|
+
"timestamp": format_dt_for_core_agent(self.timestamp),
|
90
103
|
"request_id": self.request_id,
|
91
104
|
}
|
92
105
|
}
|
@@ -102,7 +115,7 @@ class FinishRequest(object):
|
|
102
115
|
def message(self):
|
103
116
|
return {
|
104
117
|
"FinishRequest": {
|
105
|
-
"timestamp": self.timestamp
|
118
|
+
"timestamp": format_dt_for_core_agent(self.timestamp),
|
106
119
|
"request_id": self.request_id,
|
107
120
|
}
|
108
121
|
}
|
@@ -121,7 +134,7 @@ class TagSpan(object):
|
|
121
134
|
def message(self):
|
122
135
|
return {
|
123
136
|
"TagSpan": {
|
124
|
-
"timestamp": self.timestamp
|
137
|
+
"timestamp": format_dt_for_core_agent(self.timestamp),
|
125
138
|
"request_id": self.request_id,
|
126
139
|
"span_id": self.span_id,
|
127
140
|
"tag": self.tag,
|
@@ -142,7 +155,7 @@ class TagRequest(object):
|
|
142
155
|
def message(self):
|
143
156
|
return {
|
144
157
|
"TagRequest": {
|
145
|
-
"timestamp": self.timestamp
|
158
|
+
"timestamp": format_dt_for_core_agent(self.timestamp),
|
146
159
|
"request_id": self.request_id,
|
147
160
|
"tag": self.tag,
|
148
161
|
"value": self.value,
|
@@ -162,7 +175,7 @@ class ApplicationEvent(object):
|
|
162
175
|
def message(self):
|
163
176
|
return {
|
164
177
|
"ApplicationEvent": {
|
165
|
-
"timestamp": self.timestamp
|
178
|
+
"timestamp": format_dt_for_core_agent(self.timestamp),
|
166
179
|
"event_type": self.event_type,
|
167
180
|
"event_value": self.event_value,
|
168
181
|
"source": self.source,
|
scout_apm/core/config.py
CHANGED
@@ -4,6 +4,7 @@ import logging
|
|
4
4
|
import os
|
5
5
|
import re
|
6
6
|
import warnings
|
7
|
+
from typing import Any, Dict, List, Optional, Union
|
7
8
|
|
8
9
|
from scout_apm.core import platform_detection
|
9
10
|
|
@@ -30,13 +31,13 @@ class ScoutConfig(object):
|
|
30
31
|
Null(),
|
31
32
|
]
|
32
33
|
|
33
|
-
def value(self, key):
|
34
|
+
def value(self, key: str) -> Any:
|
34
35
|
value = self.locate_layer_for_key(key).value(key)
|
35
36
|
if key in CONVERSIONS:
|
36
37
|
return CONVERSIONS[key](value)
|
37
38
|
return value
|
38
39
|
|
39
|
-
def locate_layer_for_key(self, key):
|
40
|
+
def locate_layer_for_key(self, key: str) -> Any:
|
40
41
|
for layer in self.layers:
|
41
42
|
if layer.has_config(key):
|
42
43
|
return layer
|
@@ -44,7 +45,7 @@ class ScoutConfig(object):
|
|
44
45
|
# Should be unreachable because Null returns None for all keys.
|
45
46
|
raise ValueError("key {!r} not found in any layer".format(key))
|
46
47
|
|
47
|
-
def log(self):
|
48
|
+
def log(self) -> None:
|
48
49
|
logger.debug("Configuration Loaded:")
|
49
50
|
for key in self.known_keys:
|
50
51
|
if key in self.secret_keys:
|
@@ -76,13 +77,20 @@ class ScoutConfig(object):
|
|
76
77
|
"framework",
|
77
78
|
"framework_version",
|
78
79
|
"hostname",
|
79
|
-
"ignore",
|
80
|
+
"ignore", # Deprecated in favor of ignore_endpoints
|
81
|
+
"ignore_endpoints",
|
82
|
+
"ignore_jobs",
|
80
83
|
"key",
|
81
84
|
"log_level",
|
82
85
|
"log_payload_content",
|
83
86
|
"monitor",
|
84
87
|
"name",
|
85
88
|
"revision_sha",
|
89
|
+
"sample_rate",
|
90
|
+
"endpoint_sample_rate",
|
91
|
+
"sample_endpoints",
|
92
|
+
"sample_jobs",
|
93
|
+
"job_sample_rate",
|
86
94
|
"scm_subdirectory",
|
87
95
|
"shutdown_message_enabled",
|
88
96
|
"shutdown_timeout_seconds",
|
@@ -90,7 +98,7 @@ class ScoutConfig(object):
|
|
90
98
|
|
91
99
|
secret_keys = {"key"}
|
92
100
|
|
93
|
-
def core_agent_permissions(self):
|
101
|
+
def core_agent_permissions(self) -> int:
|
94
102
|
try:
|
95
103
|
return int(str(self.value("core_agent_permissions")), 8)
|
96
104
|
except ValueError:
|
@@ -100,7 +108,7 @@ class ScoutConfig(object):
|
|
100
108
|
return 0o700
|
101
109
|
|
102
110
|
@classmethod
|
103
|
-
def set(cls, **kwargs):
|
111
|
+
def set(cls, **kwargs: Any) -> None:
|
104
112
|
"""
|
105
113
|
Sets a configuration value for the Scout agent. Values set here will
|
106
114
|
not override values set in ENV.
|
@@ -109,7 +117,7 @@ class ScoutConfig(object):
|
|
109
117
|
SCOUT_PYTHON_VALUES[key] = value
|
110
118
|
|
111
119
|
@classmethod
|
112
|
-
def unset(cls, *keys):
|
120
|
+
def unset(cls, *keys: str) -> None:
|
113
121
|
"""
|
114
122
|
Removes a configuration value for the Scout agent.
|
115
123
|
"""
|
@@ -117,7 +125,7 @@ class ScoutConfig(object):
|
|
117
125
|
SCOUT_PYTHON_VALUES.pop(key, None)
|
118
126
|
|
119
127
|
@classmethod
|
120
|
-
def reset_all(cls):
|
128
|
+
def reset_all(cls) -> None:
|
121
129
|
"""
|
122
130
|
Remove all configuration settings set via `ScoutConfig.set(...)`.
|
123
131
|
|
@@ -135,10 +143,10 @@ class Python(object):
|
|
135
143
|
A configuration overlay that lets other parts of python set values.
|
136
144
|
"""
|
137
145
|
|
138
|
-
def has_config(self, key):
|
146
|
+
def has_config(self, key: str) -> bool:
|
139
147
|
return key in SCOUT_PYTHON_VALUES
|
140
148
|
|
141
|
-
def value(self, key):
|
149
|
+
def value(self, key: str) -> Any:
|
142
150
|
return SCOUT_PYTHON_VALUES[key]
|
143
151
|
|
144
152
|
|
@@ -151,15 +159,15 @@ class Env(object):
|
|
151
159
|
environment variable
|
152
160
|
"""
|
153
161
|
|
154
|
-
def has_config(self, key):
|
162
|
+
def has_config(self, key: str) -> bool:
|
155
163
|
env_key = self.modify_key(key)
|
156
164
|
return env_key in os.environ
|
157
165
|
|
158
|
-
def value(self, key):
|
166
|
+
def value(self, key: str) -> Any:
|
159
167
|
env_key = self.modify_key(key)
|
160
168
|
return os.environ[env_key]
|
161
169
|
|
162
|
-
def modify_key(self, key):
|
170
|
+
def modify_key(self, key: str) -> str:
|
163
171
|
env_key = ("SCOUT_" + key).upper()
|
164
172
|
return env_key
|
165
173
|
|
@@ -169,27 +177,27 @@ class Derived(object):
|
|
169
177
|
A configuration overlay that calculates from other values.
|
170
178
|
"""
|
171
179
|
|
172
|
-
def __init__(self, config):
|
180
|
+
def __init__(self, config: ScoutConfig):
|
173
181
|
"""
|
174
182
|
config argument is the overall ScoutConfig var, so we can lookup the
|
175
183
|
components of the derived info.
|
176
184
|
"""
|
177
185
|
self.config = config
|
178
186
|
|
179
|
-
def has_config(self, key):
|
187
|
+
def has_config(self, key: str) -> bool:
|
180
188
|
return self.lookup_func(key) is not None
|
181
189
|
|
182
|
-
def value(self, key):
|
190
|
+
def value(self, key: str) -> Any:
|
183
191
|
return self.lookup_func(key)()
|
184
192
|
|
185
|
-
def lookup_func(self, key):
|
193
|
+
def lookup_func(self, key: str) -> Optional[Any]:
|
186
194
|
"""
|
187
195
|
Returns the derive_#{key} function, or None if it isn't defined
|
188
196
|
"""
|
189
197
|
func_name = "derive_" + key
|
190
198
|
return getattr(self, func_name, None)
|
191
199
|
|
192
|
-
def derive_core_agent_full_name(self):
|
200
|
+
def derive_core_agent_full_name(self) -> str:
|
193
201
|
triple = self.config.value("core_agent_triple")
|
194
202
|
if not platform_detection.is_valid_triple(triple):
|
195
203
|
warnings.warn(
|
@@ -201,7 +209,7 @@ class Derived(object):
|
|
201
209
|
triple=triple,
|
202
210
|
)
|
203
211
|
|
204
|
-
def derive_core_agent_triple(self):
|
212
|
+
def derive_core_agent_triple(self) -> str:
|
205
213
|
return platform_detection.get_triple()
|
206
214
|
|
207
215
|
|
@@ -223,7 +231,10 @@ class Defaults(object):
|
|
223
231
|
"core_agent_socket_path": "tcp://127.0.0.1:6590",
|
224
232
|
"core_agent_version": "v1.5.0", # can be an exact tag name, or 'latest'
|
225
233
|
"disabled_instruments": [],
|
226
|
-
"download_url":
|
234
|
+
"download_url": (
|
235
|
+
"https://s3-us-west-1.amazonaws.com/scout-public-downloads/"
|
236
|
+
"apm_core_agent/release"
|
237
|
+
), # noqa: B950
|
227
238
|
"errors_batch_size": 5,
|
228
239
|
"errors_enabled": True,
|
229
240
|
"errors_ignored_exceptions": (),
|
@@ -231,26 +242,34 @@ class Defaults(object):
|
|
231
242
|
"framework": "",
|
232
243
|
"framework_version": "",
|
233
244
|
"hostname": None,
|
245
|
+
"ignore": [],
|
246
|
+
"ignore_endpoints": [],
|
247
|
+
"ignore_jobs": [],
|
234
248
|
"key": "",
|
235
249
|
"log_payload_content": False,
|
236
250
|
"monitor": False,
|
237
251
|
"name": "Python App",
|
238
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,
|
239
258
|
"scm_subdirectory": "",
|
240
259
|
"shutdown_message_enabled": True,
|
241
260
|
"shutdown_timeout_seconds": 2.0,
|
242
261
|
"uri_reporting": "filtered_params",
|
243
262
|
}
|
244
263
|
|
245
|
-
def _git_revision_sha(self):
|
264
|
+
def _git_revision_sha(self) -> str:
|
246
265
|
# N.B. The environment variable SCOUT_REVISION_SHA may also be used,
|
247
266
|
# but that will be picked up by Env
|
248
267
|
return os.environ.get("HEROKU_SLUG_COMMIT", "")
|
249
268
|
|
250
|
-
def has_config(self, key):
|
269
|
+
def has_config(self, key: str) -> bool:
|
251
270
|
return key in self.defaults
|
252
271
|
|
253
|
-
def value(self, key):
|
272
|
+
def value(self, key: str) -> Any:
|
254
273
|
return self.defaults[key]
|
255
274
|
|
256
275
|
|
@@ -261,14 +280,18 @@ class Null(object):
|
|
261
280
|
Used as the last step of the layered configuration.
|
262
281
|
"""
|
263
282
|
|
264
|
-
def has_config(self, key):
|
283
|
+
def has_config(self, key: str) -> bool:
|
265
284
|
return True
|
266
285
|
|
267
|
-
def value(self, key):
|
286
|
+
def value(self, key: str) -> None:
|
268
287
|
return None
|
269
288
|
|
270
289
|
|
271
|
-
def
|
290
|
+
def _strip_leading_slash(path: str) -> str:
|
291
|
+
return path.lstrip(" /").strip()
|
292
|
+
|
293
|
+
|
294
|
+
def convert_to_bool(value: Any) -> bool:
|
272
295
|
if isinstance(value, bool):
|
273
296
|
return value
|
274
297
|
if isinstance(value, str):
|
@@ -277,14 +300,38 @@ def convert_to_bool(value):
|
|
277
300
|
return False
|
278
301
|
|
279
302
|
|
280
|
-
def convert_to_float(value):
|
303
|
+
def convert_to_float(value: Any) -> float:
|
281
304
|
try:
|
282
305
|
return float(value)
|
283
306
|
except ValueError:
|
284
307
|
return 0.0
|
285
308
|
|
286
309
|
|
287
|
-
def
|
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]:
|
288
335
|
if isinstance(value, list):
|
289
336
|
return value
|
290
337
|
if isinstance(value, tuple):
|
@@ -296,13 +343,59 @@ def convert_to_list(value):
|
|
296
343
|
return []
|
297
344
|
|
298
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
|
+
|
299
385
|
CONVERSIONS = {
|
300
386
|
"collect_remote_ip": convert_to_bool,
|
301
387
|
"core_agent_download": convert_to_bool,
|
302
388
|
"core_agent_launch": convert_to_bool,
|
303
389
|
"disabled_instruments": convert_to_list,
|
304
|
-
"ignore":
|
390
|
+
"ignore": convert_ignore_paths,
|
391
|
+
"ignore_endpoints": convert_ignore_paths,
|
392
|
+
"ignore_jobs": convert_ignore_paths,
|
305
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,
|
306
399
|
"shutdown_message_enabled": convert_to_bool,
|
307
400
|
"shutdown_timeout_seconds": convert_to_float,
|
308
401
|
}
|
scout_apm/core/metadata.py
CHANGED
@@ -4,7 +4,7 @@ import datetime as dt
|
|
4
4
|
import sys
|
5
5
|
from os import getpid
|
6
6
|
|
7
|
-
from scout_apm.core.agent.commands import ApplicationEvent
|
7
|
+
from scout_apm.core.agent.commands import ApplicationEvent, format_dt_for_core_agent
|
8
8
|
from scout_apm.core.agent.socket import CoreAgentSocketThread
|
9
9
|
from scout_apm.core.config import scout_config
|
10
10
|
|
@@ -15,7 +15,7 @@ def report_app_metadata():
|
|
15
15
|
event_type="scout.metadata",
|
16
16
|
event_value=get_metadata(),
|
17
17
|
source="Pid: " + str(getpid()),
|
18
|
-
timestamp=dt.datetime.
|
18
|
+
timestamp=dt.datetime.now(dt.timezone.utc),
|
19
19
|
)
|
20
20
|
)
|
21
21
|
|
@@ -24,7 +24,7 @@ def get_metadata():
|
|
24
24
|
data = {
|
25
25
|
"language": "python",
|
26
26
|
"language_version": "{}.{}.{}".format(*sys.version_info[:3]),
|
27
|
-
"server_time": dt.datetime.
|
27
|
+
"server_time": format_dt_for_core_agent(dt.datetime.now(dt.timezone.utc)),
|
28
28
|
"framework": scout_config.value("framework"),
|
29
29
|
"framework_version": scout_config.value("framework_version"),
|
30
30
|
"environment": "",
|
scout_apm/core/queue_time.py
CHANGED
@@ -86,7 +86,7 @@ def track_job_queue_time(
|
|
86
86
|
bool: Whether we succeeded in marking queue time for the job. Used for testing.
|
87
87
|
"""
|
88
88
|
if header_value is not None:
|
89
|
-
now = datetime_to_timestamp(dt.datetime.
|
89
|
+
now = datetime_to_timestamp(dt.datetime.now(dt.timezone.utc)) * 1e9
|
90
90
|
try:
|
91
91
|
ambiguous_float_start = typing.cast(float, header_value)
|
92
92
|
start = _convert_ambiguous_timestamp_to_ns(ambiguous_float_start)
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# coding=utf-8
|
2
|
+
|
3
|
+
import random
|
4
|
+
from typing import Dict, Optional, Tuple
|
5
|
+
|
6
|
+
|
7
|
+
class Sampler:
|
8
|
+
"""
|
9
|
+
Handles sampling decision logic for Scout APM.
|
10
|
+
|
11
|
+
This class encapsulates all sampling-related functionality including:
|
12
|
+
- Loading and managing sampling configuration
|
13
|
+
- Pattern matching for operations (endpoints and jobs)
|
14
|
+
- Making sampling decisions based on operation type and patterns
|
15
|
+
"""
|
16
|
+
|
17
|
+
# Constants for operation type detection
|
18
|
+
CONTROLLER_PREFIX = "Controller/"
|
19
|
+
JOB_PREFIX = "Job/"
|
20
|
+
|
21
|
+
def __init__(self, config):
|
22
|
+
"""
|
23
|
+
Initialize sampler with Scout configuration.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
config: ScoutConfig instance containing sampling configuration
|
27
|
+
"""
|
28
|
+
self.config = config
|
29
|
+
self.sample_rate = config.value("sample_rate")
|
30
|
+
self.sample_endpoints = config.value("sample_endpoints")
|
31
|
+
self.sample_jobs = config.value("sample_jobs")
|
32
|
+
self.ignore_endpoints = set(
|
33
|
+
config.value("ignore_endpoints") + config.value("ignore")
|
34
|
+
)
|
35
|
+
self.ignore_jobs = set(config.value("ignore_jobs"))
|
36
|
+
self.endpoint_sample_rate = config.value("endpoint_sample_rate")
|
37
|
+
self.job_sample_rate = config.value("job_sample_rate")
|
38
|
+
|
39
|
+
def _any_sampling(self):
|
40
|
+
"""
|
41
|
+
Check if any sampling is enabled.
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
Boolean indicating if any sampling is enabled
|
45
|
+
"""
|
46
|
+
return (
|
47
|
+
self.sample_rate < 100
|
48
|
+
or self.sample_endpoints
|
49
|
+
or self.sample_jobs
|
50
|
+
or self.ignore_endpoints
|
51
|
+
or self.ignore_jobs
|
52
|
+
or self.endpoint_sample_rate is not None
|
53
|
+
or self.job_sample_rate is not None
|
54
|
+
)
|
55
|
+
|
56
|
+
def _find_matching_rate(
|
57
|
+
self, name: str, patterns: Dict[str, float]
|
58
|
+
) -> Optional[str]:
|
59
|
+
"""
|
60
|
+
Finds the matching sample rate for a given operation name.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
name: The operation name to match
|
64
|
+
patterns: Dictionary of pattern to sample rate mappings
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
The sample rate for the matching pattern or None if no match found
|
68
|
+
"""
|
69
|
+
|
70
|
+
for pattern, rate in patterns.items():
|
71
|
+
if name.startswith(pattern):
|
72
|
+
return rate
|
73
|
+
return None
|
74
|
+
|
75
|
+
def _get_operation_type_and_name(
|
76
|
+
self, operation: str
|
77
|
+
) -> Tuple[Optional[str], Optional[str]]:
|
78
|
+
"""
|
79
|
+
Determines if an operation is an endpoint or job and extracts its name.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
operation: The full operation string (e.g. "Controller/users/show")
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
Tuple of (type, name) where type is either 'endpoint' or 'job',
|
86
|
+
and name is the operation name without the prefix
|
87
|
+
"""
|
88
|
+
if operation.startswith(self.CONTROLLER_PREFIX):
|
89
|
+
return "endpoint", operation[len(self.CONTROLLER_PREFIX) :]
|
90
|
+
elif operation.startswith(self.JOB_PREFIX):
|
91
|
+
return "job", operation[len(self.JOB_PREFIX) :]
|
92
|
+
else:
|
93
|
+
return None, None
|
94
|
+
|
95
|
+
def get_effective_sample_rate(self, operation: str, is_ignored: bool) -> int:
|
96
|
+
"""
|
97
|
+
Determines the effective sample rate for a given operation.
|
98
|
+
|
99
|
+
Prioritization:
|
100
|
+
1. Sampling rate for specific endpoint or job
|
101
|
+
2. Specified ignore pattern or flag for operation
|
102
|
+
3. Global endpoint or job sample rate
|
103
|
+
4. Global sample rate
|
104
|
+
|
105
|
+
Args:
|
106
|
+
operation: The operation string (e.g. "Controller/users/show")
|
107
|
+
is_ignored: boolean for if the specific transaction is ignored
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
Integer between 0 and 100 representing sample rate
|
111
|
+
"""
|
112
|
+
op_type, name = self._get_operation_type_and_name(operation)
|
113
|
+
patterns = self.sample_endpoints if op_type == "endpoint" else self.sample_jobs
|
114
|
+
ignores = self.ignore_endpoints if op_type == "endpoint" else self.ignore_jobs
|
115
|
+
default_operation_rate = (
|
116
|
+
self.endpoint_sample_rate if op_type == "endpoint" else self.job_sample_rate
|
117
|
+
)
|
118
|
+
|
119
|
+
if not op_type or not name:
|
120
|
+
return self.sample_rate
|
121
|
+
matching_rate = self._find_matching_rate(name, patterns)
|
122
|
+
if matching_rate is not None:
|
123
|
+
return matching_rate
|
124
|
+
for prefix in ignores:
|
125
|
+
if name.startswith(prefix) or is_ignored:
|
126
|
+
return 0
|
127
|
+
if default_operation_rate is not None:
|
128
|
+
return default_operation_rate
|
129
|
+
|
130
|
+
# Fall back to global sample rate
|
131
|
+
return self.sample_rate
|
132
|
+
|
133
|
+
def should_sample(self, operation: str, is_ignored: bool) -> bool:
|
134
|
+
"""
|
135
|
+
Determines if an operation should be sampled.
|
136
|
+
If no sampling is enabled, always return True.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
operation: The operation string (e.g. "Controller/users/show"
|
140
|
+
or "Job/mailer")
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
Boolean indicating whether to sample this operation
|
144
|
+
"""
|
145
|
+
if not self._any_sampling():
|
146
|
+
return True
|
147
|
+
return random.randint(1, 100) <= self.get_effective_sample_rate(
|
148
|
+
operation, is_ignored
|
149
|
+
)
|
scout_apm/core/samplers/cpu.py
CHANGED
@@ -14,7 +14,7 @@ class Cpu(object):
|
|
14
14
|
human_name = "Process CPU"
|
15
15
|
|
16
16
|
def __init__(self):
|
17
|
-
self.last_run = dt.datetime.
|
17
|
+
self.last_run = dt.datetime.now(dt.timezone.utc)
|
18
18
|
self.last_cpu_times = psutil.Process().cpu_times()
|
19
19
|
self.num_processors = psutil.cpu_count()
|
20
20
|
if self.num_processors is None:
|
@@ -22,7 +22,7 @@ class Cpu(object):
|
|
22
22
|
self.num_processors = 1
|
23
23
|
|
24
24
|
def run(self):
|
25
|
-
now = dt.datetime.
|
25
|
+
now = dt.datetime.now(dt.timezone.utc)
|
26
26
|
process = psutil.Process() # get a handle on the current process
|
27
27
|
cpu_times = process.cpu_times()
|
28
28
|
|
@@ -30,7 +30,7 @@ class SamplersThread(SingletonThread):
|
|
30
30
|
event = ApplicationEvent(
|
31
31
|
event_value=event_value,
|
32
32
|
event_type=event_type,
|
33
|
-
timestamp=dt.datetime.
|
33
|
+
timestamp=dt.datetime.now(dt.timezone.utc),
|
34
34
|
source="Pid: " + str(os.getpid()),
|
35
35
|
)
|
36
36
|
CoreAgentSocketThread.send(event)
|
@@ -10,6 +10,7 @@ from scout_apm.core.agent.commands import BatchCommand
|
|
10
10
|
from scout_apm.core.agent.socket import CoreAgentSocketThread
|
11
11
|
from scout_apm.core.config import scout_config
|
12
12
|
from scout_apm.core.n_plus_one_tracker import NPlusOneTracker
|
13
|
+
from scout_apm.core.sampler import Sampler
|
13
14
|
from scout_apm.core.samplers.memory import get_rss_in_mb
|
14
15
|
from scout_apm.core.samplers.thread import SamplersThread
|
15
16
|
|
@@ -23,7 +24,16 @@ class TrackedRequest(object):
|
|
23
24
|
their keyname
|
24
25
|
"""
|
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
|
+
|
26
35
|
__slots__ = (
|
36
|
+
"sampler",
|
27
37
|
"request_id",
|
28
38
|
"start_time",
|
29
39
|
"end_time",
|
@@ -49,7 +59,7 @@ class TrackedRequest(object):
|
|
49
59
|
|
50
60
|
def __init__(self):
|
51
61
|
self.request_id = "req-" + str(uuid4())
|
52
|
-
self.start_time = dt.datetime.
|
62
|
+
self.start_time = dt.datetime.now(dt.timezone.utc)
|
53
63
|
self.end_time = None
|
54
64
|
self.active_spans = []
|
55
65
|
self.complete_spans = []
|
@@ -147,11 +157,13 @@ class TrackedRequest(object):
|
|
147
157
|
|
148
158
|
logger.debug("Stopping request: %s", self.request_id)
|
149
159
|
if self.end_time is None:
|
150
|
-
self.end_time = dt.datetime.
|
160
|
+
self.end_time = dt.datetime.now(dt.timezone.utc)
|
151
161
|
|
152
162
|
if self.is_real_request:
|
153
|
-
self.
|
154
|
-
|
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())
|
155
167
|
self.sent = True
|
156
168
|
batch_command = BatchCommand.from_tracked_request(self)
|
157
169
|
if scout_config.value("log_payload_content"):
|
@@ -219,7 +231,7 @@ class Span(object):
|
|
219
231
|
should_capture_backtrace=True,
|
220
232
|
):
|
221
233
|
self.span_id = "span-" + str(uuid4())
|
222
|
-
self.start_time = dt.datetime.
|
234
|
+
self.start_time = dt.datetime.now(dt.timezone.utc)
|
223
235
|
self.end_time = None
|
224
236
|
self.request_id = request_id
|
225
237
|
self.operation = operation
|
@@ -238,7 +250,7 @@ class Span(object):
|
|
238
250
|
)
|
239
251
|
|
240
252
|
def stop(self):
|
241
|
-
self.end_time = dt.datetime.
|
253
|
+
self.end_time = dt.datetime.now(dt.timezone.utc)
|
242
254
|
self.end_objtrace_counts = objtrace.get_counts()
|
243
255
|
|
244
256
|
def tag(self, key, value):
|
@@ -254,7 +266,9 @@ class Span(object):
|
|
254
266
|
return (self.end_time - self.start_time).total_seconds()
|
255
267
|
else:
|
256
268
|
# Current, running duration
|
257
|
-
return (
|
269
|
+
return (
|
270
|
+
dt.datetime.now(tz=dt.timezone.utc) - self.start_time
|
271
|
+
).total_seconds()
|
258
272
|
|
259
273
|
# Add any interesting annotations to the span. Assumes that we are in the
|
260
274
|
# process of stopping this span.
|
scout_apm/core/web_requests.py
CHANGED
scout_apm/rq.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# coding=utf-8
|
2
2
|
|
3
3
|
import datetime as dt
|
4
|
+
import logging
|
4
5
|
|
5
6
|
import wrapt
|
6
7
|
from rq import SimpleWorker as RqSimpleWorker
|
@@ -14,6 +15,8 @@ from scout_apm.core.tracked_request import TrackedRequest
|
|
14
15
|
install_attempted = False
|
15
16
|
installed = None
|
16
17
|
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
17
20
|
|
18
21
|
def ensure_scout_installed():
|
19
22
|
global install_attempted, installed
|
@@ -65,7 +68,14 @@ def wrap_perform(wrapped, instance, args, kwargs):
|
|
65
68
|
tracked_request.is_real_request = True
|
66
69
|
tracked_request.tag("task_id", instance.get_id())
|
67
70
|
tracked_request.tag("queue", instance.origin)
|
68
|
-
|
71
|
+
# rq strips tzinfo from enqueued_at during serde in at least some cases
|
72
|
+
# internally everything uses UTC naive datetimes, so we operate on that
|
73
|
+
# assumption here.
|
74
|
+
if instance.enqueued_at.tzinfo is None:
|
75
|
+
queued_at = instance.enqueued_at.replace(tzinfo=dt.timezone.utc)
|
76
|
+
else:
|
77
|
+
queued_at = instance.enqueued_at
|
78
|
+
queue_time = (dt.datetime.now(dt.timezone.utc) - queued_at).total_seconds()
|
69
79
|
tracked_request.tag("queue_time", queue_time)
|
70
80
|
operation = "Job/{}".format(instance.func_name)
|
71
81
|
tracked_request.operation = operation
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: scout_apm
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.4.0
|
4
4
|
Summary: Scout Application Performance Monitoring Agent
|
5
5
|
Home-page: https://github.com/scoutapp/scout_apm_python
|
6
6
|
Author: Scout
|
@@ -32,10 +32,23 @@ Requires-Python: >=3.8, <4
|
|
32
32
|
Description-Content-Type: text/markdown
|
33
33
|
License-File: LICENSE
|
34
34
|
Requires-Dist: asgiref
|
35
|
-
Requires-Dist: psutil
|
35
|
+
Requires-Dist: psutil>=5
|
36
36
|
Requires-Dist: urllib3
|
37
37
|
Requires-Dist: certifi
|
38
38
|
Requires-Dist: wrapt<2.0,>=1.10
|
39
|
+
Dynamic: author
|
40
|
+
Dynamic: author-email
|
41
|
+
Dynamic: classifier
|
42
|
+
Dynamic: description
|
43
|
+
Dynamic: description-content-type
|
44
|
+
Dynamic: home-page
|
45
|
+
Dynamic: keywords
|
46
|
+
Dynamic: license
|
47
|
+
Dynamic: license-file
|
48
|
+
Dynamic: project-url
|
49
|
+
Dynamic: requires-dist
|
50
|
+
Dynamic: requires-python
|
51
|
+
Dynamic: summary
|
39
52
|
|
40
53
|
# Scout Python APM Agent
|
41
54
|
|
@@ -1,64 +1,65 @@
|
|
1
|
+
scout_apm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
scout_apm/bottle.py,sha256=jWBUXgW4-sOiUbr9HYimO5Ryd9dvNVZa5Rb611jxxNI,2934
|
3
|
+
scout_apm/celery.py,sha256=zzM5wn9ScaFk-7YizntSOG8I2WEzEc-crSYZ7_AJjxA,4729
|
1
4
|
scout_apm/compat.py,sha256=kTG20OAM8SkbVQZS_-bd_bn4F0BjpsfGoxfCk7kyYCI,2496
|
2
|
-
scout_apm/rq.py,sha256=i7RhKtuJYhlCbBbdnY3JxxJm7j0hqhSXUztoclQ_4u8,1903
|
3
5
|
scout_apm/dramatiq.py,sha256=N0VxvprOeXSAmmvE7nmZ5ul1rMuJQC-eEzerwGTC7yc,1424
|
4
|
-
scout_apm/bottle.py,sha256=jWBUXgW4-sOiUbr9HYimO5Ryd9dvNVZa5Rb611jxxNI,2934
|
5
6
|
scout_apm/falcon.py,sha256=5Ll4RQRz8v_nB_A3cx65I1UQ2hxgWaU1uCsAtkbg4F0,5357
|
6
|
-
scout_apm/sqlalchemy.py,sha256=MKhRJwiJvJDRR4hQA2W8FG2YMueDFXNOmXbP9sQGVqM,1358
|
7
|
-
scout_apm/celery.py,sha256=WuN70JpsnSO5Ok_pO3VK3uELuahHi2QOs7SqAeTX_Mw,4695
|
8
|
-
scout_apm/hug.py,sha256=tSo8r6oFNFoqF3T7lVXi1CxmrRkOX8DiDJrks58dHyw,1387
|
9
|
-
scout_apm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
7
|
scout_apm/huey.py,sha256=XjQNVfO8Z14iu1t9qqv-ZRn_3HqfuQv5t1ZYhlRZ17Y,1752
|
11
|
-
scout_apm/
|
12
|
-
scout_apm/
|
13
|
-
scout_apm/
|
14
|
-
scout_apm/
|
15
|
-
scout_apm/
|
16
|
-
scout_apm/
|
17
|
-
scout_apm/
|
18
|
-
scout_apm/
|
19
|
-
scout_apm/
|
20
|
-
scout_apm/core/
|
21
|
-
scout_apm/core/
|
22
|
-
scout_apm/core/
|
8
|
+
scout_apm/hug.py,sha256=tSo8r6oFNFoqF3T7lVXi1CxmrRkOX8DiDJrks58dHyw,1387
|
9
|
+
scout_apm/rq.py,sha256=PbXT0uoNFfbTA724q-dxUKD4lsZSK7yOQ2YSfRcrJB0,2300
|
10
|
+
scout_apm/sqlalchemy.py,sha256=MKhRJwiJvJDRR4hQA2W8FG2YMueDFXNOmXbP9sQGVqM,1358
|
11
|
+
scout_apm/api/__init__.py,sha256=dx6qEJ-xMsWc4E4bvgoxSo5k7vrCELLApJvOoyLWSpA,5660
|
12
|
+
scout_apm/async_/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
|
13
|
+
scout_apm/async_/api.py,sha256=K8Sh5IiVO-PnJNRzErAQulNxuFcQoMR8-a2-P0IMAZo,1116
|
14
|
+
scout_apm/async_/starlette.py,sha256=aFnc1XTXC4ug9llYX9dDeiElXQIb6QgA8gq3N_g9-Ew,3812
|
15
|
+
scout_apm/async_/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
+
scout_apm/async_/instruments/jinja2.py,sha256=pYrLigxeSgEAWmzWxqh5zCa86TkZQ9A1S7W8uTXE3C8,374
|
17
|
+
scout_apm/core/__init__.py,sha256=SnXENrNGgE8_ontzysrZdrARLTsBDz7hXD50zludFbM,2944
|
18
|
+
scout_apm/core/_objtrace.cpython-310-x86_64-linux-gnu.so,sha256=IE3crb0jBtn7R_jRm-bfBB8sXS8QJdIzWWacfkeYT7M,30000
|
19
|
+
scout_apm/core/backtrace.py,sha256=x2owyERxWdomJBz4leN3wHsf_P393864UUXW4uFrQtc,3495
|
20
|
+
scout_apm/core/config.py,sha256=TuKWfz3T1rYhRLyJm9ewVBZcRlz3YjkFRBX1YJlVHg8,11868
|
23
21
|
scout_apm/core/context.py,sha256=9qpFGKAGIqyer1NqAhBmU8DuVTf-4_doUFSC55vfDm4,4220
|
24
|
-
scout_apm/core/platform_detection.py,sha256=gWgZNfcnjy97HzDTz-Q2F6xzrch7eVQtTGp2KqcdMEk,1777
|
25
|
-
scout_apm/core/objtrace.py,sha256=F7L0V2QBzXYUEFfqon3adXkrA9UQJgmWCFprJo-l01I,463
|
26
22
|
scout_apm/core/error.py,sha256=EQ1e1wV9K2vkHXXRzoZxIPW89CF-ce7c8wYTI2b3ajM,3237
|
27
23
|
scout_apm/core/error_service.py,sha256=QKyFGgjc_Ihm8ONv7j0hkPgtBu30U3poyfAD40BJ7jQ,5359
|
28
|
-
scout_apm/core/
|
29
|
-
scout_apm/core/metadata.py,sha256=SnB-hiqnJ_t99Oij7cU42kZJqxJvnHrorLUMsivbs98,2226
|
24
|
+
scout_apm/core/metadata.py,sha256=huqY2c5vPrQSMZykxQzrSyYYiw8s9E_OIm_ABW9ZzzM,2284
|
30
25
|
scout_apm/core/n_plus_one_tracker.py,sha256=hE3YEVEe45Oygq8lR_6ftPRdCab6WYjnN8HeY8wLPL8,978
|
31
|
-
scout_apm/core/
|
32
|
-
scout_apm/core/
|
33
|
-
scout_apm/core/
|
34
|
-
scout_apm/core/
|
26
|
+
scout_apm/core/objtrace.py,sha256=F7L0V2QBzXYUEFfqon3adXkrA9UQJgmWCFprJo-l01I,463
|
27
|
+
scout_apm/core/platform_detection.py,sha256=gWgZNfcnjy97HzDTz-Q2F6xzrch7eVQtTGp2KqcdMEk,1777
|
28
|
+
scout_apm/core/queue_time.py,sha256=KA1tsQ8K4kNYSXTfWS-RulTQ41Bhs2-HmqJwIkVxgcc,3199
|
29
|
+
scout_apm/core/sampler.py,sha256=NZKX2RAnvOfsHY6EN47qaRmKOCoL04slNG1k91n8n_I,5105
|
30
|
+
scout_apm/core/stacktracer.py,sha256=loNFpOwFtTvf6XsCxari4iFJ8Pe4rZrLmoU691ZuV1M,900
|
31
|
+
scout_apm/core/threading.py,sha256=i_e3Zbqcq-yIDkipcTKCGJwmqGzpiYffl6IK88ylC-g,1624
|
32
|
+
scout_apm/core/tracked_request.py,sha256=zCtv5hWoZ-Cy3yPM0UUaFQkVEyPHTt_BfLXC4FzBv7s,10394
|
33
|
+
scout_apm/core/web_requests.py,sha256=DD6xDdDZOfxCmpOTvXEJY3JLQ0n4Zw9SmlSfnN4xC3w,5680
|
34
|
+
scout_apm/core/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
35
|
+
scout_apm/core/agent/commands.py,sha256=Ze4CKBHstpSk5OOzr1sNT84XFY2U9hLUGqfpDBSqhtY,7147
|
35
36
|
scout_apm/core/agent/manager.py,sha256=Vm6JfjRJW9YGQabWeZ5VXuWC7g8P_qBok1NjP6Oqoh4,10385
|
36
|
-
scout_apm/core/agent/commands.py,sha256=a64GfyvF3K2eGFazI71sLx7sFIHMxN27klAgprT7Bp8,6613
|
37
37
|
scout_apm/core/agent/socket.py,sha256=GR0tfElZNxiCRCwtvHiqLU_37GIcz_ABQ3B7Jh-XeP8,6777
|
38
|
-
scout_apm/core/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
39
|
-
scout_apm/core/cli/core_agent_manager.py,sha256=iOZhpXCnKZriJznCc2LD7eDXKqmc98zsYtDIrj3tXlU,813
|
40
38
|
scout_apm/core/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
|
-
scout_apm/core/
|
39
|
+
scout_apm/core/cli/core_agent_manager.py,sha256=iOZhpXCnKZriJznCc2LD7eDXKqmc98zsYtDIrj3tXlU,813
|
42
40
|
scout_apm/core/samplers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
43
|
-
scout_apm/core/samplers/cpu.py,sha256=
|
41
|
+
scout_apm/core/samplers/cpu.py,sha256=X0bpFks18cR61pbf69ptyKE0CtnUmBJUJXSRQ_S2IdM,2522
|
44
42
|
scout_apm/core/samplers/memory.py,sha256=D1py5gmf5GISq6_5HNnwI3HU2EunXaQ2HzqVKRwp5no,444
|
45
|
-
scout_apm/
|
43
|
+
scout_apm/core/samplers/thread.py,sha256=P0s7T99EpYbbkekZnlCJQ787G8g1NnNbZjn8CLtDdUg,1342
|
44
|
+
scout_apm/django/__init__.py,sha256=RQdhc6VLBlJsiWLVb8yggtaY2pMla1j6YP4yrKPAYgk,207
|
45
|
+
scout_apm/django/apps.py,sha256=aUQlZG8qQFCi0-vFeU0fmnpI60n3nvpXRWBAjdgG5GA,5260
|
46
|
+
scout_apm/django/middleware.py,sha256=fE-bon42PigKZ5SXVxDBHOyPwBmWzZNmzqILKZqM72I,7761
|
47
|
+
scout_apm/django/request.py,sha256=bswpkpJIKO5oFm3OiBRwgXhtZFKx6pbgzGNCLrF8qz0,4853
|
48
|
+
scout_apm/django/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
49
|
+
scout_apm/django/instruments/huey.py,sha256=tRvBg5wr76W_aGpLfVpaDnLcTpR8jdhGMFOz3Xl580s,653
|
50
|
+
scout_apm/django/instruments/sql.py,sha256=8_n7oUHtrAc7miWrk_k8CLhc9KzS6W0iUbmZJFN-S6g,4227
|
51
|
+
scout_apm/django/instruments/template.py,sha256=zr_xlfgEbHoIItAFLbMEc0I4jj4sU8BW5IXMPNMqOI0,963
|
52
|
+
scout_apm/flask/__init__.py,sha256=Rt5D7c_NifqIG4cGGh7oo4XmekXacUlZda1WidECodU,4306
|
53
|
+
scout_apm/flask/sqlalchemy.py,sha256=HwoYvN0c4LZ9_spCkvVE8gcBJyHFRJCgndFVFKwCzao,757
|
54
|
+
scout_apm/instruments/__init__.py,sha256=X76KWdu2jgDdeJtg5DOC6rYPaq0ZXkhjiftxcsem3hI,631
|
55
|
+
scout_apm/instruments/elasticsearch.py,sha256=xkkV1wHCd6clAFX7sMMrU75mfxXkKwN0i43aWPZg3is,8272
|
46
56
|
scout_apm/instruments/jinja2.py,sha256=a2-u9klcZQdkCJaylc55a4KGi3tHaNlbMXQr1PW3zI0,3662
|
57
|
+
scout_apm/instruments/pymongo.py,sha256=wVqA59ciH6LvOH3VRmMZG067U6qypoA6QzLtWp0IdAM,2538
|
47
58
|
scout_apm/instruments/redis.py,sha256=nrkt35bNI172yDzHmpcXFc972QDxLdOkdvh2gWkcz08,2190
|
48
59
|
scout_apm/instruments/urllib3.py,sha256=riwzKX-sgj9kCbN8E5gvtBlyWPAae8bMvi-JgMuGO_w,2219
|
49
|
-
scout_apm/
|
50
|
-
scout_apm/
|
51
|
-
scout_apm/
|
52
|
-
scout_apm/
|
53
|
-
scout_apm/
|
54
|
-
scout_apm/
|
55
|
-
scout_apm/async_/starlette.py,sha256=aFnc1XTXC4ug9llYX9dDeiElXQIb6QgA8gq3N_g9-Ew,3812
|
56
|
-
scout_apm/async_/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
|
57
|
-
scout_apm/async_/instruments/jinja2.py,sha256=pYrLigxeSgEAWmzWxqh5zCa86TkZQ9A1S7W8uTXE3C8,374
|
58
|
-
scout_apm/async_/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
59
|
-
scout_apm-3.2.1.dist-info/WHEEL,sha256=G6WAJh_GXUMYPkzQt82j5xHS9Nx3k6AYK5BZjp4Pr1U,112
|
60
|
-
scout_apm-3.2.1.dist-info/LICENSE,sha256=IL2YQsmIcNnRK09t7_ELMSBMdyrMWIJpBOCAhZ9IMCU,1084
|
61
|
-
scout_apm-3.2.1.dist-info/RECORD,,
|
62
|
-
scout_apm-3.2.1.dist-info/METADATA,sha256=Pqd8zA2mMd2PaN7fgdQMMzRFvM2Un-Qxg6NK5KcYcoI,3129
|
63
|
-
scout_apm-3.2.1.dist-info/top_level.txt,sha256=tXGCTyC-E-TraDQng0CvkawiUZU-h4kkhe-5avNfnTw,10
|
64
|
-
scout_apm-3.2.1.dist-info/entry_points.txt,sha256=eiVubJRHQCFcJ1fqH_2myVIOlt9xx32sKTRtWs9xWjk,82
|
60
|
+
scout_apm-3.4.0.dist-info/METADATA,sha256=A4hDk-f1t70l9tTZ7I9_EvOI1SzKraJViblRJSbmEsg,3401
|
61
|
+
scout_apm-3.4.0.dist-info/WHEEL,sha256=Y1po9wJemUxV3QfSxUJEf7hMWR6d4KoBJZzLLzcgMiE,112
|
62
|
+
scout_apm-3.4.0.dist-info/entry_points.txt,sha256=eiVubJRHQCFcJ1fqH_2myVIOlt9xx32sKTRtWs9xWjk,82
|
63
|
+
scout_apm-3.4.0.dist-info/top_level.txt,sha256=tXGCTyC-E-TraDQng0CvkawiUZU-h4kkhe-5avNfnTw,10
|
64
|
+
scout_apm-3.4.0.dist-info/RECORD,,
|
65
|
+
scout_apm-3.4.0.dist-info/licenses/LICENSE,sha256=IL2YQsmIcNnRK09t7_ELMSBMdyrMWIJpBOCAhZ9IMCU,1084
|
File without changes
|
File without changes
|
File without changes
|