scout-apm 3.1.0__tar.gz → 3.3.0__tar.gz
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-3.1.0 → scout_apm-3.3.0}/CHANGELOG.md +13 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/DEVELOPMENT.md +4 -5
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/PKG-INFO +2 -2
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/README.md +1 -1
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/setup.py +3 -3
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/api/__init__.py +1 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/async_/starlette.py +6 -5
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/bottle.py +3 -3
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/celery.py +6 -2
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/agent/commands.py +20 -7
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/config.py +122 -29
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/metadata.py +3 -3
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/queue_time.py +1 -1
 - scout_apm-3.3.0/src/scout_apm/core/sampler.py +149 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/samplers/cpu.py +2 -2
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/samplers/thread.py +1 -1
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/tracked_request.py +23 -7
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/web_requests.py +1 -1
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/django/middleware.py +1 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/dramatiq.py +3 -1
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/falcon.py +1 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/flask/__init__.py +1 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/huey.py +1 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/rq.py +12 -3
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm.egg-info/PKG-INFO +2 -2
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm.egg-info/SOURCES.txt +1 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm.egg-info/requires.txt +2 -2
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/LICENSE +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/MANIFEST.in +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/pyproject.toml +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/setup.cfg +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/__init__.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/async_/__init__.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/async_/api.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/async_/instruments/__init__.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/async_/instruments/jinja2.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/compat.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/__init__.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/_objtrace.c +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/agent/__init__.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/agent/manager.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/agent/socket.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/backtrace.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/cli/__init__.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/cli/core_agent_manager.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/context.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/error.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/error_service.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/n_plus_one_tracker.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/objtrace.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/platform_detection.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/samplers/__init__.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/samplers/memory.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/stacktracer.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/core/threading.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/django/__init__.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/django/apps.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/django/instruments/__init__.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/django/instruments/huey.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/django/instruments/sql.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/django/instruments/template.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/django/request.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/flask/sqlalchemy.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/hug.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/instruments/__init__.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/instruments/elasticsearch.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/instruments/jinja2.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/instruments/pymongo.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/instruments/redis.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/instruments/urllib3.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm/sqlalchemy.py +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm.egg-info/dependency_links.txt +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm.egg-info/entry_points.txt +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm.egg-info/not-zip-safe +0 -0
 - {scout_apm-3.1.0 → scout_apm-3.3.0}/src/scout_apm.egg-info/top_level.txt +0 -0
 
| 
         @@ -2,6 +2,19 @@ 
     | 
|
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            ## Pending
         
     | 
| 
       4 
4 
     | 
    
         | 
