Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement sampling #807

Merged
merged 27 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
942c0c6
add new sampling configuration keys and converters
quinnmil Jan 3, 2025
fe829b1
add types to all config methods
quinnmil Jan 3, 2025
60e9e25
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 3, 2025
b248ba5
use 0-100 ints instead of floats
quinnmil Jan 3, 2025
16eaff7
add sampler module
quinnmil Jan 3, 2025
7229c75
add sampler tests
quinnmil Jan 3, 2025
e501b7b
improve sampler tests
quinnmil Jan 5, 2025
0574fd9
fix always sample endpoint test
quinnmil Jan 6, 2025
1915274
remove unnecessary logging
quinnmil Jan 6, 2025
41867ef
remove debug logging
quinnmil Jan 6, 2025
bb39635
short circuit sampling logic if no sampling configured
quinnmil Jan 7, 2025
d9cf86c
simplify pattern match
quinnmil Jan 9, 2025
5998f80
rename config option to X_sample_rate
quinnmil Jan 10, 2025
bf3e393
check endpoint/job sample rate in any_sampling
quinnmil Jan 13, 2025
0090e94
add test assertion
quinnmil Jan 13, 2025
82b626a
add test assertion
quinnmil Jan 13, 2025
b552529
refactor sampler to better handler legacy ignore and prioritization
quinnmil Jan 14, 2025
a235170
de-couple from TrackedRequest
quinnmil Jan 14, 2025
fb4744d
simplify sampler logic to rely on configuration
quinnmil Jan 14, 2025
d78d973
flatten control flow
quinnmil Jan 14, 2025
215f0b2
use prefix to check ignores
quinnmil Jan 14, 2025
7e8ceae
add sampler to tracked_request class
quinnmil Jan 6, 2025
c9e198e
add 3.3.0 changelog
quinnmil Jan 6, 2025
b304e84
use singleton pattern for sampler
quinnmil Jan 7, 2025
6ad4cad
add sampler tests to test_tracked_request
quinnmil Jan 7, 2025
2876f80
update changelog url
quinnmil Jan 7, 2025
c81950a
pass ignored boolean to should_sample
quinnmil Jan 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
- Change to tz-aware dates internally (Issue #799)
- psutil dependency un-pin (#790)

## [3.3.0] 2025-01-07
### Added
- Added support for down-sampling via Scout configuration.
- Sample rates can be set globally or for specific jobs/endpoints
- Check out our [documentation](https://scoutapm.com/docs/python/configuration#sampling) for more information and example usage.

## [3.2.0] 2024-09-12
### Added
- "Operation" attribute added to TrackedRequest class to better support development of [scout_apm_python_logging](https://github.com/scoutapp/scout_apm_python_logging)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

setup(
name="scout_apm",
version="3.2.1",
version="3.3.0",
description="Scout Application Performance Monitoring Agent",
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down
16 changes: 14 additions & 2 deletions src/scout_apm/core/tracked_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from scout_apm.core.agent.socket import CoreAgentSocketThread
from scout_apm.core.config import scout_config
from scout_apm.core.n_plus_one_tracker import NPlusOneTracker
from scout_apm.core.sampler import Sampler
from scout_apm.core.samplers.memory import get_rss_in_mb
from scout_apm.core.samplers.thread import SamplersThread

Expand All @@ -23,7 +24,16 @@ class TrackedRequest(object):
their keyname
"""

_sampler = None

@classmethod
def get_sampler(cls):
if cls._sampler is None:
cls._sampler = Sampler(scout_config)
return cls._sampler

__slots__ = (
"sampler",
"request_id",
"start_time",
"end_time",
Expand Down Expand Up @@ -150,8 +160,10 @@ def finish(self):
self.end_time = dt.datetime.now(dt.timezone.utc)

if self.is_real_request:
self.tag("mem_delta", self._get_mem_delta())
if not self.is_ignored() and not self.sent:
if not self.sent and self.get_sampler().should_sample(
self.operation, self.is_ignored()
):
self.tag("mem_delta", self._get_mem_delta())
self.sent = True
batch_command = BatchCommand.from_tracked_request(self)
if scout_config.value("log_payload_content"):
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/core/test_tracked_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
)


@pytest.fixture(autouse=True)
def clear_sampler():
"""Reset the sampler before each test"""
TrackedRequest._sampler = None


@pytest.fixture
def reset_config():
"""
Expand All @@ -26,6 +32,8 @@ def reset_config():
finally:
# Reset Scout configuration.
scout_config.reset_all()
# Clear the sampler
TrackedRequest._sampler = None


def test_tracked_request_repr(tracked_request):
Expand Down Expand Up @@ -358,3 +366,46 @@ def test_request_only_sent_once(tracked_request, caplog):
len([log_tuple for log_tuple in caplog.record_tuples if log_tuple == info_log])
== 2
)


def test_sampler_behavior(tracked_request):
"""Test that sampler is only created when first needed and shared across requests"""
assert TrackedRequest._sampler is None
sampler1 = TrackedRequest.get_sampler()

assert TrackedRequest._sampler is not None

# Should get the same sampler instance
sampler2 = TrackedRequest.get_sampler()
assert sampler1 is sampler2

# Test that all TrackedRequests share the same sampler
request1 = TrackedRequest()
request2 = TrackedRequest()

sampler1 = request1.get_sampler()
sampler2 = request2.get_sampler()

assert sampler1 is sampler2


@pytest.mark.parametrize(
"operation,is_real,expected_calls",
[
("Controller/test", True, 1), # Should check sampling
("Controller/test", False, 0), # Shouldn't check sampling if not real
],
)
def test_finish_sampling_behavior(tracked_request, operation, is_real, expected_calls):
"""Test that sampling only occurs under the right conditions"""
mock_sampler = mock.Mock()
mock_sampler.should_sample.return_value = True
TrackedRequest._sampler = mock_sampler

tracked_request.operation = operation
tracked_request.is_real_request = is_real
tracked_request.sent = False

tracked_request.finish()

assert mock_sampler.should_sample.call_count == expected_calls
Loading