scout-apm 3.1.0__cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl → 3.3.0__cp311-cp311-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
@@ -71,10 +71,10 @@ def wrap_callback(wrapped, instance, args, kwargs):
71
71
  "x-request-start", ""
72
72
  )
73
73
  track_request_queue_time(queue_time, tracked_request)
74
+ operation = "Controller{}".format(controller_name)
74
75
 
75
- with tracked_request.span(
76
- operation="Controller{}".format(controller_name), should_capture_backtrace=False
77
- ):
76
+ with tracked_request.span(operation=operation):
77
+ tracked_request.operation = operation
78
78
  try:
79
79
  value = wrapped(*args, **kwargs)
80
80
  except Exception:
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(dt.datetime.utcnow())
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):
@@ -54,7 +56,9 @@ def task_prerun_callback(task=None, **kwargs):
54
56
  tracked_request.tag("routing_key", delivery_info.get("routing_key", "unknown"))
55
57
  tracked_request.tag("queue", delivery_info.get("queue", "unknown"))
56
58
 
57
- 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
58
62
 
59
63
 
60
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,
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": "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": "",
@@ -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.utcnow()) * 1e9
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
+ )
@@ -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.utcnow()
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.utcnow()
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.utcnow(),
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",
@@ -35,6 +45,7 @@ class TrackedRequest(object):
35
45
  "n_plus_one_tracker",
36
46
  "hit_max",
37
47
  "sent",
48
+ "operation",
38
49
  )
39
50
 
40
51
  # Stop adding new spans at this point, to avoid exhausting memory
@@ -48,7 +59,7 @@ class TrackedRequest(object):
48
59
 
49
60
  def __init__(self):
50
61
  self.request_id = "req-" + str(uuid4())
51
- self.start_time = dt.datetime.utcnow()
62
+ self.start_time = dt.datetime.now(dt.timezone.utc)
52
63
  self.end_time = None
53
64
  self.active_spans = []
54
65
  self.complete_spans = []
@@ -58,6 +69,7 @@ class TrackedRequest(object):
58
69
  self.n_plus_one_tracker = NPlusOneTracker()
59
70
  self.hit_max = False
60
71
  self.sent = False
72
+ self.operation = None
61
73
  logger.debug("Starting request: %s", self.request_id)
62
74
 
63
75
  def __repr__(self):
@@ -145,11 +157,13 @@ class TrackedRequest(object):
145
157
 
146
158
  logger.debug("Stopping request: %s", self.request_id)
147
159
  if self.end_time is None:
148
- self.end_time = dt.datetime.utcnow()
160
+ self.end_time = dt.datetime.now(dt.timezone.utc)
149
161
 
150
162
  if self.is_real_request:
151
- self.tag("mem_delta", self._get_mem_delta())
152
- if not self.is_ignored() and not self.sent:
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())
153
167
  self.sent = True
154
168
  batch_command = BatchCommand.from_tracked_request(self)
155
169
  if scout_config.value("log_payload_content"):
@@ -217,7 +231,7 @@ class Span(object):
217
231
  should_capture_backtrace=True,
218
232
  ):
219
233
  self.span_id = "span-" + str(uuid4())
220
- self.start_time = dt.datetime.utcnow()
234
+ self.start_time = dt.datetime.now(dt.timezone.utc)
221
235
  self.end_time = None
222
236
  self.request_id = request_id
223
237
  self.operation = operation
@@ -236,7 +250,7 @@ class Span(object):
236
250
  )
237
251
 
238
252
  def stop(self):
239
- self.end_time = dt.datetime.utcnow()
253
+ self.end_time = dt.datetime.now(dt.timezone.utc)
240
254
  self.end_objtrace_counts = objtrace.get_counts()
241
255
 
242
256
  def tag(self, key, value):
@@ -252,7 +266,9 @@ class Span(object):
252
266
  return (self.end_time - self.start_time).total_seconds()
253
267
  else:
254
268
  # Current, running duration
255
- return (dt.datetime.utcnow() - self.start_time).total_seconds()
269
+ return (
270
+ dt.datetime.now(tz=dt.timezone.utc) - self.start_time
271
+ ).total_seconds()
256
272
 
257
273
  # Add any interesting annotations to the span. Assumes that we are in the
258
274
  # process of stopping this span.
@@ -96,7 +96,7 @@ def filter_element(key, value):
96
96
  def ignore_path(path):
