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 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
@@ -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
- with tracked_request.span(
94
- operation="Job/{}.{}".format(
95
- instance.func.__module__, instance.func.__qualname__
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="Controller{}".format(controller_name), should_capture_backtrace=False
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(dt.datetime.utcnow())
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
- start = getattr(task.request, "scout_task_start", None)
38
- if start is not None:
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
- tracked_request.start_span(operation=("Job/" + task.name))
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):
@@ -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.isoformat() + "Z",
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.isoformat() + "Z",
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.isoformat() + "Z",
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.isoformat() + "Z",
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.isoformat() + "Z",
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.isoformat() + "Z",
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.isoformat() + "Z",
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,
@@ -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 in [signal.SIGTERM, signal.SIGQUIT]:
77
- logger.debug("Core agent returned signal: {}".format(err.returncode))
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.4.0", # can be an exact tag name, or 'latest'
232
+ "core_agent_version": "v1.5.0", # can be an exact tag name, or 'latest'
225
233
  "disabled_instruments": [],
226
- "download_url": "https://s3-us-west-1.amazonaws.com/scout-public-downloads/apm_core_agent/release", # noqa: B950
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 convert_to_bool(value):
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 convert_to_list(value):
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": convert_to_list,
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
  }
@@ -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.utcnow(),
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.utcnow().isoformat() + "Z",
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": "",