scout-apm 3.2.1__cp310-cp310-musllinux_1_2_x86_64.whl → 3.4.0__cp310-cp310-musllinux_1_2_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
scout_apm/celery.py 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):
@@ -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",
@@ -49,7 +59,7 @@ class TrackedRequest(object):
49
59
 
50
60
  def __init__(self):
51
61
  self.request_id = "req-" + str(uuid4())
52
- self.start_time = dt.datetime.utcnow()
62
+ self.start_time = dt.datetime.now(dt.timezone.utc)
53
63
  self.end_time = None
54
64
  self.active_spans = []
55
65
  self.complete_spans = []
@@ -147,11 +157,13 @@ class TrackedRequest(object):
147
157
 
148
158
  logger.debug("Stopping request: %s", self.request_id)
149
159
  if self.end_time is None:
150
- self.end_time = dt.datetime.utcnow()
160
+ self.end_time = dt.datetime.now(dt.timezone.utc)
151
161
 
152
162
  if self.is_real_request:
153
- self.tag("mem_delta", self._get_mem_delta())
154
- 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())
155
167
  self.sent = True
156
168
  batch_command = BatchCommand.from_tracked_request(self)
157
169
  if scout_config.value("log_payload_content"):
@@ -219,7 +231,7 @@ class Span(object):
219
231
  should_capture_backtrace=True,
220
232
  ):
221
233
  self.span_id = "span-" + str(uuid4())
222
- self.start_time = dt.datetime.utcnow()
234
+ self.start_time = dt.datetime.now(dt.timezone.utc)
223
235
  self.end_time = None
224
236
  self.request_id = request_id
225
237
  self.operation = operation
@@ -238,7 +250,7 @@ class Span(object):
238
250
  )
239
251
 
240
252
  def stop(self):
241
- self.end_time = dt.datetime.utcnow()
253
+ self.end_time = dt.datetime.now(dt.timezone.utc)
242
254
  self.end_objtrace_counts = objtrace.get_counts()
243
255
 
244
256
  def tag(self, key, value):
@@ -254,7 +266,9 @@ class Span(object):
254
266
  return (self.end_time - self.start_time).total_seconds()
255
267
  else:
256
268
  # Current, running duration
257
- return (dt.datetime.utcnow() - self.start_time).total_seconds()
269
+ return (
270
+ dt.datetime.now(tz=dt.timezone.utc) - self.start_time
271
+ ).total_seconds()
258
272
 
259
273
  # Add any interesting annotations to the span. Assumes that we are in the
260
274
  # process of stopping this span.
@@ -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
 
scout_apm/rq.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # coding=utf-8
2
2
 
3
3
  import datetime as dt
4
+ import logging
4
5
 
5
6
  import wrapt
6
7
  from rq import SimpleWorker as RqSimpleWorker
@@ -14,6 +15,8 @@ from scout_apm.core.tracked_request import TrackedRequest
14
15
  install_attempted = False
15
16
  installed = None
16
17
 
18
+ logger = logging.getLogger(__name__)
19
+
17
20
 
18
21
  def ensure_scout_installed():
19
22
  global install_attempted, installed
@@ -65,7 +68,14 @@ def wrap_perform(wrapped, instance, args, kwargs):
65
68
  tracked_request.is_real_request = True
66
69
  tracked_request.tag("task_id", instance.get_id())
67
70
  tracked_request.tag("queue", instance.origin)
68
- 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
+ else:
77
+ queued_at = instance.enqueued_at
78
+ queue_time = (dt.datetime.now(dt.timezone.utc) - queued_at).total_seconds()
69
79
  tracked_request.tag("queue_time", queue_time)
70
80
  operation = "Job/{}".format(instance.func_name)
71
81
  tracked_request.operation = operation
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: scout_apm
3
- Version: 3.2.1
3
+ Version: 3.4.0
4
4
  Summary: Scout Application Performance Monitoring Agent
5
5
  Home-page: https://github.com/scoutapp/scout_apm_python
6
6
  Author: Scout
@@ -32,10 +32,23 @@ Requires-Python: >=3.8, <4
32
32
  Description-Content-Type: text/markdown
33
33
  License-File: LICENSE
34
34
  Requires-Dist: asgiref
35
- Requires-Dist: psutil<6,>=5
35
+ Requires-Dist: psutil>=5
36
36
  Requires-Dist: urllib3
37
37
  Requires-Dist: certifi
38
38
  Requires-Dist: wrapt<2.0,>=1.10