| 
      
 5 
     | 
    
         
            +
            - Change to tz-aware dates internally (Issue #799)
         
     | 
| 
      
 6 
     | 
    
         
            +
            - psutil dependency un-pin (#790)
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            ## [3.3.0] 2025-01-07
         
     | 
| 
      
 9 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 10 
     | 
    
         
            +
            - Added support for down-sampling via Scout configuration.
         
     | 
| 
      
 11 
     | 
    
         
            +
              - Sample rates can be set globally or for specific jobs/endpoints
         
     | 
| 
      
 12 
     | 
    
         
            +
              - Check out our [documentation](https://scoutapm.com/docs/python/configuration#sampling) for more information and example usage.
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            ## [3.2.0] 2024-09-12
         
     | 
| 
      
 15 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 16 
     | 
    
         
            +
            - "Operation" attribute added to TrackedRequest class to better support development of [scout_apm_python_logging](https://github.com/scoutapp/scout_apm_python_logging)
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
       5 
18 
     | 
    
         
             
            ## [3.1.0] 2023-12-18
         
     | 
| 
       6 
19 
     | 
    
         
             
            ### Added
         
     | 
| 
       7 
20 
     | 
    
         
             
            - Updates Core Agent to v1.5.0
         
     | 
| 
         @@ -85,7 +85,7 @@ Running the test app 
     | 
|
| 
       85 
85 
     | 
    
         
             
            --------------------
         
     | 
| 
       86 
86 
     | 
    
         | 
| 
       87 
87 
     | 
    
         
             
            Note: this has not been tested in a while. Instead, the
         
     | 
| 
       88 
     | 
    
         
            -
            [scout-test-apps repo](https://github.com/ 
     | 
| 
      
 88 
     | 
    
         
            +
            [scout-test-apps repo](https://github.com/scoutapp/scout-test-apps) has
         
     | 
| 
       89 
89 
     | 
    
         
             
            been used with many individual scout apps.
         
     | 
| 
       90 
90 
     | 
    
         | 
| 
       91 
91 
     | 
    
         
             
            Add the following env variables:
         
     | 
| 
         @@ -133,7 +133,6 @@ architectures. Its documentation is excellent. 
     | 
|
| 
       133 
133 
     | 
    
         
             
            Documentation
         
     | 
| 
       134 
134 
     | 
    
         
             
            -------------
         
     | 
| 
       135 
135 
     | 
    
         | 
| 
       136 
     | 
    
         
            -
            The user documentation is stored in the [ 
     | 
| 
       137 
     | 
    
         
            -
             
     | 
| 
       138 
     | 
    
         
            -
             
     | 
| 
       139 
     | 
    
         
            -
            features.
         
     | 
| 
      
 136 
     | 
    
         
            +
            The user documentation is stored in the [scout-documentation](https://github.com/scoutapp/scout-documentation) repo.
         
     | 
| 
      
 137 
     | 
    
         
            +
            It is private to the Scout Monitoring team; if you want to submit a feature feel free to
         
     | 
| 
      
 138 
     | 
    
         
            +
            send us some Markdown and we will get it added.
         
     | 
| 
         @@ -1,6 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            Metadata-Version: 2.1
         
     | 
| 
       2 
2 
     | 
    
         
             
            Name: scout_apm
         
     | 
| 
       3 
     | 
    
         
            -
            Version: 3. 
     | 
| 
      
 3 
     | 
    
         
            +
            Version: 3.3.0
         
     | 
| 
       4 
4 
     | 
    
         
             
            Summary: Scout Application Performance Monitoring Agent
         
     | 
| 
       5 
5 
     | 
    
         
             
            Home-page: https://github.com/scoutapp/scout_apm_python
         
     | 
| 
       6 
6 
     | 
    
         
             
            Author: Scout
         
     | 
| 
         @@ -70,7 +70,7 @@ To use Scout, you'll need to 
     | 
|
| 
       70 
70 
     | 
    
         | 
| 
       71 
71 
     | 
    
         
             
            For full installation instructions, including information on configuring Scout
         
     | 
| 
       72 
72 
     | 
    
         
             
            via environment variables and troubleshooting, see our
         
     | 
| 
       73 
     | 
    
         
            -
            [Python docs](https:// 
     | 
| 
      
 73 
     | 
    
         
            +
            [Python docs](https://scoutapm.com/docs/python).
         
     | 
| 
       74 
74 
     | 
    
         | 
| 
       75 
75 
     | 
    
         
             
            ## Support
         
     | 
| 
       76 
76 
     | 
    
         | 
| 
         @@ -35,7 +35,7 @@ To use Scout, you'll need to 
     | 
|
| 
       35 
35 
     | 
    
         | 
| 
       36 
36 
     | 
    
         
             
            For full installation instructions, including information on configuring Scout
         
     | 
| 
       37 
37 
     | 
    
         
             
            via environment variables and troubleshooting, see our
         
     | 
| 
       38 
     | 
    
         
            -
            [Python docs](https:// 
     | 
| 
      
 38 
     | 
    
         
            +
            [Python docs](https://scoutapm.com/docs/python).
         
     | 
| 
       39 
39 
     | 
    
         | 
| 
       40 
40 
     | 
    
         
             
            ## Support
         
     | 
| 
       41 
41 
     | 
    
         | 
| 
         @@ -33,7 +33,7 @@ else: 
     | 
|
| 
       33 
33 
     | 
    
         | 
| 
       34 
34 
     | 
    
         
             
            setup(
         
     | 
| 
       35 
35 
     | 
    
         
             
                name="scout_apm",
         
     | 
| 
       36 
     | 
    
         
            -
                version="3. 
     | 
| 
      
 36 
     | 
    
         
            +
                version="3.3.0",
         
     | 
| 
       37 
37 
     | 
    
         
             
                description="Scout Application Performance Monitoring Agent",
         
     | 
| 
       38 
38 
     | 
    
         
             
                long_description=long_description,
         
     | 
| 
       39 
39 
     | 
    
         
             
                long_description_content_type="text/markdown",
         
     | 
| 
         @@ -59,8 +59,8 @@ setup( 
     | 
|
| 
       59 
59 
     | 
    
         
             
                },
         
     | 
| 
       60 
60 
     | 
    
         
             
                install_requires=[
         
     | 
| 
       61 
61 
     | 
    
         
             
                    "asgiref",
         
     | 
| 
       62 
     | 
    
         
            -
                    "psutil>=5 
     | 
| 
       63 
     | 
    
         
            -
                    "urllib3",
         
     | 
| 
      
 62 
     | 
    
         
            +
                    "psutil>=5",
         
     | 
| 
      
 63 
     | 
    
         
            +
                    "urllib3~=2.2.0",
         
     | 
| 
       64 
64 
     | 
    
         
             
                    "certifi",
         
     | 
| 
       65 
65 
     | 
    
         
             
                    "wrapt>=1.10,<2.0",
         
     | 
| 
       66 
66 
     | 
    
         
             
                ],
         
     | 
| 
         @@ -97,6 +97,7 @@ class Transaction(AsyncDecoratorMixin, ContextDecorator): 
     | 
|
| 
       97 
97 
     | 
    
         
             
                    operation = text(kind) + "/" + text(name)
         
     | 
| 
       98 
98 
     | 
    
         | 
| 
       99 
99 
     | 
    
         
             
                    tracked_request = TrackedRequest.instance()
         
     | 
| 
      
 100 
     | 
    
         
            +
                    tracked_request.operation = operation
         
     | 
| 
       100 
101 
     | 
    
         
             
                    tracked_request.is_real_request = True
         
     | 
| 
       101 
102 
     | 
    
         
             
                    span = tracked_request.start_span(
         
     | 
| 
       102 
103 
     | 
    
         
             
                        operation=operation, should_capture_backtrace=False
         
     | 
| 
         @@ -40,6 +40,7 @@ class ScoutMiddleware: 
     | 
|
| 
       40 
40 
     | 
    
         
             
                                endpoint.__module__,
         
     | 
| 
       41 
41 
     | 
    
         
             
                                endpoint.__qualname__,
         
     | 
| 
       42 
42 
     | 
    
         
             
                            )
         
     | 
| 
      
 43 
     | 
    
         
            +
                            tracked_request.operation = controller_span.operation
         
     | 
| 
       43 
44 
     | 
    
         
             
                        else:
         
     | 
| 
       44 
45 
     | 
    
         
             
                            # Mark the request as not real
         
     | 
| 
       45 
46 
     | 
    
         
             
                            tracked_request.is_real_request = False
         
     | 
| 
         @@ -90,11 +91,11 @@ def install_background_instrumentation(): 
     | 
|
| 
       90 
91 
     | 
    
         
             
                    tracked_request = TrackedRequest.instance()
         
     | 
| 
       91 
92 
     | 
    
         
             
                    tracked_request.is_real_request = True
         
     | 
| 
       92 
93 
     | 
    
         | 
| 
       93 
     | 
    
         
            -
                     
     | 
| 
       94 
     | 
    
         
            -
                         
     | 
| 
       95 
     | 
    
         
            -
             
     | 
| 
       96 
     | 
    
         
            -
             
     | 
| 
       97 
     | 
    
         
            -
                    ):
         
     | 
| 
      
 94 
     | 
    
         
            +
                    operation = "Job/{}.{}".format(
         
     | 
| 
      
 95 
     | 
    
         
            +
                        instance.func.__module__, instance.func.__qualname__
         
     | 
| 
      
 96 
     | 
    
         
            +
                    )
         
     | 
| 
      
 97 
     | 
    
         
            +
                    tracked_request.operation = operation
         
     | 
| 
      
 98 
     | 
    
         
            +
                    with tracked_request.span(operation=operation):
         
     | 
| 
       98 
99 
     | 
    
         
             
                        return await wrapped(*args, **kwargs)
         
     | 
| 
       99 
100 
     | 
    
         | 
| 
       100 
101 
     | 
    
         
             
                BackgroundTask.__call__ = wrapped_background_call(BackgroundTask.__call__)
         
     | 
| 
         @@ -71,10 +71,10 @@ def wrap_callback(wrapped, instance, args, kwargs): 
     | 
|
| 
       71 
71 
     | 
    
         
             
                    "x-request-start", ""
         
     | 
| 
       72 
72 
     | 
    
         
             
                )
         
     | 
| 
       73 
73 
     | 
    
         
             
                track_request_queue_time(queue_time, tracked_request)
         
     | 
| 
      
 74 
     | 
    
         
            +
                operation = "Controller{}".format(controller_name)
         
     | 
| 
       74 
75 
     | 
    
         | 
| 
       75 
     | 
    
         
            -
                with tracked_request.span(
         
     | 
| 
       76 
     | 
    
         
            -
                    operation= 
     | 
| 
       77 
     | 
    
         
            -
                ):
         
     | 
| 
      
 76 
     | 
    
         
            +
                with tracked_request.span(operation=operation):
         
     | 
| 
      
 77 
     | 
    
         
            +
                    tracked_request.operation = operation
         
     | 
| 
       78 
78 
     | 
    
         
             
                    try:
         
     | 
| 
       79 
79 
     | 
    
         
             
                        value = wrapped(*args, **kwargs)
         
     | 
| 
       80 
80 
     | 
    
         
             
                    except Exception:
         
     | 
| 
         @@ -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):
         
     | 
| 
         @@ -54,7 +56,9 @@ def task_prerun_callback(task=None, **kwargs): 
     | 
|
| 
       54 
56 
     | 
    
         
             
                    tracked_request.tag("routing_key", delivery_info.get("routing_key", "unknown"))
         
     | 
| 
       55 
57 
     | 
    
         
             
                    tracked_request.tag("queue", delivery_info.get("queue", "unknown"))
         
     | 
| 
       56 
58 
     | 
    
         | 
| 
       57 
     | 
    
         
            -
                 
     | 
| 
      
 59 
     | 
    
         
            +
                operation = "Job/" + task.name
         
     | 
| 
      
 60 
     | 
    
         
            +
                tracked_request.start_span(operation=operation)
         
     | 
| 
      
 61 
     | 
    
         
            +
                tracked_request.operation = operation
         
     | 
| 
       58 
62 
     | 
    
         | 
| 
       59 
63 
     | 
    
         | 
| 
       60 
64 
     | 
    
         
             
            def task_postrun_callback(task=None, **kwargs):
         
     | 
| 
         @@ -1,5 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # coding=utf-8
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
      
 3 
     | 
    
         
            +
            import datetime as dt
         
     | 
| 
       3 
4 
     | 
    
         
             
            import logging
         
     | 
| 
       4 
5 
     | 
    
         
             
            import re
         
     | 
| 
       5 
6 
     | 
    
         | 
| 
         @@ -10,6 +11,18 @@ logger = logging.getLogger(__name__) 
     | 
|
| 
       10 
11 
     | 
    
         
             
            key_regex = re.compile(r"^[a-zA-Z0-9]{20}$")
         
     | 
| 
       11 
12 
     | 
    
         | 
| 
       12 
13 
     | 
    
         | 
| 
      
 14 
     | 
    
         
            +
            def format_dt_for_core_agent(event_time: dt.datetime) -> str:
         
     | 
| 
      
 15 
     | 
    
         
            +
                """
         
     | 
| 
      
 16 
     | 
    
         
            +
                Returns expected format for Core Agent compatibility.
         
     | 
| 
      
 17 
     | 
    
         
            +
                Coerce any tz-aware datetime to UTC just in case.
         
     | 
| 
      
 18 
     | 
    
         
            +
                """
         
     | 
| 
      
 19 
     | 
    
         
            +
                # if we somehow got a naive datetime, convert it to UTC
         
     | 
| 
      
 20 
     | 
    
         
            +
                if event_time.tzinfo is None:
         
     | 
| 
      
 21 
     | 
    
         
            +
                    logger.warning("Naive datetime passed to format_dt_for_core_agent")
         
     | 
| 
      
 22 
     | 
    
         
            +
                    event_time = event_time.astimezone(dt.timezone.utc)
         
     | 
| 
      
 23 
     | 
    
         
            +
                return event_time.isoformat()
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
       13 
26 
     | 
    
         
             
            class Register(object):
         
     | 
| 
       14 
27 
     | 
    
         
             
                __slots__ = ("app", "key", "hostname")
         
     | 
| 
       15 
28 
     | 
    
         | 
| 
         @@ -49,7 +62,7 @@ class StartSpan(object): 
     | 
|
| 
       49 
62 
     | 
    
         
             
                def message(self):
         
     | 
| 
       50 
63 
     | 
    
         
             
                    return {
         
     | 
| 
       51 
64 
     | 
    
         
             
                        "StartSpan": {
         
     | 
| 
       52 
     | 
    
         
            -
                            "timestamp": self.timestamp 
     | 
| 
      
 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,
         
     | 
| 
         @@ -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 
     | 
    
         
             
            }
         
     | 
| 
         @@ -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": "",
         
     | 
| 
         @@ -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 
     | 
    
         
            +
                    )
         
     | 
| 
         @@ -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",
         
     | 
| 
         @@ -35,6 +45,7 @@ class TrackedRequest(object): 
     | 
|
| 
       35 
45 
     | 
    
         
             
                    "n_plus_one_tracker",
         
     | 
| 
       36 
46 
     | 
    
         
             
                    "hit_max",
         
     | 
| 
       37 
47 
     | 
    
         
             
                    "sent",
         
     | 
| 
      
 48 
     | 
    
         
            +
                    "operation",
         
     | 
| 
       38 
49 
     | 
    
         
             
                )
         
     | 
| 
       39 
50 
     | 
    
         | 
| 
       40 
51 
     | 
    
         
             
                # Stop adding new spans at this point, to avoid exhausting memory
         
     | 
| 
         @@ -48,7 +59,7 @@ class TrackedRequest(object): 
     | 
|
| 
       48 
59 
     | 
    
         | 
| 
       49 
60 
     | 
    
         
             
                def __init__(self):
         
     | 
| 
       50 
61 
     | 
    
         
             
                    self.request_id = "req-" + str(uuid4())
         
     | 
| 
       51 
     | 
    
         
            -
                    self.start_time = dt.datetime. 
     | 
| 
      
 62 
     | 
    
         
            +
                    self.start_time = dt.datetime.now(dt.timezone.utc)
         
     | 
| 
       52 
63 
     | 
    
         
             
                    self.end_time = None
         
     | 
| 
       53 
64 
     | 
    
         
             
                    self.active_spans = []
         
     | 
| 
       54 
65 
     | 
    
         
             
                    self.complete_spans = []
         
     | 
| 
         @@ -58,6 +69,7 @@ class TrackedRequest(object): 
     | 
|
| 
       58 
69 
     | 
    
         
             
                    self.n_plus_one_tracker = NPlusOneTracker()
         
     | 
| 
       59 
70 
     | 
    
         
             
                    self.hit_max = False
         
     | 
| 
       60 
71 
     | 
    
         
             
                    self.sent = False
         
     | 
| 
      
 72 
     | 
    
         
            +
                    self.operation = None
         
     | 
| 
       61 
73 
     | 
    
         
             
                    logger.debug("Starting request: %s", self.request_id)
         
     | 
| 
       62 
74 
     | 
    
         | 
| 
       63 
75 
     | 
    
         
             
                def __repr__(self):
         
     | 
| 
         @@ -145,11 +157,13 @@ class TrackedRequest(object): 
     | 
|
| 
       145 
157 
     | 
    
         | 
| 
       146 
158 
     | 
    
         
             
                    logger.debug("Stopping request: %s", self.request_id)
         
     | 
| 
       147 
159 
     | 
    
         
             
                    if self.end_time is None:
         
     | 
| 
       148 
     | 
    
         
            -
                        self.end_time = dt.datetime. 
     | 
| 
      
 160 
     | 
    
         
            +
                        self.end_time = dt.datetime.now(dt.timezone.utc)
         
     | 
| 
       149 
161 
     | 
    
         | 
| 
       150 
162 
     | 
    
         
             
                    if self.is_real_request:
         
     | 
| 
       151 
     | 
    
         
            -
                        self. 
     | 
| 
       152 
     | 
    
         
            -
             
     | 
| 
      
 163 
     | 
    
         
            +
                        if not self.sent and self.get_sampler().should_sample(
         
     | 
| 
      
 164 
     | 
    
         
            +
                            self.operation, self.is_ignored()
         
     | 
| 
      
 165 
     | 
    
         
            +
                        ):
         
     | 
| 
      
 166 
     | 
    
         
            +
                            self.tag("mem_delta", self._get_mem_delta())
         
     | 
| 
       153 
167 
     | 
    
         
             
                            self.sent = True
         
     | 
| 
       154 
168 
     | 
    
         
             
                            batch_command = BatchCommand.from_tracked_request(self)
         
     | 
| 
       155 
169 
     | 
    
         
             
                            if scout_config.value("log_payload_content"):
         
     | 
| 
         @@ -217,7 +231,7 @@ class Span(object): 
     | 
|
| 
       217 
231 
     | 
    
         
             
                    should_capture_backtrace=True,
         
     | 
| 
       218 
232 
     | 
    
         
             
                ):
         
     | 
| 
       219 
233 
     | 
    
         
             
                    self.span_id = "span-" + str(uuid4())
         
     | 
| 
       220 
     | 
    
         
            -
                    self.start_time = dt.datetime. 
     | 
| 
      
 234 
     | 
    
         
            +
                    self.start_time = dt.datetime.now(dt.timezone.utc)
         
     | 
| 
       221 
235 
     | 
    
         
             
                    self.end_time = None
         
     | 
| 
       222 
236 
     | 
    
         
             
                    self.request_id = request_id
         
     | 
| 
       223 
237 
     | 
    
         
             
                    self.operation = operation
         
     | 
| 
         @@ -236,7 +250,7 @@ class Span(object): 
     | 
|
| 
       236 
250 
     | 
    
         
             
                    )
         
     | 
| 
       237 
251 
     | 
    
         | 
| 
       238 
252 
     | 
    
         
             
                def stop(self):
         
     | 
| 
       239 
     | 
    
         
            -
                    self.end_time = dt.datetime. 
     | 
| 
      
 253 
     | 
    
         
            +
                    self.end_time = dt.datetime.now(dt.timezone.utc)
         
     | 
| 
       240 
254 
     | 
    
         
             
                    self.end_objtrace_counts = objtrace.get_counts()
         
     | 
| 
       241 
255 
     | 
    
         | 
| 
       242 
256 
     | 
    
         
             
                def tag(self, key, value):
         
     | 
| 
         @@ -252,7 +266,9 @@ class Span(object): 
     | 
|
| 
       252 
266 
     | 
    
         
             
                        return (self.end_time - self.start_time).total_seconds()
         
     | 
| 
       253 
267 
     | 
    
         
             
                    else:
         
     | 
| 
       254 
268 
     | 
    
         
             
                        # Current, running duration
         
     | 
| 
       255 
     | 
    
         
            -
                        return ( 
     | 
| 
      
 269 
     | 
    
         
            +
                        return (
         
     | 
| 
      
 270 
     | 
    
         
            +
                            dt.datetime.now(tz=dt.timezone.utc) - self.start_time
         
     | 
| 
      
 271 
     | 
    
         
            +
                        ).total_seconds()
         
     | 
| 
       256 
272 
     | 
    
         | 
| 
       257 
273 
     | 
    
         
             
                # Add any interesting annotations to the span. Assumes that we are in the
         
     | 
| 
       258 
274 
     | 
    
         
             
                # process of stopping this span.
         
     | 
| 
         @@ -122,6 +122,7 @@ class ViewTimingMiddleware(object): 
     | 
|
| 
       122 
122 
     | 
    
         
             
                    span = tracked_request.current_span()
         
     | 
| 
       123 
123 
     | 
    
         
             
                    if span is not None:
         
     | 
| 
       124 
124 
     | 
    
         
             
                        span.operation = get_controller_name(request)
         
     | 
| 
      
 125 
     | 
    
         
            +
                        tracked_request.operation = span.operation
         
     | 
| 
       125 
126 
     | 
    
         | 
| 
       126 
127 
     | 
    
         
             
                def process_exception(self, request, exception):
         
     | 
| 
       127 
128 
     | 
    
         
             
                    """
         
     | 
| 
         @@ -17,7 +17,9 @@ class ScoutMiddleware(dramatiq.Middleware): 
     | 
|
| 
       17 
17 
     | 
    
         
             
                    tracked_request = TrackedRequest.instance()
         
     | 
| 
       18 
18 
     | 
    
         
             
                    tracked_request.tag("queue", message.queue_name)
         
     | 
| 
       19 
19 
     | 
    
         
             
                    tracked_request.tag("message_id", message.message_id)
         
     | 
| 
       20 
     | 
    
         
            -
                     
     | 
| 
      
 20 
     | 
    
         
            +
                    operation = "Job/" + message.actor_name
         
     | 
| 
      
 21 
     | 
    
         
            +
                    tracked_request.start_span(operation=operation)
         
     | 
| 
      
 22 
     | 
    
         
            +
                    tracked_request.operation = operation
         
     | 
| 
       21 
23 
     | 
    
         | 
| 
       22 
24 
     | 
    
         
             
                def after_process_message(self, broker, message, result=None, exception=None):
         
     | 
| 
       23 
25 
     | 
    
         
             
                    if self._do_nothing:
         
     | 
| 
         @@ -106,6 +106,7 @@ class ScoutMiddleware(object): 
     | 
|
| 
       106 
106 
     | 
    
         
             
                    span = tracked_request.start_span(
         
     | 
| 
       107 
107 
     | 
    
         
             
                        operation=operation, should_capture_backtrace=False
         
     | 
| 
       108 
108 
     | 
    
         
             
                    )
         
     | 
| 
      
 109 
     | 
    
         
            +
                    tracked_request.operation = operation
         
     | 
| 
       109 
110 
     | 
    
         
             
                    req.context.scout_resource_span = span
         
     | 
| 
       110 
111 
     | 
    
         | 
| 
       111 
112 
     | 
    
         
             
                def _name_operation(self, req, responder, resource):
         
     | 
| 
         @@ -47,6 +47,7 @@ class ScoutApm(object): 
     | 
|
| 
       47 
47 
     | 
    
         | 
| 
       48 
48 
     | 
    
         
             
                    tracked_request = TrackedRequest.instance()
         
     | 
| 
       49 
49 
     | 
    
         
             
                    tracked_request.is_real_request = True
         
     | 
| 
      
 50 
     | 
    
         
            +
                    tracked_request.operation = operation
         
     | 
| 
       50 
51 
     | 
    
         
             
                    request._scout_tracked_request = tracked_request
         
     | 
| 
       51 
52 
     | 
    
         | 
| 
       52 
53 
     | 
    
         
             
                    werkzeug_track_request_data(request, tracked_request)
         
     | 
| 
         @@ -30,6 +30,7 @@ def scout_on_pre_execute(task): 
     | 
|
| 
       30 
30 
     | 
    
         | 
| 
       31 
31 
     | 
    
         
             
                operation = "Job/{}.{}".format(task.__module__, task.__class__.__name__)
         
     | 
| 
       32 
32 
     | 
    
         
             
                tracked_request.start_span(operation=operation)
         
     | 
| 
      
 33 
     | 
    
         
            +
                tracked_request.operation = operation
         
     | 
| 
       33 
34 
     | 
    
         | 
| 
       34 
35 
     | 
    
         | 
| 
       35 
36 
     | 
    
         
             
            def scout_on_post_execute(task, task_value, exception):
         
     | 
| 
         @@ -1,6 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # coding=utf-8
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            import datetime as dt
         
     | 
| 
      
 4 
     | 
    
         
            +
            import logging
         
     | 
| 
       4 
5 
     | 
    
         | 
| 
       5 
6 
     | 
    
         
             
            import wrapt
         
     | 
| 
       6 
7 
     | 
    
         
             
            from rq import SimpleWorker as RqSimpleWorker
         
     | 
| 
         @@ -14,6 +15,8 @@ from scout_apm.core.tracked_request import TrackedRequest 
     | 
|
| 
       14 
15 
     | 
    
         
             
            install_attempted = False
         
     | 
| 
       15 
16 
     | 
    
         
             
            installed = None
         
     | 
| 
       16 
17 
     | 
    
         | 
| 
      
 18 
     | 
    
         
            +
            logger = logging.getLogger(__name__)
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
       17 
20 
     | 
    
         | 
| 
       18 
21 
     | 
    
         
             
            def ensure_scout_installed():
         
     | 
| 
       19 
22 
     | 
    
         
             
                global install_attempted, installed
         
     | 
| 
         @@ -65,10 +68,16 @@ def wrap_perform(wrapped, instance, args, kwargs): 
     | 
|
| 
       65 
68 
     | 
    
         
             
                tracked_request.is_real_request = True
         
     | 
| 
       66 
69 
     | 
    
         
             
                tracked_request.tag("task_id", instance.get_id())
         
     | 
| 
       67 
70 
     | 
    
         
             
                tracked_request.tag("queue", instance.origin)
         
     | 
| 
       68 
     | 
    
         
            -
                 
     | 
| 
      
 71 
     | 
    
         
            +
                # rq strips tzinfo from enqueued_at during serde in at least some cases
         
     | 
| 
      
 72 
     | 
    
         
            +
                # internally everything uses UTC naive datetimes, so we operate on that
         
     | 
| 
      
 73 
     | 
    
         
            +
                # assumption here.
         
     | 
| 
      
 74 
     | 
    
         
            +
                if instance.enqueued_at.tzinfo is None:
         
     | 
| 
      
 75 
     | 
    
         
            +
                    queued_at = instance.enqueued_at.replace(tzinfo=dt.timezone.utc)
         
     | 
| 
      
 76 
     | 
    
         
            +
                queue_time = (dt.datetime.now(dt.timezone.utc) - queued_at).total_seconds()
         
     | 
| 
       69 
77 
     | 
    
         
             
                tracked_request.tag("queue_time", queue_time)
         
     | 
| 
       70 
     | 
    
         
            -
             
     | 
| 
       71 
     | 
    
         
            -
                 
     | 
| 
      
 78 
     | 
    
         
            +
                operation = "Job/{}".format(instance.func_name)
         
     | 
| 
      
 79 
     | 
    
         
            +
                tracked_request.operation = operation
         
     | 
| 
      
 80 
     | 
    
         
            +
                with tracked_request.span(operation=operation):
         
     | 
| 
       72 
81 
     | 
    
         
             
                    try:
         
     | 
| 
       73 
82 
     | 
    
         
             
                        return wrapped(*args, **kwargs)
         
     | 
| 
       74 
83 
     | 
    
         
             
                    except Exception:
         
     | 
| 
         @@ -1,6 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            Metadata-Version: 2.1
         
     | 
| 
       2 
2 
     | 
    
         
             
            Name: scout-apm
         
     | 
| 
       3 
     | 
    
         
            -
            Version: 3. 
     | 
| 
      
 3 
     | 
    
         
            +
            Version: 3.3.0
         
     | 
| 
       4 
4 
     | 
    
         
             
            Summary: Scout Application Performance Monitoring Agent
         
     | 
| 
       5 
5 
     | 
    
         
             
            Home-page: https://github.com/scoutapp/scout_apm_python
         
     | 
| 
       6 
6 
     | 
    
         
             
            Author: Scout
         
     | 
| 
         @@ -70,7 +70,7 @@ To use Scout, you'll need to 
     | 
|
| 
       70 
70 
     | 
    
         | 
| 
       71 
71 
     | 
    
         
             
            For full installation instructions, including information on configuring Scout
         
     | 
| 
       72 
72 
     | 
    
         
             
            via environment variables and troubleshooting, see our
         
     | 
| 
       73 
     | 
    
         
            -
            [Python docs](https:// 
     | 
| 
      
 73 
     | 
    
         
            +
            [Python docs](https://scoutapm.com/docs/python).
         
     | 
| 
       74 
74 
     | 
    
         | 
| 
       75 
75 
     | 
    
         
             
            ## Support
         
     | 
| 
       76 
76 
     | 
    
         | 
| 
         @@ -41,6 +41,7 @@ src/scout_apm/core/n_plus_one_tracker.py 
     | 
|
| 
       41 
41 
     | 
    
         
             
            src/scout_apm/core/objtrace.py
         
     | 
| 
       42 
42 
     | 
    
         
             
            src/scout_apm/core/platform_detection.py
         
     | 
| 
       43 
43 
     | 
    
         
             
            src/scout_apm/core/queue_time.py
         
     | 
| 
      
 44 
     | 
    
         
            +
            src/scout_apm/core/sampler.py
         
     | 
| 
       44 
45 
     | 
    
         
             
            src/scout_apm/core/stacktracer.py
         
     | 
| 
       45 
46 
     | 
    
         
             
            src/scout_apm/core/threading.py
         
     | 
| 
       46 
47 
     | 
    
         
             
            src/scout_apm/core/tracked_request.py
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     |