97
97
  ignored_paths = scout_config.value("ignore")
98
98
  for ignored in ignored_paths:
99
- if path.startswith(ignored):
99
+ if path.lstrip(" /").startswith(ignored):
100
100
  return True
101
101
  return False
102
102
 
@@ -122,6 +122,7 @@ class ViewTimingMiddleware(object):
122
122
  span = tracked_request.current_span()
123
123
  if span is not None:
124
124
  span.operation = get_controller_name(request)
125
+ tracked_request.operation = span.operation
125
126
 
126
127
  def process_exception(self, request, exception):
127
128
  """
scout_apm/dramatiq.py CHANGED
@@ -17,7 +17,9 @@ class ScoutMiddleware(dramatiq.Middleware):
17
17
  tracked_request = TrackedRequest.instance()
18
18
  tracked_request.tag("queue", message.queue_name)
19
19
  tracked_request.tag("message_id", message.message_id)
20
- tracked_request.start_span(operation="Job/" + message.actor_name)
20
+ operation = "Job/" + message.actor_name
21
+ tracked_request.start_span(operation=operation)
22
+ tracked_request.operation = operation
21
23
 
22
24
  def after_process_message(self, broker, message, result=None, exception=None):
23
25
  if self._do_nothing:
scout_apm/falcon.py CHANGED
@@ -106,6 +106,7 @@ class ScoutMiddleware(object):
106
106
  span = tracked_request.start_span(
107
107
  operation=operation, should_capture_backtrace=False
108
108
  )
109
+ tracked_request.operation = operation
109
110
  req.context.scout_resource_span = span
110
111
 
111
112
  def _name_operation(self, req, responder, resource):
@@ -47,6 +47,7 @@ class ScoutApm(object):
47
47
 
48
48
  tracked_request = TrackedRequest.instance()
49
49
  tracked_request.is_real_request = True
50
+ tracked_request.operation = operation
50
51
  request._scout_tracked_request = tracked_request
51
52
 
52
53
  werkzeug_track_request_data(request, tracked_request)
scout_apm/huey.py CHANGED
@@ -30,6 +30,7 @@ def scout_on_pre_execute(task):
30
30
 
31
31
  operation = "Job/{}.{}".format(task.__module__, task.__class__.__name__)
32
32
  tracked_request.start_span(operation=operation)
33
+ tracked_request.operation = operation
33
34
 
34
35
 
35
36
  def scout_on_post_execute(task, task_value, exception):
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,10 +68,16 @@ 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
- queue_time = (dt.datetime.utcnow() - instance.enqueued_at).total_seconds()
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
+ queue_time = (dt.datetime.now(dt.timezone.utc) - queued_at).total_seconds()
69
77
  tracked_request.tag("queue_time", queue_time)
70
-
71
- with tracked_request.span(operation="Job/{}".format(instance.func_name)):
78
+ operation = "Job/{}".format(instance.func_name)
79
+ tracked_request.operation = operation
80
+ with tracked_request.span(operation=operation):
72
81
  try:
73
82
  return wrapped(*args, **kwargs)
74
83
  except Exception:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
2
- Name: scout-apm
3
- Version: 3.1.0
1
+ Metadata-Version: 2.2
2
+ Name: scout_apm
3
+ Version: 3.3.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,22 @@ 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 <6,>=5
36
- Requires-Dist: urllib3
35
+ Requires-Dist: psutil>=5
36
+ Requires-Dist: urllib3~=2.2.0
37
37
  Requires-Dist: certifi
38
- Requires-Dist: wrapt <2.0,>=1.10
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: project-url
48
+ Dynamic: requires-dist
49
+ Dynamic: requires-python
50
+ Dynamic: summary
39
51
 
40
52
  # Scout Python APM Agent
41
53
 
@@ -74,7 +86,7 @@ To use Scout, you'll need to
74
86
 
75
87
  For full installation instructions, including information on configuring Scout
76
88
  via environment variables and troubleshooting, see our
77
- [Python docs](https://docs.scoutapm.com/#python-agent).
89
+ [Python docs](https://scoutapm.com/docs/python).
78
90
 
79
91
  ## Support
80
92
 
@@ -1,64 +1,65 @@
1
- scout_apm/rq.py,sha256=agU9sBcYH9taRaO0iyoWqRpiAJZ4XcG2W3CpB1WXi3E,1836
2
- scout_apm/celery.py,sha256=zj8eG9MerdIAV0Z9W0fKjKb_pdgDNRcvN34K2w-pBhE,4629
3
- scout_apm/bottle.py,sha256=qeVifCaWXu7HMJPIkkExekPEKy4HinZ3oUQero0FbOA,2908
4
- scout_apm/sqlalchemy.py,sha256=MKhRJwiJvJDRR4hQA2W8FG2YMueDFXNOmXbP9sQGVqM,1358
1
+ scout_apm-3.3.0.dist-info/entry_points.txt,sha256=eiVubJRHQCFcJ1fqH_2myVIOlt9xx32sKTRtWs9xWjk,82
2
+ scout_apm-3.3.0.dist-info/RECORD,,
3
+ scout_apm-3.3.0.dist-info/METADATA,sha256=us-cfey-lzP4bYukYOSb9VuI88IgE8pla3szPXPyYY0,3386
4
+ scout_apm-3.3.0.dist-info/WHEEL,sha256=kj8YvqeHCGcv7t8HOqVuxTGA5G-VhviS3sSvWOjo8A8,153
5
+ scout_apm-3.3.0.dist-info/top_level.txt,sha256=tXGCTyC-E-TraDQng0CvkawiUZU-h4kkhe-5avNfnTw,10
6
+ scout_apm-3.3.0.dist-info/LICENSE,sha256=IL2YQsmIcNnRK09t7_ELMSBMdyrMWIJpBOCAhZ9IMCU,1084
7
+ scout_apm/rq.py,sha256=afjakKsiNLXgwPMrBB-o8brMPptEuaIfQZ5Cu70ak1w,2249
8
+ scout_apm/compat.py,sha256=kTG20OAM8SkbVQZS_-bd_bn4F0BjpsfGoxfCk7kyYCI,2496
5
9
  scout_apm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- scout_apm/dramatiq.py,sha256=MHOpRIewdXS3polvw_yOpJQPs2kyvdeok8SqGJVUVY4,1348
10
+ scout_apm/sqlalchemy.py,sha256=MKhRJwiJvJDRR4hQA2W8FG2YMueDFXNOmXbP9sQGVqM,1358
11
+ scout_apm/celery.py,sha256=zzM5wn9ScaFk-7YizntSOG8I2WEzEc-crSYZ7_AJjxA,4729
7
12
  scout_apm/hug.py,sha256=tSo8r6oFNFoqF3T7lVXi1CxmrRkOX8DiDJrks58dHyw,1387
8
- scout_apm/huey.py,sha256=fluE_JbNlkZv_Gko5R8jtdGWD8eAlvbOw_mnf6aEGCk,1710
9
- scout_apm/compat.py,sha256=kTG20OAM8SkbVQZS_-bd_bn4F0BjpsfGoxfCk7kyYCI,2496
10
- scout_apm/falcon.py,sha256=MLdbTUnuVo8pAPKtuAw-MWt3mttG1Z-8QfzqiFzbew0,5311
11
- scout_apm/core/web_requests.py,sha256=lHBEldhnCa9Ke9wDp0tiCLKH-r5a6jHi54U33aq5EOY,5667
13
+ scout_apm/falcon.py,sha256=5Ll4RQRz8v_nB_A3cx65I1UQ2hxgWaU1uCsAtkbg4F0,5357
14
+ scout_apm/dramatiq.py,sha256=N0VxvprOeXSAmmvE7nmZ5ul1rMuJQC-eEzerwGTC7yc,1424
15
+ scout_apm/bottle.py,sha256=jWBUXgW4-sOiUbr9HYimO5Ryd9dvNVZa5Rb611jxxNI,2934
16
+ scout_apm/huey.py,sha256=XjQNVfO8Z14iu1t9qqv-ZRn_3HqfuQv5t1ZYhlRZ17Y,1752
17
+ scout_apm/async_/starlette.py,sha256=aFnc1XTXC4ug9llYX9dDeiElXQIb6QgA8gq3N_g9-Ew,3812
18
+ scout_apm/async_/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
19
+ scout_apm/async_/api.py,sha256=K8Sh5IiVO-PnJNRzErAQulNxuFcQoMR8-a2-P0IMAZo,1116
20
+ scout_apm/async_/instruments/jinja2.py,sha256=pYrLigxeSgEAWmzWxqh5zCa86TkZQ9A1S7W8uTXE3C8,374
21
+ scout_apm/async_/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ scout_apm/api/__init__.py,sha256=dx6qEJ-xMsWc4E4bvgoxSo5k7vrCELLApJvOoyLWSpA,5660
23
+ scout_apm/django/middleware.py,sha256=fE-bon42PigKZ5SXVxDBHOyPwBmWzZNmzqILKZqM72I,7761
24
+ scout_apm/django/apps.py,sha256=aUQlZG8qQFCi0-vFeU0fmnpI60n3nvpXRWBAjdgG5GA,5260
25
+ scout_apm/django/__init__.py,sha256=RQdhc6VLBlJsiWLVb8yggtaY2pMla1j6YP4yrKPAYgk,207
26
+ scout_apm/django/request.py,sha256=bswpkpJIKO5oFm3OiBRwgXhtZFKx6pbgzGNCLrF8qz0,4853
27
+ scout_apm/django/instruments/template.py,sha256=zr_xlfgEbHoIItAFLbMEc0I4jj4sU8BW5IXMPNMqOI0,963
28
+ scout_apm/django/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ scout_apm/django/instruments/sql.py,sha256=8_n7oUHtrAc7miWrk_k8CLhc9KzS6W0iUbmZJFN-S6g,4227
30
+ scout_apm/django/instruments/huey.py,sha256=tRvBg5wr76W_aGpLfVpaDnLcTpR8jdhGMFOz3Xl580s,653
31
+ scout_apm/flask/__init__.py,sha256=Rt5D7c_NifqIG4cGGh7oo4XmekXacUlZda1WidECodU,4306
32
+ scout_apm/flask/sqlalchemy.py,sha256=HwoYvN0c4LZ9_spCkvVE8gcBJyHFRJCgndFVFKwCzao,757
33
+ scout_apm/core/error.py,sha256=EQ1e1wV9K2vkHXXRzoZxIPW89CF-ce7c8wYTI2b3ajM,3237
34
+ scout_apm/core/tracked_request.py,sha256=zCtv5hWoZ-Cy3yPM0UUaFQkVEyPHTt_BfLXC4FzBv7s,10394
35
+ scout_apm/core/n_plus_one_tracker.py,sha256=hE3YEVEe45Oygq8lR_6ftPRdCab6WYjnN8HeY8wLPL8,978
36
+ scout_apm/core/context.py,sha256=9qpFGKAGIqyer1NqAhBmU8DuVTf-4_doUFSC55vfDm4,4220
37
+ scout_apm/core/queue_time.py,sha256=KA1tsQ8K4kNYSXTfWS-RulTQ41Bhs2-HmqJwIkVxgcc,3199
38
+ scout_apm/core/objtrace.py,sha256=F7L0V2QBzXYUEFfqon3adXkrA9UQJgmWCFprJo-l01I,463
12
39
  scout_apm/core/threading.py,sha256=i_e3Zbqcq-yIDkipcTKCGJwmqGzpiYffl6IK88ylC-g,1624
13
- scout_apm/core/tracked_request.py,sha256=BGmrB8wYGFmLZWpIRizhZz6QgauY0Yx_Xp0fiKln4JQ,9933
14
40
  scout_apm/core/backtrace.py,sha256=x2owyERxWdomJBz4leN3wHsf_P393864UUXW4uFrQtc,3495
15
41
  scout_apm/core/__init__.py,sha256=SnXENrNGgE8_ontzysrZdrARLTsBDz7hXD50zludFbM,2944
16
- scout_apm/core/platform_detection.py,sha256=gWgZNfcnjy97HzDTz-Q2F6xzrch7eVQtTGp2KqcdMEk,1777
17
- scout_apm/core/context.py,sha256=9qpFGKAGIqyer1NqAhBmU8DuVTf-4_doUFSC55vfDm4,4220
18
- scout_apm/core/config.py,sha256=2jRfxjSwgixJSDhF3gHio5sZn67vgfh-Tub_peUBd24,8452
19
- scout_apm/core/error.py,sha256=EQ1e1wV9K2vkHXXRzoZxIPW89CF-ce7c8wYTI2b3ajM,3237
20
- scout_apm/core/n_plus_one_tracker.py,sha256=hE3YEVEe45Oygq8lR_6ftPRdCab6WYjnN8HeY8wLPL8,978
21
- scout_apm/core/metadata.py,sha256=SnB-hiqnJ_t99Oij7cU42kZJqxJvnHrorLUMsivbs98,2226
22
- scout_apm/core/_objtrace.cpython-311-aarch64-linux-gnu.so,sha256=usmITCsl3_BAIgIilGJxYWP8iMEv0B0Zkb00tZN8OKI,85832
23
42
  scout_apm/core/error_service.py,sha256=QKyFGgjc_Ihm8ONv7j0hkPgtBu30U3poyfAD40BJ7jQ,5359
24
- scout_apm/core/objtrace.py,sha256=F7L0V2QBzXYUEFfqon3adXkrA9UQJgmWCFprJo-l01I,463
25
- scout_apm/core/queue_time.py,sha256=F8YQU7hm4RUvth7cUYMj1eAs44Z4dFm89lDiMTA-Vpo,3187
43
+ scout_apm/core/sampler.py,sha256=NZKX2RAnvOfsHY6EN47qaRmKOCoL04slNG1k91n8n_I,5105
44
+ scout_apm/core/platform_detection.py,sha256=gWgZNfcnjy97HzDTz-Q2F6xzrch7eVQtTGp2KqcdMEk,1777
45
+ scout_apm/core/web_requests.py,sha256=DD6xDdDZOfxCmpOTvXEJY3JLQ0n4Zw9SmlSfnN4xC3w,5680
26
46
  scout_apm/core/stacktracer.py,sha256=loNFpOwFtTvf6XsCxari4iFJ8Pe4rZrLmoU691ZuV1M,900
27
- scout_apm/core/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- scout_apm/core/cli/core_agent_manager.py,sha256=iOZhpXCnKZriJznCc2LD7eDXKqmc98zsYtDIrj3tXlU,813
29
- scout_apm/core/samplers/memory.py,sha256=D1py5gmf5GISq6_5HNnwI3HU2EunXaQ2HzqVKRwp5no,444
47
+ scout_apm/core/metadata.py,sha256=huqY2c5vPrQSMZykxQzrSyYYiw8s9E_OIm_ABW9ZzzM,2284
48
+ scout_apm/core/config.py,sha256=TuKWfz3T1rYhRLyJm9ewVBZcRlz3YjkFRBX1YJlVHg8,11868
49
+ scout_apm/core/_objtrace.cpython-311-aarch64-linux-gnu.so,sha256=BGy7KE6ahCSJS7qyfEoRonkUp5rBMim499VxuxW3ii4,85832
50
+ scout_apm/core/samplers/cpu.py,sha256=X0bpFks18cR61pbf69ptyKE0CtnUmBJUJXSRQ_S2IdM,2522
30
51
  scout_apm/core/samplers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- scout_apm/core/samplers/thread.py,sha256=14xSBOK-Yz-pvXOjcjupebs1d33fPe1ks7VvQbMIdjI,1330
32
- scout_apm/core/samplers/cpu.py,sha256=Tsgv87ROes6MvdYNLfReplPrgL0d4HF_Ldvkm3AFjPE,2498
52
+ scout_apm/core/samplers/memory.py,sha256=D1py5gmf5GISq6_5HNnwI3HU2EunXaQ2HzqVKRwp5no,444
53
+ scout_apm/core/samplers/thread.py,sha256=P0s7T99EpYbbkekZnlCJQ787G8g1NnNbZjn8CLtDdUg,1342
54
+ scout_apm/core/cli/core_agent_manager.py,sha256=iOZhpXCnKZriJznCc2LD7eDXKqmc98zsYtDIrj3tXlU,813
55
+ scout_apm/core/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
+ scout_apm/core/agent/commands.py,sha256=Ze4CKBHstpSk5OOzr1sNT84XFY2U9hLUGqfpDBSqhtY,7147
33
57
  scout_apm/core/agent/manager.py,sha256=Vm6JfjRJW9YGQabWeZ5VXuWC7g8P_qBok1NjP6Oqoh4,10385
34
58
  scout_apm/core/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
59
  scout_apm/core/agent/socket.py,sha256=GR0tfElZNxiCRCwtvHiqLU_37GIcz_ABQ3B7Jh-XeP8,6777
36
- scout_apm/core/agent/commands.py,sha256=a64GfyvF3K2eGFazI71sLx7sFIHMxN27klAgprT7Bp8,6613
37
- scout_apm/api/__init__.py,sha256=rRryDuK5bT9C3qRAiUjt6y0qqlQA2-N2D0KJUhZ8w0w,5614
38
- scout_apm/flask/sqlalchemy.py,sha256=HwoYvN0c4LZ9_spCkvVE8gcBJyHFRJCgndFVFKwCzao,757
39
- scout_apm/flask/__init__.py,sha256=TE4wkYLN7AE3I1GDnyGayyFdPYGCvVabUaW8j2tHnkc,4260
40
- scout_apm/django/request.py,sha256=bswpkpJIKO5oFm3OiBRwgXhtZFKx6pbgzGNCLrF8qz0,4853
41
- scout_apm/django/apps.py,sha256=aUQlZG8qQFCi0-vFeU0fmnpI60n3nvpXRWBAjdgG5GA,5260
42
- scout_apm/django/__init__.py,sha256=RQdhc6VLBlJsiWLVb8yggtaY2pMla1j6YP4yrKPAYgk,207
43
- scout_apm/django/middleware.py,sha256=SN2V9r5l7Fac73kglIrwkCvOSdj2hT2vQroYsd23d7U,7706
44
- scout_apm/django/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- scout_apm/django/instruments/sql.py,sha256=8_n7oUHtrAc7miWrk_k8CLhc9KzS6W0iUbmZJFN-S6g,4227
46
- scout_apm/django/instruments/huey.py,sha256=tRvBg5wr76W_aGpLfVpaDnLcTpR8jdhGMFOz3Xl580s,653
47
- scout_apm/django/instruments/template.py,sha256=zr_xlfgEbHoIItAFLbMEc0I4jj4sU8BW5IXMPNMqOI0,963
48
60
  scout_apm/instruments/elasticsearch.py,sha256=xkkV1wHCd6clAFX7sMMrU75mfxXkKwN0i43aWPZg3is,8272
49
61
  scout_apm/instruments/jinja2.py,sha256=a2-u9klcZQdkCJaylc55a4KGi3tHaNlbMXQr1PW3zI0,3662
62
+ scout_apm/instruments/redis.py,sha256=nrkt35bNI172yDzHmpcXFc972QDxLdOkdvh2gWkcz08,2190
50
63
  scout_apm/instruments/pymongo.py,sha256=wVqA59ciH6LvOH3VRmMZG067U6qypoA6QzLtWp0IdAM,2538
51
64
  scout_apm/instruments/__init__.py,sha256=X76KWdu2jgDdeJtg5DOC6rYPaq0ZXkhjiftxcsem3hI,631
52
65
  scout_apm/instruments/urllib3.py,sha256=riwzKX-sgj9kCbN8E5gvtBlyWPAae8bMvi-JgMuGO_w,2219
53
- scout_apm/instruments/redis.py,sha256=nrkt35bNI172yDzHmpcXFc972QDxLdOkdvh2gWkcz08,2190
54
- scout_apm/async_/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
55
- scout_apm/async_/api.py,sha256=K8Sh5IiVO-PnJNRzErAQulNxuFcQoMR8-a2-P0IMAZo,1116
56
- scout_apm/async_/starlette.py,sha256=uX23RlGK57K7RbJ52FQJdXtOWl9EB14cL8JZOOnEwyE,3696
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.1.0.dist-info/WHEEL,sha256=s4nCzmeJHzzKs_3mY44pZnIB7xTTMwwgEpWpc7aab8A,154
60
- scout_apm-3.1.0.dist-info/top_level.txt,sha256=tXGCTyC-E-TraDQng0CvkawiUZU-h4kkhe-5avNfnTw,10
61
- scout_apm-3.1.0.dist-info/LICENSE,sha256=IL2YQsmIcNnRK09t7_ELMSBMdyrMWIJpBOCAhZ9IMCU,1084
62
- scout_apm-3.1.0.dist-info/entry_points.txt,sha256=eiVubJRHQCFcJ1fqH_2myVIOlt9xx32sKTRtWs9xWjk,82
63
- scout_apm-3.1.0.dist-info/RECORD,,
64
- scout_apm-3.1.0.dist-info/METADATA,sha256=1G2oC--ICt8gostTiHr4bKN-dcVddXC7ICS9ZDqhspA,3138
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp311-cp311-manylinux_2_17_aarch64
5
5
  Tag: cp311-cp311-manylinux2014_aarch64