39
+ Dynamic: author
40
+ Dynamic: author-email
41
+ Dynamic: classifier
42
+ Dynamic: description
43
+ Dynamic: description-content-type
44
+ Dynamic: home-page
45
+ Dynamic: keywords
46
+ Dynamic: license
47
+ Dynamic: license-file
48
+ Dynamic: project-url
49
+ Dynamic: requires-dist
50
+ Dynamic: requires-python
51
+ Dynamic: summary
39
52
 
40
53
  # Scout Python APM Agent
41
54
 
@@ -1,64 +1,65 @@
1
+ scout_apm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ scout_apm/bottle.py,sha256=jWBUXgW4-sOiUbr9HYimO5Ryd9dvNVZa5Rb611jxxNI,2934
3
+ scout_apm/celery.py,sha256=zzM5wn9ScaFk-7YizntSOG8I2WEzEc-crSYZ7_AJjxA,4729
1
4
  scout_apm/compat.py,sha256=kTG20OAM8SkbVQZS_-bd_bn4F0BjpsfGoxfCk7kyYCI,2496
2
- scout_apm/rq.py,sha256=i7RhKtuJYhlCbBbdnY3JxxJm7j0hqhSXUztoclQ_4u8,1903
3
5
  scout_apm/dramatiq.py,sha256=N0VxvprOeXSAmmvE7nmZ5ul1rMuJQC-eEzerwGTC7yc,1424
4
- scout_apm/bottle.py,sha256=jWBUXgW4-sOiUbr9HYimO5Ryd9dvNVZa5Rb611jxxNI,2934
5
6
  scout_apm/falcon.py,sha256=5Ll4RQRz8v_nB_A3cx65I1UQ2hxgWaU1uCsAtkbg4F0,5357
6
- scout_apm/sqlalchemy.py,sha256=MKhRJwiJvJDRR4hQA2W8FG2YMueDFXNOmXbP9sQGVqM,1358
7
- scout_apm/celery.py,sha256=WuN70JpsnSO5Ok_pO3VK3uELuahHi2QOs7SqAeTX_Mw,4695
8
- scout_apm/hug.py,sha256=tSo8r6oFNFoqF3T7lVXi1CxmrRkOX8DiDJrks58dHyw,1387
9
- scout_apm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
7
  scout_apm/huey.py,sha256=XjQNVfO8Z14iu1t9qqv-ZRn_3HqfuQv5t1ZYhlRZ17Y,1752
11
- scout_apm/django/apps.py,sha256=aUQlZG8qQFCi0-vFeU0fmnpI60n3nvpXRWBAjdgG5GA,5260
12
- scout_apm/django/request.py,sha256=bswpkpJIKO5oFm3OiBRwgXhtZFKx6pbgzGNCLrF8qz0,4853
13
- scout_apm/django/middleware.py,sha256=fE-bon42PigKZ5SXVxDBHOyPwBmWzZNmzqILKZqM72I,7761
14
- scout_apm/django/__init__.py,sha256=RQdhc6VLBlJsiWLVb8yggtaY2pMla1j6YP4yrKPAYgk,207
15
- scout_apm/django/instruments/template.py,sha256=zr_xlfgEbHoIItAFLbMEc0I4jj4sU8BW5IXMPNMqOI0,963
16
- scout_apm/django/instruments/sql.py,sha256=8_n7oUHtrAc7miWrk_k8CLhc9KzS6W0iUbmZJFN-S6g,4227
17
- scout_apm/django/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- scout_apm/django/instruments/huey.py,sha256=tRvBg5wr76W_aGpLfVpaDnLcTpR8jdhGMFOz3Xl580s,653
19
- scout_apm/core/threading.py,sha256=i_e3Zbqcq-yIDkipcTKCGJwmqGzpiYffl6IK88ylC-g,1624
20
- scout_apm/core/tracked_request.py,sha256=yV-66SFpS64jBuqbThMHa3vDmYOHxmIgMnqJRdyJ3YY,9984
21
- scout_apm/core/stacktracer.py,sha256=loNFpOwFtTvf6XsCxari4iFJ8Pe4rZrLmoU691ZuV1M,900
22
- scout_apm/core/_objtrace.cpython-310-x86_64-linux-gnu.so,sha256=EJiSebpkCTvWZgDcsD-zUG5bGU3vWvIBCPt8fyLyvA0,30024
8
+ scout_apm/hug.py,sha256=tSo8r6oFNFoqF3T7lVXi1CxmrRkOX8DiDJrks58dHyw,1387
9
+ scout_apm/rq.py,sha256=PbXT0uoNFfbTA724q-dxUKD4lsZSK7yOQ2YSfRcrJB0,2300
10
+ scout_apm/sqlalchemy.py,sha256=MKhRJwiJvJDRR4hQA2W8FG2YMueDFXNOmXbP9sQGVqM,1358
11
+ scout_apm/api/__init__.py,sha256=dx6qEJ-xMsWc4E4bvgoxSo5k7vrCELLApJvOoyLWSpA,5660
12
+ scout_apm/async_/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
13
+ scout_apm/async_/api.py,sha256=K8Sh5IiVO-PnJNRzErAQulNxuFcQoMR8-a2-P0IMAZo,1116
14
+ scout_apm/async_/starlette.py,sha256=aFnc1XTXC4ug9llYX9dDeiElXQIb6QgA8gq3N_g9-Ew,3812
15
+ scout_apm/async_/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ scout_apm/async_/instruments/jinja2.py,sha256=pYrLigxeSgEAWmzWxqh5zCa86TkZQ9A1S7W8uTXE3C8,374
17
+ scout_apm/core/__init__.py,sha256=SnXENrNGgE8_ontzysrZdrARLTsBDz7hXD50zludFbM,2944
18
+ scout_apm/core/_objtrace.cpython-310-x86_64-linux-gnu.so,sha256=IE3crb0jBtn7R_jRm-bfBB8sXS8QJdIzWWacfkeYT7M,30000
19
+ scout_apm/core/backtrace.py,sha256=x2owyERxWdomJBz4leN3wHsf_P393864UUXW4uFrQtc,3495
20
+ scout_apm/core/config.py,sha256=TuKWfz3T1rYhRLyJm9ewVBZcRlz3YjkFRBX1YJlVHg8,11868
23
21
  scout_apm/core/context.py,sha256=9qpFGKAGIqyer1NqAhBmU8DuVTf-4_doUFSC55vfDm4,4220
