Skip to content

Commit d4bbd85

Browse files
authored
Celery Beat auto monitoring (#1967)
Automatically monitor Celery Beat tasks with Sentry Crons. How we do this: - we dynamically create a function that listens to the `celery_beat_init` hook. In the hook we do two things: - 1.) patch existing scheduled tasks (in `sender.scheduler.schedule`): - Each scheduled task is patched to contain information about the Sentry monitor (the monitor slug and config (timezone, schedule, ...) in its headers. - We then stop Celery Beat and replace the scheduled tasks with the new patched scheduled tasks - We restart Celery Beat to enable our patched tasks - 2.) Connect each task to the following hooks to send information about the task to sentry: `task_prerun`, `task_success`, `task_failure`, `task_retry`. (config is sent by the tasks in its headers we set up in 1))
1 parent c4d0384 commit d4bbd85

File tree

11 files changed

+733
-140
lines changed

11 files changed

+733
-140
lines changed

sentry_sdk/crons.py

-123
This file was deleted.

sentry_sdk/crons/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from sentry_sdk.crons.api import capture_checkin # noqa
2+
from sentry_sdk.crons.consts import MonitorStatus # noqa
3+
from sentry_sdk.crons.decorator import monitor # noqa

sentry_sdk/crons/api.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import uuid
2+
3+
from sentry_sdk import Hub
4+
from sentry_sdk._types import TYPE_CHECKING
5+
6+
7+
if TYPE_CHECKING:
8+
from typing import Any, Dict, Optional
9+
10+
11+
def _create_check_in_event(
12+
monitor_slug=None,
13+
check_in_id=None,
14+
status=None,
15+
duration_s=None,
16+
monitor_config=None,
17+
):
18+
# type: (Optional[str], Optional[str], Optional[str], Optional[float], Optional[Dict[str, Any]]) -> Dict[str, Any]
19+
options = Hub.current.client.options if Hub.current.client else {}
20+
check_in_id = check_in_id or uuid.uuid4().hex # type: str
21+
22+
check_in = {
23+
"type": "check_in",
24+
"monitor_slug": monitor_slug,
25+
"monitor_config": monitor_config or {},
26+
"check_in_id": check_in_id,
27+
"status": status,
28+
"duration": duration_s,
29+
"environment": options.get("environment", None),
30+
"release": options.get("release", None),
31+
}
32+
33+
return check_in
34+
35+
36+
def capture_checkin(
37+
monitor_slug=None,
38+
check_in_id=None,
39+
status=None,
40+
duration=None,
41+
monitor_config=None,
42+
):
43+
# type: (Optional[str], Optional[str], Optional[str], Optional[float], Optional[Dict[str, Any]]) -> str
44+
hub = Hub.current
45+
46+
check_in_id = check_in_id or uuid.uuid4().hex
47+
check_in_event = _create_check_in_event(
48+
monitor_slug=monitor_slug,
49+
check_in_id=check_in_id,
50+
status=status,
51+
duration_s=duration,
52+
monitor_config=monitor_config,
53+
)
54+
hub.capture_event(check_in_event)
55+
56+
return check_in_event["check_in_id"]

sentry_sdk/crons/consts.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class MonitorStatus:
2+
IN_PROGRESS = "in_progress"
3+
OK = "ok"
4+
ERROR = "error"

sentry_sdk/crons/decorator.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from functools import wraps
2+
import sys
3+
4+
from sentry_sdk._compat import reraise
5+
from sentry_sdk._types import TYPE_CHECKING
6+
from sentry_sdk.crons import capture_checkin
7+
from sentry_sdk.crons.consts import MonitorStatus
8+
from sentry_sdk.utils import now
9+
10+
11+
if TYPE_CHECKING:
12+
from typing import Any, Callable, Optional
13+
14+
15+
def monitor(monitor_slug=None):
16+
# type: (Optional[str]) -> Callable[..., Any]
17+
"""
18+
Decorator to capture checkin events for a monitor.
19+
20+
Usage:
21+
```
22+
import sentry_sdk
23+
24+
app = Celery()
25+
26+
@app.task
27+
@sentry_sdk.monitor(monitor_slug='my-fancy-slug')
28+
def test(arg):
29+
print(arg)
30+
```
31+
32+
This does not have to be used with Celery, but if you do use it with celery,
33+
put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator.
34+
"""
35+
36+
def decorate(func):
37+
# type: (Callable[..., Any]) -> Callable[..., Any]
38+
if not monitor_slug:
39+
return func
40+
41+
@wraps(func)
42+
def wrapper(*args, **kwargs):
43+
# type: (*Any, **Any) -> Any
44+
start_timestamp = now()
45+
check_in_id = capture_checkin(
46+
monitor_slug=monitor_slug, status=MonitorStatus.IN_PROGRESS
47+
)
48+
49+
try:
50+
result = func(*args, **kwargs)
51+
except Exception:
52+
duration_s = now() - start_timestamp
53+
capture_checkin(
54+
monitor_slug=monitor_slug,
55+
check_in_id=check_in_id,
56+
status=MonitorStatus.ERROR,
57+
duration=duration_s,
58+
)
59+
exc_info = sys.exc_info()
60+
reraise(*exc_info)
61+
62+
duration_s = now() - start_timestamp
63+
capture_checkin(
64+
monitor_slug=monitor_slug,
65+
check_in_id=check_in_id,
66+
status=MonitorStatus.OK,
67+
duration=duration_s,
68+
)
69+
70+
return result
71+
72+
return wrapper
73+
74+
return decorate

0 commit comments

Comments
 (0)