scout-apm 3.1.0__cp38-cp38-macosx_10_9_x86_64.whl → 3.3.0__cp38-cp38-macosx_10_9_x86_64.whl

Sign up to get free protection for your applications and to get access to all the features.
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
1
  Metadata-Version: 2.1
2
- Name: scout-apm
3
- Version: 3.1.0
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,10 @@ 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
39
 
40
40
  # Scout Python APM Agent
41
41
 
@@ -74,7 +74,7 @@ To use Scout, you'll need to
74
74
 
75
75
  For full installation instructions, including information on configuring Scout
76
76
  via environment variables and troubleshooting, see our
77
- [Python docs](https://docs.scoutapm.com/#python-agent).
77
+ [Python docs](https://scoutapm.com/docs/python).
78
78
 
79
79
  ## Support
80
80
 
@@ -1,46 +1,41 @@
1
- scout_apm-3.1.0.dist-info/RECORD,,
2
- scout_apm-3.1.0.dist-info/LICENSE,sha256=IL2YQsmIcNnRK09t7_ELMSBMdyrMWIJpBOCAhZ9IMCU,1084
3
- scout_apm-3.1.0.dist-info/WHEEL,sha256=wt_0D7ETubDJ-d2If9gFyi_RXCxFIy3PzOgckP8MsJY,109
4
- scout_apm-3.1.0.dist-info/entry_points.txt,sha256=eiVubJRHQCFcJ1fqH_2myVIOlt9xx32sKTRtWs9xWjk,82
5
- scout_apm-3.1.0.dist-info/top_level.txt,sha256=tXGCTyC-E-TraDQng0CvkawiUZU-h4kkhe-5avNfnTw,10
6
- scout_apm-3.1.0.dist-info/METADATA,sha256=1G2oC--ICt8gostTiHr4bKN-dcVddXC7ICS9ZDqhspA,3138
7
- scout_apm/huey.py,sha256=fluE_JbNlkZv_Gko5R8jtdGWD8eAlvbOw_mnf6aEGCk,1710
1
+ scout_apm/huey.py,sha256=XjQNVfO8Z14iu1t9qqv-ZRn_3HqfuQv5t1ZYhlRZ17Y,1752
8
2
  scout_apm/compat.py,sha256=kTG20OAM8SkbVQZS_-bd_bn4F0BjpsfGoxfCk7kyYCI,2496
9
- scout_apm/dramatiq.py,sha256=MHOpRIewdXS3polvw_yOpJQPs2kyvdeok8SqGJVUVY4,1348
3
+ scout_apm/dramatiq.py,sha256=N0VxvprOeXSAmmvE7nmZ5ul1rMuJQC-eEzerwGTC7yc,1424
10
4
  scout_apm/hug.py,sha256=tSo8r6oFNFoqF3T7lVXi1CxmrRkOX8DiDJrks58dHyw,1387
11
- scout_apm/rq.py,sha256=agU9sBcYH9taRaO0iyoWqRpiAJZ4XcG2W3CpB1WXi3E,1836
5
+ scout_apm/rq.py,sha256=afjakKsiNLXgwPMrBB-o8brMPptEuaIfQZ5Cu70ak1w,2249
12
6
  scout_apm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
7
  scout_apm/sqlalchemy.py,sha256=MKhRJwiJvJDRR4hQA2W8FG2YMueDFXNOmXbP9sQGVqM,1358
14
- scout_apm/celery.py,sha256=zj8eG9MerdIAV0Z9W0fKjKb_pdgDNRcvN34K2w-pBhE,4629
15
- scout_apm/bottle.py,sha256=qeVifCaWXu7HMJPIkkExekPEKy4HinZ3oUQero0FbOA,2908
16
- scout_apm/falcon.py,sha256=MLdbTUnuVo8pAPKtuAw-MWt3mttG1Z-8QfzqiFzbew0,5311
17
- scout_apm/flask/__init__.py,sha256=TE4wkYLN7AE3I1GDnyGayyFdPYGCvVabUaW8j2tHnkc,4260
8
+ scout_apm/celery.py,sha256=zzM5wn9ScaFk-7YizntSOG8I2WEzEc-crSYZ7_AJjxA,4729
9
+ scout_apm/bottle.py,sha256=jWBUXgW4-sOiUbr9HYimO5Ryd9dvNVZa5Rb611jxxNI,2934
10
+ scout_apm/falcon.py,sha256=5Ll4RQRz8v_nB_A3cx65I1UQ2hxgWaU1uCsAtkbg4F0,5357
11
+ scout_apm/flask/__init__.py,sha256=Rt5D7c_NifqIG4cGGh7oo4XmekXacUlZda1WidECodU,4306
18
12
  scout_apm/flask/sqlalchemy.py,sha256=HwoYvN0c4LZ9_spCkvVE8gcBJyHFRJCgndFVFKwCzao,757
19
- scout_apm/core/tracked_request.py,sha256=BGmrB8wYGFmLZWpIRizhZz6QgauY0Yx_Xp0fiKln4JQ,9933
20
- scout_apm/core/queue_time.py,sha256=F8YQU7hm4RUvth7cUYMj1eAs44Z4dFm89lDiMTA-Vpo,3187
21
- scout_apm/core/config.py,sha256=2jRfxjSwgixJSDhF3gHio5sZn67vgfh-Tub_peUBd24,8452
22
- scout_apm/core/metadata.py,sha256=SnB-hiqnJ_t99Oij7cU42kZJqxJvnHrorLUMsivbs98,2226
13
+ scout_apm/core/tracked_request.py,sha256=zCtv5hWoZ-Cy3yPM0UUaFQkVEyPHTt_BfLXC4FzBv7s,10394
14
+ scout_apm/core/queue_time.py,sha256=KA1tsQ8K4kNYSXTfWS-RulTQ41Bhs2-HmqJwIkVxgcc,3199
15
+ scout_apm/core/config.py,sha256=TuKWfz3T1rYhRLyJm9ewVBZcRlz3YjkFRBX1YJlVHg8,11868
16
+ scout_apm/core/metadata.py,sha256=huqY2c5vPrQSMZykxQzrSyYYiw8s9E_OIm_ABW9ZzzM,2284
23
17
  scout_apm/core/error.py,sha256=EQ1e1wV9K2vkHXXRzoZxIPW89CF-ce7c8wYTI2b3ajM,3237
24
18
  scout_apm/core/n_plus_one_tracker.py,sha256=hE3YEVEe45Oygq8lR_6ftPRdCab6WYjnN8HeY8wLPL8,978
25
19
  scout_apm/core/__init__.py,sha256=SnXENrNGgE8_ontzysrZdrARLTsBDz7hXD50zludFbM,2944
26
20
  scout_apm/core/threading.py,sha256=i_e3Zbqcq-yIDkipcTKCGJwmqGzpiYffl6IK88ylC-g,1624
27
21
  scout_apm/core/context.py,sha256=9qpFGKAGIqyer1NqAhBmU8DuVTf-4_doUFSC55vfDm4,4220
28
22
  scout_apm/core/objtrace.py,sha256=F7L0V2QBzXYUEFfqon3adXkrA9UQJgmWCFprJo-l01I,463
29
- scout_apm/core/web_requests.py,sha256=lHBEldhnCa9Ke9wDp0tiCLKH-r5a6jHi54U33aq5EOY,5667
23
+ scout_apm/core/web_requests.py,sha256=DD6xDdDZOfxCmpOTvXEJY3JLQ0n4Zw9SmlSfnN4xC3w,5680
30
24
  scout_apm/core/platform_detection.py,sha256=gWgZNfcnjy97HzDTz-Q2F6xzrch7eVQtTGp2KqcdMEk,1777
31
25
  scout_apm/core/stacktracer.py,sha256=loNFpOwFtTvf6XsCxari4iFJ8Pe4rZrLmoU691ZuV1M,900
26
+ scout_apm/core/sampler.py,sha256=NZKX2RAnvOfsHY6EN47qaRmKOCoL04slNG1k91n8n_I,5105
32
27
  scout_apm/core/backtrace.py,sha256=x2owyERxWdomJBz4leN3wHsf_P393864UUXW4uFrQtc,3495
33
- scout_apm/core/_objtrace.cpython-38-darwin.so,sha256=GSFtyWwWZAOO6AC9cFQ_YCYWOJ9uIhCKxCk1JiUotAk,35840
28
+ scout_apm/core/_objtrace.cpython-38-darwin.so,sha256=o2y3JMv1mUqfPV5oSeo7C-v7gSnE8jW5SAm6FGczkA8,35288
34
29
  scout_apm/core/error_service.py,sha256=QKyFGgjc_Ihm8ONv7j0hkPgtBu30U3poyfAD40BJ7jQ,5359
35
30
  scout_apm/core/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
31
  scout_apm/core/agent/socket.py,sha256=GR0tfElZNxiCRCwtvHiqLU_37GIcz_ABQ3B7Jh-XeP8,6777
37
- scout_apm/core/agent/commands.py,sha256=a64GfyvF3K2eGFazI71sLx7sFIHMxN27klAgprT7Bp8,6613
32
+ scout_apm/core/agent/commands.py,sha256=Ze4CKBHstpSk5OOzr1sNT84XFY2U9hLUGqfpDBSqhtY,7147
38
33
  scout_apm/core/agent/manager.py,sha256=Vm6JfjRJW9YGQabWeZ5VXuWC7g8P_qBok1NjP6Oqoh4,10385
39
34
  scout_apm/core/cli/core_agent_manager.py,sha256=iOZhpXCnKZriJznCc2LD7eDXKqmc98zsYtDIrj3tXlU,813
40
35
  scout_apm/core/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- scout_apm/core/samplers/thread.py,sha256=14xSBOK-Yz-pvXOjcjupebs1d33fPe1ks7VvQbMIdjI,1330
36
+ scout_apm/core/samplers/thread.py,sha256=P0s7T99EpYbbkekZnlCJQ787G8g1NnNbZjn8CLtDdUg,1342
42
37
  scout_apm/core/samplers/memory.py,sha256=D1py5gmf5GISq6_5HNnwI3HU2EunXaQ2HzqVKRwp5no,444
43
- scout_apm/core/samplers/cpu.py,sha256=Tsgv87ROes6MvdYNLfReplPrgL0d4HF_Ldvkm3AFjPE,2498
38
+ scout_apm/core/samplers/cpu.py,sha256=X0bpFks18cR61pbf69ptyKE0CtnUmBJUJXSRQ_S2IdM,2522
44
39
  scout_apm/core/samplers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
40
  scout_apm/instruments/elasticsearch.py,sha256=xkkV1wHCd6clAFX7sMMrU75mfxXkKwN0i43aWPZg3is,8272
46
41
  scout_apm/instruments/urllib3.py,sha256=riwzKX-sgj9kCbN8E5gvtBlyWPAae8bMvi-JgMuGO_w,2219
@@ -48,8 +43,8 @@ scout_apm/instruments/jinja2.py,sha256=a2-u9klcZQdkCJaylc55a4KGi3tHaNlbMXQr1PW3z
48
43
  scout_apm/instruments/__init__.py,sha256=X76KWdu2jgDdeJtg5DOC6rYPaq0ZXkhjiftxcsem3hI,631
49
44
  scout_apm/instruments/pymongo.py,sha256=wVqA59ciH6LvOH3VRmMZG067U6qypoA6QzLtWp0IdAM,2538
50
45
  scout_apm/instruments/redis.py,sha256=nrkt35bNI172yDzHmpcXFc972QDxLdOkdvh2gWkcz08,2190
51
- scout_apm/api/__init__.py,sha256=rRryDuK5bT9C3qRAiUjt6y0qqlQA2-N2D0KJUhZ8w0w,5614
52
- scout_apm/async_/starlette.py,sha256=uX23RlGK57K7RbJ52FQJdXtOWl9EB14cL8JZOOnEwyE,3696
46
+ scout_apm/api/__init__.py,sha256=dx6qEJ-xMsWc4E4bvgoxSo5k7vrCELLApJvOoyLWSpA,5660
47
+ scout_apm/async_/starlette.py,sha256=aFnc1XTXC4ug9llYX9dDeiElXQIb6QgA8gq3N_g9-Ew,3812
53
48
  scout_apm/async_/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
54
49
  scout_apm/async_/api.py,sha256=K8Sh5IiVO-PnJNRzErAQulNxuFcQoMR8-a2-P0IMAZo,1116
55
50
  scout_apm/async_/instruments/jinja2.py,sha256=pYrLigxeSgEAWmzWxqh5zCa86TkZQ9A1S7W8uTXE3C8,374
@@ -57,8 +52,14 @@ scout_apm/async_/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
57
52
  scout_apm/django/request.py,sha256=bswpkpJIKO5oFm3OiBRwgXhtZFKx6pbgzGNCLrF8qz0,4853
58
53
  scout_apm/django/__init__.py,sha256=RQdhc6VLBlJsiWLVb8yggtaY2pMla1j6YP4yrKPAYgk,207
59
54
  scout_apm/django/apps.py,sha256=aUQlZG8qQFCi0-vFeU0fmnpI60n3nvpXRWBAjdgG5GA,5260
60
- scout_apm/django/middleware.py,sha256=SN2V9r5l7Fac73kglIrwkCvOSdj2hT2vQroYsd23d7U,7706
55
+ scout_apm/django/middleware.py,sha256=fE-bon42PigKZ5SXVxDBHOyPwBmWzZNmzqILKZqM72I,7761
61
56
  scout_apm/django/instruments/huey.py,sha256=tRvBg5wr76W_aGpLfVpaDnLcTpR8jdhGMFOz3Xl580s,653
62
57
  scout_apm/django/instruments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
58
  scout_apm/django/instruments/template.py,sha256=zr_xlfgEbHoIItAFLbMEc0I4jj4sU8BW5IXMPNMqOI0,963
64
59
  scout_apm/django/instruments/sql.py,sha256=8_n7oUHtrAc7miWrk_k8CLhc9KzS6W0iUbmZJFN-S6g,4227
60
+ scout_apm-3.3.0.dist-info/RECORD,,
61
+ scout_apm-3.3.0.dist-info/LICENSE,sha256=IL2YQsmIcNnRK09t7_ELMSBMdyrMWIJpBOCAhZ9IMCU,1084
62
+ scout_apm-3.3.0.dist-info/WHEEL,sha256=w6_R5zToL4dhiDH-J5jk0JB_efQ_6KdbwveuTVp2z9A,108
63
+ scout_apm-3.3.0.dist-info/entry_points.txt,sha256=eiVubJRHQCFcJ1fqH_2myVIOlt9xx32sKTRtWs9xWjk,82
64
+ scout_apm-3.3.0.dist-info/top_level.txt,sha256=tXGCTyC-E-TraDQng0CvkawiUZU-h4kkhe-5avNfnTw,10
65
+ scout_apm-3.3.0.dist-info/METADATA,sha256=biZi_IvzkhsIOTKKdyc5wXQVuN0yWGQPTjSHjvZzztI,3133
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp38-cp38-macosx_10_9_x86_64
5
5