24
- scout_apm/core/platform_detection.py,sha256=gWgZNfcnjy97HzDTz-Q2F6xzrch7eVQtTGp2KqcdMEk,1777
25
- scout_apm/core/objtrace.py,sha256=F7L0V2QBzXYUEFfqon3adXkrA9UQJgmWCFprJo-l01I,463
26
22
  scout_apm/core/error.py,sha256=EQ1e1wV9K2vkHXXRzoZxIPW89CF-ce7c8wYTI2b3ajM,3237
27
23
  scout_apm/core/error_service.py,sha256=QKyFGgjc_Ihm8ONv7j0hkPgtBu30U3poyfAD40BJ7jQ,5359
28
- scout_apm/core/web_requests.py,sha256=lHBEldhnCa9Ke9wDp0tiCLKH-r5a6jHi54U33aq5EOY,5667
29
- scout_apm/core/metadata.py,sha256=SnB-hiqnJ_t99Oij7cU42kZJqxJvnHrorLUMsivbs98,2226
24
+ scout_apm/core/metadata.py,sha256=huqY2c5vPrQSMZykxQzrSyYYiw8s9E_OIm_ABW9ZzzM,2284
30
25
  scout_apm/core/n_plus_one_tracker.py,sha256=hE3YEVEe45Oygq8lR_6ftPRdCab6WYjnN8HeY8wLPL8,978
31
- scout_apm/core/__init__.py,sha256=SnXENrNGgE8_ontzysrZdrARLTsBDz7hXD50zludFbM,2944
32
- scout_apm/core/queue_time.py,sha256=F8YQU7hm4RUvth7cUYMj1eAs44Z4dFm89lDiMTA-Vpo,3187
33
- scout_apm/core/backtrace.py,sha256=x2owyERxWdomJBz4leN3wHsf_P393864UUXW4uFrQtc,3495
34
- scout_apm/core/config.py,sha256=2jRfxjSwgixJSDhF3gHio5sZn67vgfh-Tub_peUBd24,8452
26
+ scout_apm/core/objtrace.py,sha256=F7L0V2QBzXYUEFfqon3adXkrA9UQJgmWCFprJo-l01I,463
27
+ scout_apm/core/platform_detection.py,sha256=gWgZNfcnjy97HzDTz-Q2F6xzrch7eVQtTGp2KqcdMEk,1777
28
+ scout_apm/core/queue_time.py,sha256=KA1tsQ8K4kNYSXTfWS-RulTQ41Bhs2-HmqJwIkVxgcc,3199
29
+ scout_apm/core/sampler.py,sha256=NZKX2RAnvOfsHY6EN47qaRmKOCoL04slNG1k91n8n_I,5105
30
+ scout_apm/core/stacktracer.py,sha256=loNFpOwFtTvf6XsCxari4iFJ8Pe4rZrLmoU691ZuV1M,900
31
+ scout_apm/core/threading.py,sha256=i_e3Zbqcq-yIDkipcTKCGJwmqGzpiYffl6IK88ylC-g,1624
32
+ scout_apm/core/tracked_request.py,sha256=zCtv5hWoZ-Cy3yPM0UUaFQkVEyPHTt_BfLXC4FzBv7s,10394
33
+ scout_apm/core/web_requests.py,sha256=DD6xDdDZOfxCmpOTvXEJY3JLQ0n4Zw9SmlSfnN4xC3w,5680
34
+ scout_apm/core/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
+ scout_apm/core/agent/commands.py,sha256=Ze4CKBHstpSk5OOzr1sNT84XFY2U9hLUGqfpDBSqhtY,7147
35
36
  scout_apm/core/agent/manager.py,sha256=Vm6JfjRJW9YGQabWeZ5VXuWC7g8P_qBok1NjP6Oqoh4,10385
