Skip to content

Commit 1d9effe

Browse files
authored
Celery beat exclude option (#2130)
1 parent 443b7b9 commit 1d9effe

File tree

5 files changed

+114
-12
lines changed

5 files changed

+114
-12
lines changed

sentry_sdk/integrations/celery.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
capture_internal_exceptions,
1717
event_from_exception,
1818
logger,
19+
match_regex_list,
1920
)
2021

2122
if TYPE_CHECKING:
2223
from typing import Any
2324
from typing import Callable
2425
from typing import Dict
26+
from typing import List
2527
from typing import Optional
2628
from typing import Tuple
2729
from typing import TypeVar
@@ -59,10 +61,16 @@
5961
class CeleryIntegration(Integration):
6062
identifier = "celery"
6163

62-
def __init__(self, propagate_traces=True, monitor_beat_tasks=False):
63-
# type: (bool, bool) -> None
64+
def __init__(
65+
self,
66+
propagate_traces=True,
67+
monitor_beat_tasks=False,
68+
exclude_beat_tasks=None,
69+
):
70+
# type: (bool, bool, Optional[List[str]]) -> None
6471
self.propagate_traces = propagate_traces
6572
self.monitor_beat_tasks = monitor_beat_tasks
73+
self.exclude_beat_tasks = exclude_beat_tasks
6674

6775
if monitor_beat_tasks:
6876
_patch_beat_apply_entry()
@@ -420,9 +428,18 @@ def sentry_apply_entry(*args, **kwargs):
420428
app = scheduler.app
421429

422430
celery_schedule = schedule_entry.schedule
423-
monitor_config = _get_monitor_config(celery_schedule, app)
424431
monitor_name = schedule_entry.name
425432

