scout-apm 3.2.1__cp38-cp38-macosx_11_0_arm64.whl → 3.4.0__cp38-cp38-macosx_11_0_arm64.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 +3 -1
 - scout_apm/core/_objtrace.cpython-38-darwin.so +0 -0
 - scout_apm/core/agent/commands.py +20 -7
 - scout_apm/core/config.py +122 -29
 - scout_apm/core/metadata.py +3 -3
 - scout_apm/core/queue_time.py +1 -1
 - scout_apm/core/sampler.py +149 -0
 - scout_apm/core/samplers/cpu.py +2 -2
 - scout_apm/core/samplers/thread.py +1 -1
 - scout_apm/core/tracked_request.py +21 -7
 - scout_apm/core/web_requests.py +1 -1
 - scout_apm/rq.py +11 -1
 - {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info}/METADATA +2 -2
 - {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info}/RECORD +18 -17
 - {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info}/WHEEL +1 -1
 - {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info}/LICENSE +0 -0
 - {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info}/entry_points.txt +0 -0
 - {scout_apm-3.2.1.dist-info → scout_apm-3.4.0.dist-info}/top_level.txt +0 -0
 
    
        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( 
     | 
| 
      
 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):
         
     | 
| 
         Binary file 
     | 
    
        scout_apm/core/agent/commands.py
    CHANGED
    
    | 
         @@ -1,5 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # coding=utf-8
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
      
 3 
     | 
    
         
            +
            import datetime as dt
         
     | 
| 
       3 
4 
     | 
    
         
             
            import logging
         
     | 
| 
       4 
5 
     | 
    
         
             
            import re
         
     | 
| 
       5 
6 
     | 
    
         | 
| 
         @@ -10,6 +11,18 @@ logger = logging.getLogger(__name__) 
     | 
|
| 
       10 
11 
     | 
    
         
             
            key_regex = re.compile(r"^[a-zA-Z0-9]{20}$")
         
     | 
| 
       11 
12 
     | 
    
         | 
| 
       12 
13 
     | 
    
         | 
| 
      
 14 
     | 
    
         
            +
            def format_dt_for_core_agent(event_time: dt.datetime) -> str:
         
     | 
| 
      
 15 
     | 
    
         
            +
                """
         
     | 
| 
      
 16 
     | 
    
         
            +
                Returns expected format for Core Agent compatibility.
         
     | 
| 
      
 17 
     | 
    
         
            +
                Coerce any tz-aware datetime to UTC just in case.
         
     | 
| 
      
 18 
     | 
    
         
            +
                """
         
     | 
| 
      
 19 
     | 
    
         
            +
                # if we somehow got a naive datetime, convert it to UTC
         
     | 
| 
      
 20 
     | 
    
         
            +
                if event_time.tzinfo is None:
         
     | 
| 
      
 21 
     | 
    
         
            +
                    logger.warning("Naive datetime passed to format_dt_for_core_agent")
         
     | 
| 
      
 22 
     | 
    
         
            +
                    event_time = event_time.astimezone(dt.timezone.utc)
         
     | 
| 
      
 23 
     | 
    
         
            +
                return event_time.isoformat()
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
       13 
26 
     | 
    
         
             
            class Register(object):
         
     | 
| 
       14 
27 
     | 
    
         
             
                __slots__ = ("app", "key", "hostname")
         
     | 
| 
       15 
28 
     | 
    
         | 
| 
         @@ -49,7 +62,7 @@ class StartSpan(object): 
     | 
|
| 
       49 
62 
     | 
    
         
             
                def message(self):
         
     | 
| 
       50 
