scout-apm 3.0.2__cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl → 3.3.0__cp38-cp38-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/api/__init__.py +1 -0
- scout_apm/async_/starlette.py +6 -5
- scout_apm/bottle.py +5 -8
- scout_apm/celery.py +10 -11
- scout_apm/core/_objtrace.cpython-38-aarch64-linux-gnu.so +0 -0
- scout_apm/core/agent/commands.py +20 -7
- scout_apm/core/agent/manager.py +6 -3
- scout_apm/core/config.py +123 -30
- scout_apm/core/metadata.py +3 -3
- scout_apm/core/queue_time.py +99 -0
- 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 +23 -7
- scout_apm/core/web_requests.py +3 -63
- scout_apm/django/middleware.py +3 -5
- scout_apm/dramatiq.py +3 -1
- scout_apm/falcon.py +3 -5
- scout_apm/flask/__init__.py +1 -0
- scout_apm/huey.py +1 -0
- scout_apm/rq.py +12 -3
- {scout_apm-3.0.2.dist-info → scout_apm-3.3.0.dist-info}/METADATA +6 -6
- {scout_apm-3.0.2.dist-info → scout_apm-3.3.0.dist-info}/RECORD +45 -43
- {scout_apm-3.0.2.dist-info → scout_apm-3.3.0.dist-info}/WHEEL +1 -1
- {scout_apm-3.0.2.dist-info → scout_apm-3.3.0.dist-info}/LICENSE +0 -0
- {scout_apm-3.0.2.dist-info → scout_apm-3.3.0.dist-info}/entry_points.txt +0 -0
- {scout_apm-3.0.2.dist-info → scout_apm-3.3.0.dist-info}/top_level.txt +0 -0
scout_apm/api/__init__.py
CHANGED
@@ -97,6 +97,7 @@ class Transaction(AsyncDecoratorMixin, ContextDecorator):
|
|
97
97
|
operation = text(kind) + "/" + text(name)
|
98
98
|
|
99
99
|
tracked_request = TrackedRequest.instance()
|
100
|
+
tracked_request.operation = operation
|
100
101
|
tracked_request.is_real_request = True
|
101
102
|
span = tracked_request.start_span(
|
102
103
|
operation=operation, should_capture_backtrace=False
|
scout_apm/async_/starlette.py
CHANGED
@@ -40,6 +40,7 @@ class ScoutMiddleware:
|
|
40
40
|
endpoint.__module__,
|
41
41
|
endpoint.__qualname__,
|
42
42
|
)
|
43
|
+
tracked_request.operation = controller_span.operation
|
43
44
|
else:
|
44
45
|
# Mark the request as not real
|
45
46
|
tracked_request.is_real_request = False
|
@@ -90,11 +91,11 @@ def install_background_instrumentation():
|
|
90
91
|
tracked_request = TrackedRequest.instance()
|
91
92
|
tracked_request.is_real_request = True
|
92
93
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
):
|
94
|
+
operation = "Job/{}.{}".format(
|
95
|
+
instance.func.__module__, instance.func.__qualname__
|
96
|
+
)
|
97
|
+
tracked_request.operation = operation
|
98
|
+
with tracked_request.span(operation=operation):
|
98
99
|
return await wrapped(*args, **kwargs)
|
99
100
|
|
100
101
|
BackgroundTask.__call__ = wrapped_background_call(BackgroundTask.__call__)
|
scout_apm/bottle.py
CHANGED
@@ -5,12 +5,9 @@ from bottle import request, response
|
|
5
5
|
|
6
6
|
import scout_apm.core
|
7
7
|
from scout_apm.core.config import scout_config
|
8
|
+
from scout_apm.core.queue_time import track_request_queue_time
|
8
9
|
from scout_apm.core.tracked_request import TrackedRequest
|
9
|
-
from scout_apm.core.web_requests import
|
10
|
-
create_filtered_path,
|
11
|
-
ignore_path,
|
12
|
-
track_request_queue_time,
|
13
|
-
)
|
10
|
+
from scout_apm.core.web_requests import create_filtered_path, ignore_path
|
14
11
|
|
15
12
|
|
16
13
|
class ScoutPlugin(object):
|
@@ -74,10 +71,10 @@ def wrap_callback(wrapped, instance, args, kwargs):
|
|
74
71
|
"x-request-start", ""
|
75
72
|
)
|
76
73
|
track_request_queue_time(queue_time, tracked_request)
|
74
|
+
operation = "Controller{}".format(controller_name)
|
77
75
|
|
78
|
-
with tracked_request.span(
|
79
|
-
operation=
|
80
|
-
):
|
76
|
+
with tracked_request.span(operation=operation):
|
77
|
+
tracked_request.operation = operation
|
81
78
|
try:
|
82
79
|
value = wrapped(*args, **kwargs)
|
83
80
|
except Exception:
|
scout_apm/celery.py
CHANGED
@@ -5,6 +5,8 @@ import logging
|
|
5
5
|
|
6
6
|
from celery.signals import before_task_publish, task_failure, task_postrun, task_prerun
|
7
7
|
|
8
|
+
from scout_apm.core.queue_time import track_job_queue_time
|
9
|
+
|
8
10
|
try:
|
9
11
|
import django
|
10
12
|
from django.views.debug import SafeExceptionReporterFilter
|
@@ -27,22 +29,17 @@ logger = logging.getLogger(__name__)
|
|
27
29
|
|
28
30
|
def before_task_publish_callback(headers=None, properties=None, **kwargs):
|
29
31
|
if "scout_task_start" not in headers:
|
30
|
-
headers["scout_task_start"] = datetime_to_timestamp(
|
32
|
+
headers["scout_task_start"] = datetime_to_timestamp(
|
33
|
+
dt.datetime.now(dt.timezone.utc)
|
34
|
+
)
|
31
35
|
|
32
36
|
|
33
37
|
def task_prerun_callback(task=None, **kwargs):
|
34
38
|
tracked_request = TrackedRequest.instance()
|
35
39
|
tracked_request.is_real_request = True
|
36
40
|
|
37
|
-
|
38
|
-
|
39
|
-
now = datetime_to_timestamp(dt.datetime.utcnow())
|
40
|
-
try:
|
41
|
-
queue_time = now - start
|
42
|
-
except TypeError:
|
43
|
-
pass
|
44
|
-
else:
|
45
|
-
tracked_request.tag("queue_time", queue_time)
|
41
|
+
start_time_header = getattr(task.request, "scout_task_start", None)
|
42
|
+
track_job_queue_time(start_time_header, tracked_request)
|
46
43
|
|
47
44
|
task_id = getattr(task.request, "id", None)
|
48
45
|
if task_id:
|
@@ -59,7 +56,9 @@ def task_prerun_callback(task=None, **kwargs):
|
|
59
56
|
tracked_request.tag("routing_key", delivery_info.get("routing_key", "unknown"))
|
60
57
|
tracked_request.tag("queue", delivery_info.get("queue", "unknown"))
|
61
58
|
|
62
|
-
|
59
|
+
operation = "Job/" + task.name
|
60
|
+
tracked_request.start_span(operation=operation)
|
61
|
+
tracked_request.operation = operation
|
63
62
|
|
64
63
|
|
65
64
|
def task_postrun_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/agent/manager.py
CHANGED
@@ -5,7 +5,6 @@ import hashlib
|
|
5
5
|
import json
|
6
6
|
import logging
|
7
7
|
import os
|
8
|
-
import signal
|
9
8
|
import subprocess
|
10
9
|
import tarfile
|
11
10
|
import time
|
@@ -17,6 +16,8 @@ from scout_apm.core.config import scout_config
|
|
17
16
|
|
18
17
|
logger = logging.getLogger(__name__)
|
19
18
|
|
19
|
+
CA_ALREADY_RUNNING_EXIT_CODE = 3
|
20
|
+
|
20
21
|
|
21
22
|
class CoreAgentManager(object):
|
22
23
|
def __init__(self):
|
@@ -73,8 +74,10 @@ class CoreAgentManager(object):
|
|
73
74
|
stdout=devnull,
|
74
75
|
)
|
75
76
|
except subprocess.CalledProcessError as err:
|
76
|
-
if err.returncode
|
77
|
-
|
77
|
+
if err.returncode == CA_ALREADY_RUNNING_EXIT_CODE:
|
78
|
+
# Other processes may have already started the core agent.
|
79
|
+
logger.debug("Core agent already running.")
|
80
|
+
return True
|
78
81
|
else:
|
79
82
|
logger.exception("CalledProcessError running Core Agent")
|
80
83
|
return False
|
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
|
|
@@ -221,9 +229,12 @@ class Defaults(object):
|
|
221
229
|
"core_agent_log_level": "info",
|
222
230
|
"core_agent_permissions": 700,
|
223
231
|
"core_agent_socket_path": "tcp://127.0.0.1:6590",
|
224
|
-
"core_agent_version": "v1.
|
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": "",
|