433+
hub = Hub.current
434+
integration = hub.get_integration(CeleryIntegration)
435+
if integration is None:
436+
return original_apply_entry(*args, **kwargs)
437+
438+
if match_regex_list(monitor_name, integration.exclude_beat_tasks):
439+
return original_apply_entry(*args, **kwargs)
440+
441+
monitor_config = _get_monitor_config(celery_schedule, app)
442+
426443
headers = schedule_entry.options.pop("headers", {})
427444
headers.update(
428445
{

sentry_sdk/tracing_utils.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from sentry_sdk.utils import (
88
capture_internal_exceptions,
99
Dsn,
10+
match_regex_list,
1011
to_string,
1112
)
1213
from sentry_sdk._compat import PY2, iteritems
@@ -334,15 +335,7 @@ def should_propagate_trace(hub, url):
334335
client = hub.client # type: Any
335336
trace_propagation_targets = client.options["trace_propagation_targets"]
336337

337-
if trace_propagation_targets is None:
338-
return False
339-
340-
for target in trace_propagation_targets:
341-
matched = re.search(target, url)
342-
if matched:
343-
return True
344-
345-
return False
338+
return match_regex_list(url, trace_propagation_targets, substring_matching=True)
346339

347340

348341
# Circular imports

sentry_sdk/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,22 @@ def is_valid_sample_rate(rate, source):
13041304
return True
13051305

13061306

1307+
def match_regex_list(item, regex_list=None, substring_matching=False):
1308+
# type: (str, Optional[List[str]], bool) -> bool
1309+
if regex_list is None:
1310+
return False
1311+
1312+
for item_matcher in regex_list:
1313+
if not substring_matching and item_matcher[-1] != "$":
1314+
item_matcher += "$"
1315+
1316+
matched = re.search(item_matcher, item)
1317+
if matched:
1318+
return True
1319+
1320+
return False
1321+
1322+
13071323
if PY37:
13081324

13091325
def nanosecond_time():

tests/integrations/celery/test_celery_beat_crons.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
_get_headers,
99
_get_humanized_interval,
1010
_get_monitor_config,
11+
_patch_beat_apply_entry,
1112
crons_task_success,
1213
crons_task_failure,
1314
crons_task_retry,
@@ -243,3 +244,56 @@ def test_get_monitor_config_default_timezone():
243244
monitor_config = _get_monitor_config(celery_schedule, app)
244245

245246
assert monitor_config["timezone"] == "UTC"
247+
248+
249+
@pytest.mark.parametrize(
250+
"task_name,exclude_beat_tasks,task_in_excluded_beat_tasks",
251+
[
252+
["some_task_name", ["xxx", "some_task.*"], True],
253+
["some_task_name", ["xxx", "some_other_task.*"], False],
254+
],
255+
)
256+
def test_exclude_beat_tasks_option(
257+
task_name, exclude_beat_tasks, task_in_excluded_beat_tasks
258+
):
259+
"""
260+
Test excluding Celery Beat tasks from automatic instrumentation.
261+
"""
262+
fake_apply_entry = mock.MagicMock()
263+
264+
fake_scheduler = mock.MagicMock()
265+
fake_scheduler.apply_entry = fake_apply_entry
266+
267+
fake_integration = mock.MagicMock()
268+
fake_integration.exclude_beat_tasks = exclude_beat_tasks
269+
270+
fake_schedule_entry = mock.MagicMock()
271+
fake_schedule_entry.name = task_name
272+
273+
fake_get_monitor_config = mock.MagicMock()
274+
275+
with mock.patch(
276+
"sentry_sdk.integrations.celery.Scheduler", fake_scheduler
277+
) as Scheduler: # noqa: N806
278+
with mock.patch(
279+
"sentry_sdk.integrations.celery.Hub.current.get_integration",
280+
return_value=fake_integration,
281+
):
282+
with mock.patch(
283+
"sentry_sdk.integrations.celery._get_monitor_config",
284+
fake_get_monitor_config,
285+
) as _get_monitor_config:
286+
# Mimic CeleryIntegration patching of Scheduler.apply_entry()
287+
_patch_beat_apply_entry()
288+
# Mimic Celery Beat calling a task from the Beat schedule
289+
Scheduler.apply_entry(fake_scheduler, fake_schedule_entry)
290+
291+
if task_in_excluded_beat_tasks:
292+
# Only the original Scheduler.apply_entry() is called, _get_monitor_config is NOT called.
293+
fake_apply_entry.assert_called_once()
294+
_get_monitor_config.assert_not_called()
295+
296+
else:
297+
# The original Scheduler.apply_entry() is called, AND _get_monitor_config is called.
298+
fake_apply_entry.assert_called_once()
299+
_get_monitor_config.assert_called_once()

tests/test_utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from sentry_sdk.utils import (
66
is_valid_sample_rate,
77
logger,
8+
match_regex_list,
89
parse_url,
910
sanitize_url,
1011
serialize_frame,
@@ -241,3 +242,24 @@ def test_include_source_context_when_serializing_frame(include_source_context):
241242
assert include_source_context ^ ("pre_context" in result) ^ True
242243
assert include_source_context ^ ("context_line" in result) ^ True
243244
assert include_source_context ^ ("post_context" in result) ^ True
245+
246+
247+
@pytest.mark.parametrize(
248+
"item,regex_list,expected_result",
249+
[
250+
["", [], False],
251+
[None, [], False],
252+
["", None, False],
253+
[None, None, False],
254+
["some-string", [], False],
255+
["some-string", None, False],
256+
["some-string", ["some-string"], True],
257+
["some-string", ["some"], False],
258+
["some-string", ["some$"], False], # same as above
259+
["some-string", ["some.*"], True],
260+
["some-string", ["Some"], False], # we do case sensitive matching
261+
["some-string", [".*string$"], True],
262+
],
263+
)
264+
def test_match_regex_list(item, regex_list, expected_result):
265+
assert match_regex_list(item, regex_list) == expected_result

0 commit comments

Comments
 (0)