36
- scout_apm/core/agent/commands.py,sha256=a64GfyvF3K2eGFazI71sLx7sFIHMxN27klAgprT7Bp8,6613
37
37
  scout_apm/core/agent/socket.py,sha256=GR0tfElZNxiCRCwtvHiqLU_37GIcz_ABQ3B7Jh-XeP8,6777
38
- scout_apm/core/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- scout_apm/core/cli/core_agent_manager.py,sha256=iOZhpXCnKZriJznCc2LD7eDXKqmc98zsYtDIrj3tXlU,813
40
38
  scout_apm/core/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- scout_apm/core/samplers/thread.py,sha256=14xSBOK-Yz-pvXOjcjupebs1d33fPe1ks7VvQbMIdjI,1330
39
+ scout_apm/core/cli/core_agent_manager.py,sha256=iOZhpXCnKZriJznCc2LD7eDXKqmc98zsYtDIrj3tXlU,813
42
40
  scout_apm/core/samplers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- scout_apm/core/samplers/cpu.py,sha256=Tsgv87ROes6MvdYNLfReplPrgL0d4HF_Ldvkm3AFjPE,2498
41
+ scout_apm/core/samplers/cpu.py,sha256=X0bpFks18cR61pbf69ptyKE0CtnUmBJUJXSRQ_S2IdM,2522
44
42
  scout_apm/core/samplers/memory.py,sha256=D1py5gmf5GISq6_5HNnwI3HU2EunXaQ2HzqVKRwp5no,444
45
- scout_apm/api/__init__.py,sha256=dx6qEJ-xMsWc4E4bvgoxSo5k7vrCELLApJvOoyLWSpA,5660
43
+ scout_apm/core/samplers/thread.py,sha256=P0s7T99EpYbbkekZnlCJQ787G8g1NnNbZjn8CLtDdUg,1342
44
+ scout_apm/django/__init__.py,sha256=RQdhc6VLBlJsiWLVb8yggtaY2pMla1j6YP4yrKPAYgk,207
45
+ scout_apm/django/apps.py,sha256=aUQlZG8qQFCi0-vFeU0fmnpI60n3nvpXRWBAjdgG5GA,5260
46
+ scout_apm/django/middleware.py,sha256=fE-bon42PigKZ5SXVxDBHOyPwBmWzZNmzqILKZqM72I,7761
47
+ scout_apm/django/request.py,sha256=bswpkpJIKO5oFm3OiBRwgXhtZFKx6pbgzGNCLrF8qz0,4853
48
+ scout_apm/django/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ scout_apm/django/instruments/huey.py,sha256=tRvBg5wr76W_aGpLfVpaDnLcTpR8jdhGMFOz3Xl580s,653
50
+ scout_apm/django/instruments/sql.py,sha256=8_n7oUHtrAc7miWrk_k8CLhc9KzS6W0iUbmZJFN-S6g,4227
51
+ scout_apm/django/instruments/template.py,sha256=zr_xlfgEbHoIItAFLbMEc0I4jj4sU8BW5IXMPNMqOI0,963
52
+ scout_apm/flask/__init__.py,sha256=Rt5D7c_NifqIG4cGGh7oo4XmekXacUlZda1WidECodU,4306
53
+ scout_apm/flask/sqlalchemy.py,sha256=HwoYvN0c4LZ9_spCkvVE8gcBJyHFRJCgndFVFKwCzao,757
54
+ scout_apm/instruments/__init__.py,sha256=X76KWdu2jgDdeJtg5DOC6rYPaq0ZXkhjiftxcsem3hI,631
55
+ scout_apm/instruments/elasticsearch.py,sha256=xkkV1wHCd6clAFX7sMMrU75mfxXkKwN0i43aWPZg3is,8272
46
56
  scout_apm/instruments/jinja2.py,sha256=a2-u9klcZQdkCJaylc55a4KGi3tHaNlbMXQr1PW3zI0,3662