63 
     | 
    
         
             
                    return {
         
     | 
| 
       51 
64 
     | 
    
         
             
                        "StartSpan": {
         
     | 
| 
       52 
     | 
    
         
            -
                            "timestamp": self.timestamp 
     | 
| 
      
 65 
     | 
    
         
            +
                            "timestamp": format_dt_for_core_agent(self.timestamp),
         
     | 
| 
       53 
66 
     | 
    
         
             
                            "request_id": self.request_id,
         
     | 
| 
       54 
67 
     | 
    
         
             
                            "span_id": self.span_id,
         
     | 
| 
       55 
68 
     | 
    
         
             
                            "parent_id": self.parent,
         
     | 
| 
         @@ -69,7 +82,7 @@ class StopSpan(object): 
     | 
|
| 
       69 
82 
     | 
    
         
             
                def message(self):
         
     | 
| 
       70 
83 
     | 
    
         
             
                    return {
         
     | 
| 
       71 
84 
     | 
    
         
             
                        "StopSpan": {
         
     | 
| 
       72 
     | 
    
         
            -
                            "timestamp": self.timestamp 
     | 
| 
      
 85 
     | 
    
         
            +
                            "timestamp": format_dt_for_core_agent(self.timestamp),
         
     | 
| 
       73 
86 
     | 
    
         
             
                            "request_id": self.request_id,
         
     | 
| 
       74 
87 
     | 
    
         
             
                            "span_id": self.span_id,
         
     | 
| 
       75 
88 
     | 
    
         
             
                        }
         
     | 
| 
         @@ -86,7 +99,7 @@ class StartRequest(object): 
     | 
|
| 
       86 
99 
     | 
    
         
             
                def message(self):
         
     | 
| 
       87 
100 
     | 
    
         
             
                    return {
         
     | 
| 
       88 
101 
     | 
    
         
             
                        "StartRequest": {
         
     | 
| 
       89 
     | 
    
         
            -
                            "timestamp": self.timestamp 
     | 
| 
      
 102 
     | 
    
         
            +
                            "timestamp": format_dt_for_core_agent(self.timestamp),
         
     | 
| 
       90 
103 
     | 
    
         
             
                            "request_id": self.request_id,
         
     | 
| 
       91 
104 
     | 
    
         
             
                        }
         
     | 
| 
       92 
105 
     | 
    
         
             
                    }
         
     | 
| 
         @@ -102,7 +115,7 @@ class FinishRequest(object): 
     | 
|
| 
       102 
115 
     | 
    
         
             
                def message(self):
         
     | 
| 
       103 
116 
     | 
    
         
             
                    return {
         
     | 
| 
       104 
117 
     | 
    
         
             
                        "FinishRequest": {
         
     | 
| 
       105 
     | 
    
         
            -
                            "timestamp": self.timestamp 
     | 
| 
      
 118 
     | 
    
         
            +
                            "timestamp": format_dt_for_core_agent(self.timestamp),
         
     | 
| 
       106 
119 
     | 
    
         
             
                            "request_id": self.request_id,
         
     | 
| 
       107 
120 
     | 
    
         
             
                        }
         
     | 
| 
       108 
121 
     | 
    
         
             
                    }
         
     | 
| 
         @@ -121,7 +134,7 @@ class TagSpan(object): 
     | 
|
| 
       121 
134 
     | 
    
         
             
                def message(self):
         
     | 
| 
       122 
135 
     | 
    
         
             
                    return {
         
     | 
| 
       123 
136 
     | 
    
         
             
                        "TagSpan": {
         
     | 
| 
       124 
     | 
    
         
            -
                            "timestamp": self.timestamp 
     | 
| 
      
 137 
     | 
    
         
            +
                            "timestamp": format_dt_for_core_agent(self.timestamp),
         
     | 
| 
       125 
138 
     | 
    
         
             
                            "request_id": self.request_id,
         
     | 
| 
       126 
139 
     | 
    
         
             
                            "span_id": self.span_id,
         
     | 
| 
       127 
140 
     | 
    
         
             
                            "tag": self.tag,
         
     | 
| 
         @@ -142,7 +155,7 @@ class TagRequest(object): 
     | 
|
| 
       142 
155 
     | 
    
         
             
                def message(self):
         
     | 
| 
       143 
156 
     | 
    
         
             
                    return {
         
     | 
| 
       144 
157 
     | 
    
         
             
                        "TagRequest": {
         
     | 
| 
       145 
     | 
    
         
            -
                            "timestamp": self.timestamp 
     | 
| 
      
 158 
     | 
    
         
            +
                            "timestamp": format_dt_for_core_agent(self.timestamp),
         
     | 
| 
       146 
159 
     | 
    
         
             
                            "request_id": self.request_id,
         
     | 
| 
       147 
160 
     | 
    
         
             
                            "tag": self.tag,
         
     | 
| 
       148 
161 
     | 
    
         
             
                            "value": self.value,
         
     | 
| 
         @@ -162,7 +175,7 @@ class ApplicationEvent(object): 
     | 
|
| 
       162 
175 
     | 
    
         
             
                def message(self):
         
     | 
| 
       163 
176 
     | 
    
         
             
                    return {
         
     | 
| 
       164 
177 
     | 
    
         
             
                        "ApplicationEvent": {
         
     | 
| 
       165 
     | 
    
         
            -
                            "timestamp": self.timestamp 
     | 
| 
      
 178 
     | 
    
         
            +
                            "timestamp": format_dt_for_core_agent(self.timestamp),
         
     | 
| 
       166 
179 
     | 
    
         
             
                            "event_type": self.event_type,
         
     | 
| 
       167 
180 
     | 
    
         
             
                            "event_value": self.event_value,
         
     | 
| 
       168 
181 
     | 
    
         
             
                            "source": self.source,
         
     | 
    
        scout_apm/core/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":  
     | 
| 
      
 234 
     | 
    
         
            +
                        "download_url": (
         
     | 
| 
      
 235 
     | 
    
         
            +
                            "https://s3-us-west-1.amazonaws.com/scout-public-downloads/"
         
     | 
| 
      
 236 
     | 
    
         
            +
                            "apm_core_agent/release"
         
     | 
| 
      
 237 
     | 
    
         
            +
                        ),  # noqa: B950
         
     | 
| 
       227 
238 
     | 
    
         
             
                        "errors_batch_size": 5,
         
     | 
| 
       228 
239 
     | 
    
         
             
                        "errors_enabled": True,
         
     | 
| 
       229 
240 
     | 
    
         
             
                        "errors_ignored_exceptions": (),
         
     | 
| 
         @@ -231,26 +242,34 @@ class Defaults(object): 
     | 
|
| 
       231 
242 
     | 
    
         
             
                        "framework": "",
         
     | 
| 
       232 
243 
     | 
    
         
             
                        "framework_version": "",
         
     | 
| 
       233 
244 
     | 
    
         
             
                        "hostname": None,
         
     | 
| 
      
 245 
     | 
    
         
            +
                        "ignore": [],
         
     | 
| 
      
 246 
     | 
    
         
            +
                        "ignore_endpoints": [],
         
     | 
| 
      
 247 
     | 
    
         
            +
                        "ignore_jobs": [],
         
     | 
| 
       234 
248 
     | 
    
         
             
                        "key": "",
         
     | 
| 
       235 
249 
     | 
    
         
             
                        "log_payload_content": False,
         
     | 
| 
       236 
250 
     | 
    
         
             
                        "monitor": False,
         
     | 
| 
       237 
251 
     | 
    
         
             
                        "name": "Python App",
         
     | 
| 
       238 
252 
     | 
    
         
             
                        "revision_sha": self._git_revision_sha(),
         
     | 
| 
      
 253 
     | 
    
         
            +
                        "sample_rate": 100,
         
     | 
| 
      
 254 
     | 
    
         
            +
                        "sample_endpoints": [],
         
     | 
| 
      
 255 
     | 
    
         
            +
                        "endpoint_sample_rate": None,
         
     | 
| 
      
 256 
     | 
    
         
            +
                        "sample_jobs": [],
         
     | 
| 
      
 257 
     | 
    
         
            +
                        "job_sample_rate": None,
         
     | 
| 
       239 
258 
     | 
    
         
             
                        "scm_subdirectory": "",
         
     | 
| 
       240 
259 
     | 
    
         
             
                        "shutdown_message_enabled": True,
         
     | 
| 
       241 
260 
     | 
    
         
             
                        "shutdown_timeout_seconds": 2.0,
         
     | 
| 
       242 
261 
     | 
    
         
             
                        "uri_reporting": "filtered_params",
         
     | 
| 
       243 
262 
     | 
    
         
             
                    }
         
     | 
| 
       244 
263 
     | 
    
         | 
| 
       245 
     | 
    
         
            -
                def _git_revision_sha(self):
         
     | 
| 
      
 264 
     | 
    
         
            +
                def _git_revision_sha(self) -> str:
         
     | 
| 
       246 
265 
     | 
    
         
             
                    # N.B. The environment variable SCOUT_REVISION_SHA may also be used,
         
     | 
| 
       247 
266 
     | 
    
         
             
                    # but that will be picked up by Env
         
     | 
| 
       248 
267 
     | 
    
         
             
                    return os.environ.get("HEROKU_SLUG_COMMIT", "")
         
     | 
| 
       249 
268 
     | 
    
         | 
| 
       250 
     | 
    
         
            -
                def has_config(self, key):
         
     | 
| 
      
 269 
     | 
    
         
            +
                def has_config(self, key: str) -> bool:
         
     | 
| 
       251 
270 
     | 
    
         
             
                    return key in self.defaults
         
     | 
| 
       252 
271 
     | 
    
         | 
| 
       253 
     | 
    
         
            -
                def value(self, key):
         
     | 
| 
      
 272 
     | 
    
         
            +
                def value(self, key: str) -> Any:
         
     | 
| 
       254 
273 
     | 
    
         
             
                    return self.defaults[key]
         
     | 
| 
       255 
274 
     | 
    
         | 
| 
       256 
275 
     | 
    
         | 
| 
         @@ -261,14 +280,18 @@ class Null(object): 
     | 
|
| 
       261 
280 
     | 
    
         
             
                Used as the last step of the layered configuration.
         
     | 
| 
       262 
281 
     | 
    
         
             
                """
         
     | 
| 
       263 
282 
     | 
    
         | 
| 
       264 
     | 
    
         
            -
                def has_config(self, key):
         
     | 
| 
      
 283 
     | 
    
         
            +
                def has_config(self, key: str) -> bool:
         
     | 
| 
       265 
284 
     | 
    
         
             
                    return True
         
     | 
| 
       266 
285 
     | 
    
         | 
| 
       267 
     | 
    
         
            -
                def value(self, key):
         
     | 
| 
      
 286 
     | 
    
         
            +
                def value(self, key: str) -> None:
         
     | 
| 
       268 
287 
     | 
    
         
             
                    return None
         
     | 
| 
       269 
288 
     | 
    
         | 
| 
       270 
289 
     | 
    
         | 
| 
       271 
     | 
    
         
            -
            def  
     | 
| 
      
 290 
     | 
    
         
            +
            def _strip_leading_slash(path: str) -> str:
         
     | 
| 
      
 291 
     | 
    
         
            +
                return path.lstrip(" /").strip()
         
     | 
| 
      
 292 
     | 
    
         
            +
             
     | 
| 
      
 293 
     | 
    
         
            +
             
     | 
| 
      
 294 
     | 
    
         
            +
            def convert_to_bool(value: Any) -> bool:
         
     | 
| 
       272 
295 
     | 
    
         
             
                if isinstance(value, bool):
         
     | 
| 
       273 
296 
     | 
    
         
             
                    return value
         
     | 
| 
       274 
297 
     | 
    
         
             
                if isinstance(value, str):
         
     | 
| 
         @@ -277,14 +300,38 @@ def convert_to_bool(value): 
     | 
|
| 
       277 
300 
     | 
    
         
             
                return False
         
     | 
| 
       278 
301 
     | 
    
         | 
| 
       279 
302 
     | 
    
         | 
| 
       280 
     | 
    
         
            -
            def convert_to_float(value):
         
     | 
| 
      
 303 
     | 
    
         
            +
            def convert_to_float(value: Any) -> float:
         
     | 
| 
       281 
304 
     | 
    
         
             
                try:
         
     | 
| 
       282 
305 
     | 
    
         
             
                    return float(value)
         
     | 
| 
       283 
306 
     | 
    
         
             
                except ValueError:
         
     | 
| 
       284 
307 
     | 
    
         
             
                    return 0.0
         
     | 
| 
       285 
308 
     | 
    
         | 
| 
       286 
309 
     | 
    
         | 
| 
       287 
     | 
    
         
            -
            def  
     | 
| 
      
 310 
     | 
    
         
            +
            def convert_sample_rate(value: Any) -> Optional[int]:
         
     | 
| 
      
 311 
     | 
    
         
            +
                """
         
     | 
| 
      
 312 
     | 
    
         
            +
                Converts sample rate to integer, ensuring it's between 0 and 100.
         
     | 
| 
      
 313 
     | 
    
         
            +
                Allows None as a valid value.
         
     | 
| 
      
 314 
     | 
    
         
            +
                """
         
     | 
| 
      
 315 
     | 
    
         
            +
                if value is None:
         
     | 
| 
      
 316 
     | 
    
         
            +
                    return None
         
     | 
| 
      
 317 
     | 
    
         
            +
                try:
         
     | 
| 
      
 318 
     | 
    
         
            +
                    rate = int(value)
         
     | 
| 
      
 319 
     | 
    
         
            +
                    if not (0 <= rate <= 100):
         
     | 
| 
      
 320 
     | 
    
         
            +
                        logger.warning(
         
     | 
| 
      
 321 
     | 
    
         
            +
                            f"Invalid sample rate {rate}. Must be between 0 and 100. "
         
     | 
| 
      
 322 
     | 
    
         
            +
                            "Defaulting to 100."
         
     | 
| 
      
 323 
     | 
    
         
            +
                        )
         
     | 
| 
      
 324 
     | 
    
         
            +
                        return 100
         
     | 
| 
      
 325 
     | 
    
         
            +
                    return rate
         
     | 
| 
      
 326 
     | 
    
         
            +
                except (TypeError, ValueError):
         
     | 
| 
      
 327 
     | 
    
         
            +
                    logger.warning(
         
     | 
| 
      
 328 
     | 
    
         
            +
                        f"Invalid sample rate {value}. Must be a number between 0 and 100. "
         
     | 
| 
      
 329 
     | 
    
         
            +
                        "Defaulting to 100."
         
     | 
| 
      
 330 
     | 
    
         
            +
                    )
         
     | 
| 
      
 331 
     | 
    
         
            +
                    return 100
         
     | 
| 
      
 332 
     | 
    
         
            +
             
     | 
| 
      
 333 
     | 
    
         
            +
             
     | 
| 
      
 334 
     | 
    
         
            +
            def convert_to_list(value: Any) -> List[Any]:
         
     | 
| 
       288 
335 
     | 
    
         
             
                if isinstance(value, list):
         
     | 
| 
       289 
336 
     | 
    
         
             
                    return value
         
     | 
| 
       290 
337 
     | 
    
         
             
                if isinstance(value, tuple):
         
     | 
| 
         @@ -296,13 +343,59 @@ def convert_to_list(value): 
     | 
|
| 
       296 
343 
     | 
    
         
             
                return []
         
     | 
| 
       297 
344 
     | 
    
         | 
| 
       298 
345 
     | 
    
         | 
| 
      
 346 
     | 
    
         
            +
            def convert_ignore_paths(value: Any) -> List[str]:
         
     | 
| 
      
 347 
     | 
    
         
            +
                """
         
     | 
| 
      
 348 
     | 
    
         
            +
                Removes leading slashes from paths and returns a list of strings.
         
     | 
| 
      
 349 
     | 
    
         
            +
                """
         
     | 
| 
      
 350 
     | 
    
         
            +
                raw_paths = convert_to_list(value)
         
     | 
| 
      
 351 
     | 
    
         
            +
                return [_strip_leading_slash(path) for path in raw_paths]
         
     | 
| 
      
 352 
     | 
    
         
            +
             
     | 
| 
      
 353 
     | 
    
         
            +
             
     | 
| 
      
 354 
     | 
    
         
            +
            def convert_endpoint_sampling(value: Union[str, Dict[str, Any]]) -> Dict[str, int]:
         
     | 
| 
      
 355 
     | 
    
         
            +
                """
         
     | 
| 
      
 356 
     | 
    
         
            +
                Converts endpoint sampling configuration from string or dict format
         
     | 
| 
      
 357 
     | 
    
         
            +
                to a normalized dict.
         
     | 
| 
      
 358 
     | 
    
         
            +
                Example: '/endpoint:40,/test:0' -> {'/endpoint': 40, '/test': 0}
         
     | 
| 
      
 359 
     | 
    
         
            +
                """
         
     | 
| 
      
 360 
     | 
    
         
            +
                if isinstance(value, dict):
         
     | 
| 
      
 361 
     | 
    
         
            +
                    return {_strip_leading_slash(k): int(v) for k, v in value.items()}
         
     | 
| 
      
 362 
     | 
    
         
            +
                if isinstance(value, str):
         
     | 
| 
      
 363 
     | 
    
         
            +
                    if not value.strip():
         
     | 
| 
      
 364 
     | 
    
         
            +
                        return {}
         
     | 
| 
      
 365 
     | 
    
         
            +
                    result = {}
         
     | 
| 
      
 366 
     | 
    
         
            +
                    pairs = [pair.strip() for pair in value.split(",")]
         
     | 
| 
      
 367 
     | 
    
         
            +
                    for pair in pairs:
         
     | 
| 
      
 368 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 369 
     | 
    
         
            +
                            endpoint, rate = pair.split(":")
         
     | 
| 
      
 370 
     | 
    
         
            +
                            rate_int = int(rate)
         
     | 
| 
      
 371 
     | 
    
         
            +
                            if not (0 <= rate_int <= 100):
         
     | 
| 
      
 372 
     | 
    
         
            +
                                logger.warning(
         
     | 
| 
      
 373 
     | 
    
         
            +
                                    f"Invalid sampling rate {rate} for endpoint {endpoint}. "
         
     | 
| 
      
 374 
     | 
    
         
            +
                                    "Must be between 0 and 100."
         
     | 
| 
      
 375 
     | 
    
         
            +
                                )
         
     | 
| 
      
 376 
     | 
    
         
            +
                                continue
         
     | 
| 
      
 377 
     | 
    
         
            +
                            result[_strip_leading_slash(endpoint)] = rate_int
         
     | 
| 
      
 378 
     | 
    
         
            +
                        except ValueError:
         
     | 
| 
      
 379 
     | 
    
         
            +
                            logger.warning(f"Invalid sampling configuration: {pair}")
         
     | 
| 
      
 380 
     | 
    
         
            +
                            continue
         
     | 
| 
      
 381 
     | 
    
         
            +
                    return result
         
     | 
| 
      
 382 
     | 
    
         
            +
                return {}
         
     | 
| 
      
 383 
     | 
    
         
            +
             
     | 
| 
      
 384 
     | 
    
         
            +
             
     | 
| 
       299 
385 
     | 
    
         
             
            CONVERSIONS = {
         
     | 
| 
       300 
386 
     | 
    
         
             
                "collect_remote_ip": convert_to_bool,
         
     | 
| 
       301 
387 
     | 
    
         
             
                "core_agent_download": convert_to_bool,
         
     | 
| 
       302 
388 
     | 
    
         
             
                "core_agent_launch": convert_to_bool,
         
     | 
| 
       303 
389 
     | 
    
         
             
                "disabled_instruments": convert_to_list,
         
     | 
| 
       304 
     | 
    
         
            -
                "ignore":  
     | 
| 
      
 390 
     | 
    
         
            +
                "ignore": convert_ignore_paths,
         
     | 
| 
      
 391 
     | 
    
         
            +
                "ignore_endpoints": convert_ignore_paths,
         
     | 
| 
      
 392 
     | 
    
         
            +
                "ignore_jobs": convert_ignore_paths,
         
     | 
| 
       305 
393 
     | 
    
         
             
                "monitor": convert_to_bool,
         
     | 
| 
      
 394 
     | 
    
         
            +
                "sample_rate": convert_sample_rate,
         
     | 
| 
      
 395 
     | 
    
         
            +
                "sample_endpoints": convert_endpoint_sampling,
         
     | 
| 
      
 396 
     | 
    
         
            +
                "endpoint_sample_rate": convert_sample_rate,
         
     | 
| 
      
 397 
     | 
    
         
            +
                "sample_jobs": convert_endpoint_sampling,
         
     | 
| 
      
 398 
     | 
    
         
            +
                "job_sample_rate": convert_sample_rate,
         
     | 
| 
       306 
399 
     | 
    
         
             
                "shutdown_message_enabled": convert_to_bool,
         
     | 
| 
       307 
400 
     | 
    
         
             
                "shutdown_timeout_seconds": convert_to_float,
         
     | 
| 
       308 
401 
     | 
    
         
             
            }
         
     | 
    
        scout_apm/core/metadata.py
    CHANGED
    
    | 
         @@ -4,7 +4,7 @@ import datetime as dt 
     | 
|
| 
       4 
4 
     | 
    
         
             
            import sys
         
     | 
| 
       5 
5 
     | 
    
         
             
            from os import getpid
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
       7 
     | 
    
         
            -
            from scout_apm.core.agent.commands import ApplicationEvent
         
     | 
| 
      
 7 
     | 
    
         
            +
            from scout_apm.core.agent.commands import ApplicationEvent, format_dt_for_core_agent
         
     | 
| 
       8 
8 
     | 
    
         
             
            from scout_apm.core.agent.socket import CoreAgentSocketThread
         
     | 
| 
       9 
9 
     | 
    
         
             
            from scout_apm.core.config import scout_config
         
     | 
| 
       10 
10 
     | 
    
         | 
| 
         @@ -15,7 +15,7 @@ def report_app_metadata(): 
     | 
|
| 
       15 
15 
     | 
    
         
             
                        event_type="scout.metadata",
         
     | 
| 
       16 
16 
     | 
    
         
             
                        event_value=get_metadata(),
         
     | 
| 
       17 
17 
     | 
    
         
             
                        source="Pid: " + str(getpid()),
         
     | 
| 
       18 
     | 
    
         
            -
                        timestamp=dt.datetime. 
     | 
| 
      
 18 
     | 
    
         
            +
                        timestamp=dt.datetime.now(dt.timezone.utc),
         
     | 
| 
       19 
19 
     | 
    
         
             
                    )
         
     | 
| 
       20 
20 
     | 
    
         
             
                )
         
     | 
| 
       21 
21 
     | 
    
         | 
| 
         @@ -24,7 +24,7 @@ def get_metadata(): 
     | 
|
| 
       24 
24 
     | 
    
         
             
                data = {
         
     | 
| 
       25 
25 
     | 
    
         
             
                    "language": "python",
         
     | 
| 
       26 
26 
     | 
    
         
             
                    "language_version": "{}.{}.{}".format(*sys.version_info[:3]),
         
     | 
| 
       27 
     | 
    
         
            -
                    "server_time": dt.datetime. 
     | 
| 
      
 27 
     | 
    
         
            +
                    "server_time": format_dt_for_core_agent(dt.datetime.now(dt.timezone.utc)),
         
     | 
| 
       28 
28 
     | 
    
         
             
                    "framework": scout_config.value("framework"),
         
     | 
| 
       29 
29 
     | 
    
         
             
                    "framework_version": scout_config.value("framework_version"),
         
     | 
| 
       30 
30 
     | 
    
         
             
                    "environment": "",
         
     | 
    
        scout_apm/core/queue_time.py
    CHANGED
    
    | 
         @@ -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. 
     | 
| 
      
 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 
     | 
    
         
            +
                    )
         
     | 
    
        scout_apm/core/samplers/cpu.py
    CHANGED
    
    | 
         @@ -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. 
     | 
| 
      
 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. 
     | 
| 
      
 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. 
     | 
| 
      
 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. 
     | 
| 
      
 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. 
     | 
| 
      
 160 
     | 
    
         
            +
                        self.end_time = dt.datetime.now(dt.timezone.utc)
         
     | 
| 
       151 
161 
     | 
    
         | 
| 
       152 
162 
     | 
    
         
             
                    if self.is_real_request:
         
     | 
| 
       153 
     | 
    
         
            -
                        self. 
     | 
| 
       154 
     | 
    
         
            -
             
     | 
| 
      
 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. 
     | 
| 
      
 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. 
     | 
| 
      
 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 ( 
     | 
| 
      
 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.
         
     | 
    
        scout_apm/core/web_requests.py
    CHANGED
    
    
    
        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 
     | 
    
         
            -
                 
     | 
| 
      
 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 
1 
     | 
    
         
             
            Metadata-Version: 2.1
         
     | 
| 
       2 
2 
     | 
    
         
             
            Name: scout_apm
         
     | 
| 
       3 
     | 
    
         
            -
            Version: 3. 
     | 
| 
      
 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,7 +32,7 @@ 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 
     | 
| 
      
 35 
     | 
    
         
            +
            Requires-Dist: psutil>=5
         
     | 
| 
       36 
36 
     | 
    
         
             
            Requires-Dist: urllib3
         
     | 
| 
       37 
37 
     | 
    
         
             
            Requires-Dist: certifi
         
     | 
| 
       38 
38 
     | 
    
         
             
            Requires-Dist: wrapt<2.0,>=1.10
         
     | 
| 
         @@ -1,46 +1,47 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            scout_apm-3. 
     | 
| 
       2 
     | 
    
         
            -
            scout_apm-3. 
     | 
| 
       3 
     | 
    
         
            -
            scout_apm-3. 
     | 
| 
       4 
     | 
    
         
            -
            scout_apm-3. 
     | 
| 
       5 
     | 
    
         
            -
            scout_apm-3. 
     | 
| 
       6 
     | 
    
         
            -
            scout_apm-3. 
     | 
| 
      
 1 
     | 
    
         
            +
            scout_apm-3.4.0.dist-info/RECORD,,
         
     | 
| 
      
 2 
     | 
    
         
            +
            scout_apm-3.4.0.dist-info/LICENSE,sha256=IL2YQsmIcNnRK09t7_ELMSBMdyrMWIJpBOCAhZ9IMCU,1084
         
     | 
| 
      
 3 
     | 
    
         
            +
            scout_apm-3.4.0.dist-info/WHEEL,sha256=9FabR3Kab7Nb3lO5nBQWtZc544XJ0hYCmiQC2RH2bHM,107
         
     | 
| 
      
 4 
     | 
    
         
            +
            scout_apm-3.4.0.dist-info/entry_points.txt,sha256=eiVubJRHQCFcJ1fqH_2myVIOlt9xx32sKTRtWs9xWjk,82
         
     | 
| 
      
 5 
     | 
    
         
            +
            scout_apm-3.4.0.dist-info/top_level.txt,sha256=tXGCTyC-E-TraDQng0CvkawiUZU-h4kkhe-5avNfnTw,10
         
     | 
| 
      
 6 
     | 
    
         
            +
            scout_apm-3.4.0.dist-info/METADATA,sha256=qZIk_t7WaRFATIJNk-OGwAsh3K0GWDmzDldtV93IG1I,3126
         
     | 
| 
       7 
7 
     | 
    
         
             
            scout_apm/huey.py,sha256=XjQNVfO8Z14iu1t9qqv-ZRn_3HqfuQv5t1ZYhlRZ17Y,1752
         
     | 
| 
       8 
8 
     | 
    
         
             
            scout_apm/compat.py,sha256=kTG20OAM8SkbVQZS_-bd_bn4F0BjpsfGoxfCk7kyYCI,2496
         
     | 
| 
       9 
9 
     | 
    
         
             
            scout_apm/dramatiq.py,sha256=N0VxvprOeXSAmmvE7nmZ5ul1rMuJQC-eEzerwGTC7yc,1424
         
     | 
| 
       10 
10 
     | 
    
         
             
            scout_apm/hug.py,sha256=tSo8r6oFNFoqF3T7lVXi1CxmrRkOX8DiDJrks58dHyw,1387
         
     | 
| 
       11 
     | 
    
         
            -
            scout_apm/rq.py,sha256= 
     | 
| 
      
 11 
     | 
    
         
            +
            scout_apm/rq.py,sha256=PbXT0uoNFfbTA724q-dxUKD4lsZSK7yOQ2YSfRcrJB0,2300
         
     | 
| 
       12 
12 
     | 
    
         
             
            scout_apm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         
     | 
| 
       13 
13 
     | 
    
         
             
            scout_apm/sqlalchemy.py,sha256=MKhRJwiJvJDRR4hQA2W8FG2YMueDFXNOmXbP9sQGVqM,1358
         
     | 
| 
       14 
     | 
    
         
            -
            scout_apm/celery.py,sha256= 
     | 
| 
      
 14 
     | 
    
         
            +
            scout_apm/celery.py,sha256=zzM5wn9ScaFk-7YizntSOG8I2WEzEc-crSYZ7_AJjxA,4729
         
     | 
| 
       15 
15 
     | 
    
         
             
            scout_apm/bottle.py,sha256=jWBUXgW4-sOiUbr9HYimO5Ryd9dvNVZa5Rb611jxxNI,2934
         
     | 
| 
       16 
16 
     | 
    
         
             
            scout_apm/falcon.py,sha256=5Ll4RQRz8v_nB_A3cx65I1UQ2hxgWaU1uCsAtkbg4F0,5357
         
     | 
| 
       17 
17 
     | 
    
         
             
            scout_apm/flask/__init__.py,sha256=Rt5D7c_NifqIG4cGGh7oo4XmekXacUlZda1WidECodU,4306
         
     | 
| 
       18 
18 
     | 
    
         
             
            scout_apm/flask/sqlalchemy.py,sha256=HwoYvN0c4LZ9_spCkvVE8gcBJyHFRJCgndFVFKwCzao,757
         
     | 
| 
       19 
     | 
    
         
            -
            scout_apm/core/tracked_request.py,sha256= 
     | 
| 
       20 
     | 
    
         
            -
            scout_apm/core/queue_time.py,sha256= 
     | 
| 
       21 
     | 
    
         
            -
            scout_apm/core/config.py,sha256= 
     | 
| 
       22 
     | 
    
         
            -
            scout_apm/core/metadata.py,sha256= 
     | 
| 
      
 19 
     | 
    
         
            +
            scout_apm/core/tracked_request.py,sha256=zCtv5hWoZ-Cy3yPM0UUaFQkVEyPHTt_BfLXC4FzBv7s,10394
         
     | 
| 
      
 20 
     | 
    
         
            +
            scout_apm/core/queue_time.py,sha256=KA1tsQ8K4kNYSXTfWS-RulTQ41Bhs2-HmqJwIkVxgcc,3199
         
     | 
| 
      
 21 
     | 
    
         
            +
            scout_apm/core/config.py,sha256=TuKWfz3T1rYhRLyJm9ewVBZcRlz3YjkFRBX1YJlVHg8,11868
         
     | 
| 
      
 22 
     | 
    
         
            +
            scout_apm/core/metadata.py,sha256=huqY2c5vPrQSMZykxQzrSyYYiw8s9E_OIm_ABW9ZzzM,2284
         
     | 
| 
       23 
23 
     | 
    
         
             
            scout_apm/core/error.py,sha256=EQ1e1wV9K2vkHXXRzoZxIPW89CF-ce7c8wYTI2b3ajM,3237
         
     | 
| 
       24 
24 
     | 
    
         
             
            scout_apm/core/n_plus_one_tracker.py,sha256=hE3YEVEe45Oygq8lR_6ftPRdCab6WYjnN8HeY8wLPL8,978
         
     | 
| 
       25 
25 
     | 
    
         
             
            scout_apm/core/__init__.py,sha256=SnXENrNGgE8_ontzysrZdrARLTsBDz7hXD50zludFbM,2944
         
     | 
| 
       26 
26 
     | 
    
         
             
            scout_apm/core/threading.py,sha256=i_e3Zbqcq-yIDkipcTKCGJwmqGzpiYffl6IK88ylC-g,1624
         
     | 
| 
       27 
27 
     | 
    
         
             
            scout_apm/core/context.py,sha256=9qpFGKAGIqyer1NqAhBmU8DuVTf-4_doUFSC55vfDm4,4220
         
     | 
| 
       28 
28 
     | 
    
         
             
            scout_apm/core/objtrace.py,sha256=F7L0V2QBzXYUEFfqon3adXkrA9UQJgmWCFprJo-l01I,463
         
     | 
| 
       29 
     | 
    
         
            -
            scout_apm/core/web_requests.py,sha256= 
     | 
| 
      
 29 
     | 
    
         
            +
            scout_apm/core/web_requests.py,sha256=DD6xDdDZOfxCmpOTvXEJY3JLQ0n4Zw9SmlSfnN4xC3w,5680
         
     | 
| 
       30 
30 
     | 
    
         
             
            scout_apm/core/platform_detection.py,sha256=gWgZNfcnjy97HzDTz-Q2F6xzrch7eVQtTGp2KqcdMEk,1777
         
     | 
| 
       31 
31 
     | 
    
         
             
            scout_apm/core/stacktracer.py,sha256=loNFpOwFtTvf6XsCxari4iFJ8Pe4rZrLmoU691ZuV1M,900
         
     | 
| 
      
 32 
     | 
    
         
            +
            scout_apm/core/sampler.py,sha256=NZKX2RAnvOfsHY6EN47qaRmKOCoL04slNG1k91n8n_I,5105
         
     | 
| 
       32 
33 
     | 
    
         
             
            scout_apm/core/backtrace.py,sha256=x2owyERxWdomJBz4leN3wHsf_P393864UUXW4uFrQtc,3495
         
     | 
| 
       33 
     | 
    
         
            -
            scout_apm/core/_objtrace.cpython-38-darwin.so,sha256= 
     | 
| 
      
 34 
     | 
    
         
            +
            scout_apm/core/_objtrace.cpython-38-darwin.so,sha256=OtpDnebemwnO42bKzG-YnXMnTNKdtCVtNNtcvIss5qM,52224
         
     | 
| 
       34 
35 
     | 
    
         
             
            scout_apm/core/error_service.py,sha256=QKyFGgjc_Ihm8ONv7j0hkPgtBu30U3poyfAD40BJ7jQ,5359
         
     | 
| 
       35 
36 
     | 
    
         
             
            scout_apm/core/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         
     | 
| 
       36 
37 
     | 
    
         
             
            scout_apm/core/agent/socket.py,sha256=GR0tfElZNxiCRCwtvHiqLU_37GIcz_ABQ3B7Jh-XeP8,6777
         
     | 
| 
       37 
     | 
    
         
            -
            scout_apm/core/agent/commands.py,sha256= 
     | 
| 
      
 38 
     | 
    
         
            +
            scout_apm/core/agent/commands.py,sha256=Ze4CKBHstpSk5OOzr1sNT84XFY2U9hLUGqfpDBSqhtY,7147
         
     | 
| 
       38 
39 
     | 
    
         
             
            scout_apm/core/agent/manager.py,sha256=Vm6JfjRJW9YGQabWeZ5VXuWC7g8P_qBok1NjP6Oqoh4,10385
         
     | 
| 
       39 
40 
     | 
    
         
             
            scout_apm/core/cli/core_agent_manager.py,sha256=iOZhpXCnKZriJznCc2LD7eDXKqmc98zsYtDIrj3tXlU,813
         
     | 
| 
       40 
41 
     | 
    
         
             
            scout_apm/core/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         
     | 
| 
       41 
     | 
    
         
            -
            scout_apm/core/samplers/thread.py,sha256= 
     | 
| 
      
 42 
     | 
    
         
            +
            scout_apm/core/samplers/thread.py,sha256=P0s7T99EpYbbkekZnlCJQ787G8g1NnNbZjn8CLtDdUg,1342
         
     | 
| 
       42 
43 
     | 
    
         
             
            scout_apm/core/samplers/memory.py,sha256=D1py5gmf5GISq6_5HNnwI3HU2EunXaQ2HzqVKRwp5no,444
         
     | 
| 
       43 
     | 
    
         
            -
            scout_apm/core/samplers/cpu.py,sha256= 
     | 
| 
      
 44 
     | 
    
         
            +
            scout_apm/core/samplers/cpu.py,sha256=X0bpFks18cR61pbf69ptyKE0CtnUmBJUJXSRQ_S2IdM,2522
         
     | 
| 
       44 
45 
     | 
    
         
             
            scout_apm/core/samplers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         
     | 
| 
       45 
46 
     | 
    
         
             
            scout_apm/instruments/elasticsearch.py,sha256=xkkV1wHCd6clAFX7sMMrU75mfxXkKwN0i43aWPZg3is,8272
         
     | 
| 
       46 
47 
     | 
    
         
             
            scout_apm/instruments/urllib3.py,sha256=riwzKX-sgj9kCbN8E5gvtBlyWPAae8bMvi-JgMuGO_w,2219
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     |