Repository navigation
feat(celery): allow custom monitor config for beat tasks #7839
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Celery Beat auto-instrumentation derives the cron monitor config from the schedule only, so max_runtime, checkin_margin, failure_issue_threshold, recovery_threshold and owner cannot be set. Tasks whose runtime can exceed Sentry's default 30-minute max_runtime are then reported as timed-out cron failures even when they complete successfully. Add a `beat_task_monitor_config` option to CeleryIntegration that maps beat task names to partial monitor configs, merged over the derived config. Refs #7838
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -385,6 +385,130 @@ def test_get_monitor_config_timezone_in_celery_schedule(): | |
| assert monitor_config["timezone"] == str(panama_tz) | ||
|
|
||
|
|
||
| def test_get_monitor_config_with_overrides(): | ||
| app = MagicMock() | ||
| app.timezone = "Europe/Vienna" | ||
|
|
||
| celery_schedule = crontab(day_of_month="3", hour="12", minute="*/10") | ||
|
|
||
| monitor_config = _get_monitor_config( | ||
| celery_schedule, | ||
| app, | ||
| "foo", | ||
| {"max_runtime": 120, "checkin_margin": 5, "owner": "team:6"}, | ||
| ) | ||
|
|
||
| assert monitor_config == { | ||
| "schedule": { | ||
| "type": "crontab", | ||
| "value": "*/10 12 3 * *", | ||
| }, | ||
| "timezone": "UTC", | ||
| "max_runtime": 120, | ||
| "checkin_margin": 5, | ||
| "owner": "team:6", | ||
| } | ||
|
|
||
|
|
||
| def test_get_monitor_config_overrides_can_replace_schedule(): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't support overriding the schedule (or its type) via this new property as there's validation that we do within the SDK to ensure the provided values are valid. Instead, let's do the following: 1️⃣ Introduce a new type in 2️⃣ Validate the overrides in the In _ALLOWED_MONITOR_CONFIG_OVERRIDE_KEYS = frozenset(
{
"checkin_margin",
"max_runtime",
"failure_issue_threshold",
"recovery_threshold",
"owner",
}
)
def _validate_beat_task_monitor_config(
beat_task_monitor_config: "Dict[str, MonitorConfigOverrides]",
) -> None:
for task_name, overrides in beat_task_monitor_config.items():
invalid = set(overrides) - _ALLOWED_MONITOR_CONFIG_OVERRIDE_KEYS
if invalid:
raise ValueError(
f"Unsupported keys in beat_task_monitor_config for '{task_name}': "
f"{sorted(invalid)}. Allowed keys: "
f"{sorted(_ALLOWED_MONITOR_CONFIG_OVERRIDE_KEYS)}"
)and then invoke |
||
| app = MagicMock() | ||
| app.timezone = "Europe/Vienna" | ||
|
|
||
| celery_schedule = crontab(day_of_month="3", hour="12", minute="*/10") | ||
| override_schedule = {"type": "crontab", "value": "0 5 * * *"} | ||
|
|
||
| monitor_config = _get_monitor_config( | ||
| celery_schedule, app, "foo", {"schedule": override_schedule} | ||
| ) | ||
|
|
||
| assert monitor_config["schedule"] == override_schedule | ||
|
|
||
|
|
||
| def test_beat_task_monitor_config_option(): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove this test |
||
| """``beat_task_monitor_config`` is merged into the derived monitor config.""" | ||
| fake_apply_entry = MagicMock() | ||
|
|
||
| fake_scheduler = MagicMock() | ||
| fake_scheduler.apply_entry = fake_apply_entry | ||
|
|
||
| fake_integration = MagicMock() | ||
| fake_integration.monitor_beat_tasks = True | ||
| fake_integration.exclude_beat_tasks = None | ||
| fake_integration.beat_task_monitor_config = { | ||
| "some_task_name": {"max_runtime": 120, "checkin_margin": 10} | ||
| } | ||
|
|
||
| fake_client = MagicMock() | ||
| fake_client.get_integration.return_value = fake_integration | ||
|
|
||
| fake_schedule_entry = MagicMock() | ||
| fake_schedule_entry.name = "some_task_name" | ||
| fake_schedule_entry.schedule = crontab(day_of_month="3", hour="12", minute="*/10") | ||
| fake_schedule_entry.options = {} | ||
|
|
||
| with mock.patch( | ||
| "sentry_sdk.integrations.celery.beat.Scheduler", fake_scheduler | ||
| ) as Scheduler: # noqa: N806 | ||
| with mock.patch( | ||
| "sentry_sdk.integrations.celery.sentry_sdk.get_client", | ||
| return_value=fake_client, | ||
| ): | ||
| with mock.patch( | ||
| "sentry_sdk.integrations.celery.beat.capture_checkin", | ||
| return_value="check-in-id", | ||
| ) as mock_capture_checkin: | ||
| # Mimic CeleryIntegration patching of Scheduler.apply_entry() | ||
| _patch_beat_apply_entry() | ||
| # Mimic Celery Beat calling a task from the Beat schedule | ||
| Scheduler.apply_entry(fake_scheduler, fake_schedule_entry) | ||
|
|
||
| assert fake_apply_entry.call_count == 1 | ||
| monitor_config = mock_capture_checkin.call_args.kwargs["monitor_config"] | ||
| assert monitor_config["schedule"] == { | ||
| "type": "crontab", | ||
| "value": "*/10 12 3 * *", | ||
| } | ||
| assert monitor_config["max_runtime"] == 120 | ||
| assert monitor_config["checkin_margin"] == 10 | ||
|
|
||
|
|
||
| def test_beat_task_monitor_config_option_only_applies_to_matching_task(): | ||
| fake_apply_entry = MagicMock() | ||
|
|
||
| fake_scheduler = MagicMock() | ||
| fake_scheduler.apply_entry = fake_apply_entry | ||
|
Comment on lines
+478
to
+479
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Within the SDK, we're trying to move towards testing functionality via the public APIs rather than the private methods and use mocks as sparingly as we can. Instead of this approach, can we look to instead do something similar to the |
||
|
|
||
| fake_integration = MagicMock() | ||
| fake_integration.monitor_beat_tasks = True | ||
| fake_integration.exclude_beat_tasks = None | ||
| fake_integration.beat_task_monitor_config = {"some_task_name": {"max_runtime": 120}} | ||
|
|
||
| fake_client = MagicMock() | ||
| fake_client.get_integration.return_value = fake_integration | ||
|
|
||
| fake_schedule_entry = MagicMock() | ||
| fake_schedule_entry.name = "another_task_name" | ||
| fake_schedule_entry.schedule = crontab(day_of_month="3", hour="12", minute="*/10") | ||
| fake_schedule_entry.options = {} | ||
|
|
||
| with mock.patch( | ||
| "sentry_sdk.integrations.celery.beat.Scheduler", fake_scheduler | ||
| ) as Scheduler: # noqa: N806 | ||
| with mock.patch( | ||
| "sentry_sdk.integrations.celery.sentry_sdk.get_client", | ||
| return_value=fake_client, | ||
| ): | ||
| with mock.patch( | ||
| "sentry_sdk.integrations.celery.beat.capture_checkin", | ||
| return_value="check-in-id", | ||
| ) as mock_capture_checkin: | ||
| _patch_beat_apply_entry() | ||
| Scheduler.apply_entry(fake_scheduler, fake_schedule_entry) | ||
|
|
||
| monitor_config = mock_capture_checkin.call_args.kwargs["monitor_config"] | ||
| assert "max_runtime" not in monitor_config | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "task_name,exclude_beat_tasks,task_in_excluded_beat_tasks", | ||
| [ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we need a
monitor_nameprovided as part of the configuration for any overrides to work, we can set the default value to an empty dictionary instead ofNonehere.This would allow us to update the
(integration.beat_task_monitor_config or {}).get(monitor_name)conditional to either:or
depending on your preference.