57
+ scout_apm/instruments/pymongo.py,sha256=wVqA59ciH6LvOH3VRmMZG067U6qypoA6QzLtWp0IdAM,2538
47
58
  scout_apm/instruments/redis.py,sha256=nrkt35bNI172yDzHmpcXFc972QDxLdOkdvh2gWkcz08,2190
48
59
  scout_apm/instruments/urllib3.py,sha256=riwzKX-sgj9kCbN8E5gvtBlyWPAae8bMvi-JgMuGO_w,2219
49
- scout_apm/instruments/pymongo.py,sha256=wVqA59ciH6LvOH3VRmMZG067U6qypoA6QzLtWp0IdAM,2538
50
- scout_apm/instruments/__init__.py,sha256=X76KWdu2jgDdeJtg5DOC6rYPaq0ZXkhjiftxcsem3hI,631
51
- scout_apm/instruments/elasticsearch.py,sha256=xkkV1wHCd6clAFX7sMMrU75mfxXkKwN0i43aWPZg3is,8272
52
- scout_apm/flask/sqlalchemy.py,sha256=HwoYvN0c4LZ9_spCkvVE8gcBJyHFRJCgndFVFKwCzao,757
53
- scout_apm/flask/__init__.py,sha256=Rt5D7c_NifqIG4cGGh7oo4XmekXacUlZda1WidECodU,4306
54
- scout_apm/async_/api.py,sha256=K8Sh5IiVO-PnJNRzErAQulNxuFcQoMR8-a2-P0IMAZo,1116
55
- scout_apm/async_/starlette.py,sha256=aFnc1XTXC4ug9llYX9dDeiElXQIb6QgA8gq3N_g9-Ew,3812
56
- scout_apm/async_/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
57
- scout_apm/async_/instruments/jinja2.py,sha256=pYrLigxeSgEAWmzWxqh5zCa86TkZQ9A1S7W8uTXE3C8,374
58
- scout_apm/async_/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
- scout_apm-3.2.1.dist-info/WHEEL,sha256=G6WAJh_GXUMYPkzQt82j5xHS9Nx3k6AYK5BZjp4Pr1U,112
60
- scout_apm-3.2.1.dist-info/LICENSE,sha256=IL2YQsmIcNnRK09t7_ELMSBMdyrMWIJpBOCAhZ9IMCU,1084
61
- scout_apm-3.2.1.dist-info/RECORD,,
62
- scout_apm-3.2.1.dist-info/METADATA,sha256=Pqd8zA2mMd2PaN7fgdQMMzRFvM2Un-Qxg6NK5KcYcoI,3129
63
- scout_apm-3.2.1.dist-info/top_level.txt,sha256=tXGCTyC-E-TraDQng0CvkawiUZU-h4kkhe-5avNfnTw,10
64
- scout_apm-3.2.1.dist-info/entry_points.txt,sha256=eiVubJRHQCFcJ1fqH_2myVIOlt9xx32sKTRtWs9xWjk,82
60
+ scout_apm-3.4.0.dist-info/METADATA,sha256=A4hDk-f1t70l9tTZ7I9_EvOI1SzKraJViblRJSbmEsg,3401
61
+ scout_apm-3.4.0.dist-info/WHEEL,sha256=Y1po9wJemUxV3QfSxUJEf7hMWR6d4KoBJZzLLzcgMiE,112
62
+ scout_apm-3.4.0.dist-info/entry_points.txt,sha256=eiVubJRHQCFcJ1fqH_2myVIOlt9xx32sKTRtWs9xWjk,82
63
+ scout_apm-3.4.0.dist-info/top_level.txt,sha256=tXGCTyC-E-TraDQng0CvkawiUZU-h4kkhe-5avNfnTw,10
64
+ scout_apm-3.4.0.dist-info/RECORD,,
65
+ scout_apm-3.4.0.dist-info/licenses/LICENSE,sha256=IL2YQsmIcNnRK09t7_ELMSBMdyrMWIJpBOCAhZ9IMCU,1084
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (80.4.0)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp310-cp310-musllinux_1_2_x86_64
5
5