scout-apm 3.0.2__cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl → 3.3.0__cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.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/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-39-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 +19 -7
- {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": "",
|