Skip to content

Commit b742c45

Browse files
authored
feat(crons): Make monitor async friendly (#2912)
1 parent 6c2eb53 commit b742c45

File tree

6 files changed

+254
-55
lines changed

6 files changed

+254
-55
lines changed

sentry_sdk/crons/_decorator.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from functools import wraps
2+
from inspect import iscoroutinefunction
3+
4+
from sentry_sdk._types import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from typing import (
8+
Awaitable,
9+
Callable,
10+
ParamSpec,
11+
TypeVar,
12+
Union,
13+
)
14+
15+
P = ParamSpec("P")
16+
R = TypeVar("R")
17+
18+
19+
class MonitorMixin:
20+
def __call__(self, fn):
21+
# type: (Callable[P, R]) -> Callable[P, Union[R, Awaitable[R]]]
22+
if iscoroutinefunction(fn):
23+
24+
@wraps(fn)
25+
async def inner(*args: "P.args", **kwargs: "P.kwargs"):
26+
# type: (...) -> R
27+
with self: # type: ignore[attr-defined]
28+
return await fn(*args, **kwargs)
29+
30+
else:
31+
32+
@wraps(fn)
33+
def inner(*args: "P.args", **kwargs: "P.kwargs"):
34+
# type: (...) -> R
35+
with self: # type: ignore[attr-defined]
36+
return fn(*args, **kwargs)
37+
38+
return inner

sentry_sdk/crons/_decorator_py2.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from functools import wraps
2+
3+
from sentry_sdk._types import TYPE_CHECKING
4+
5+
if TYPE_CHECKING:
6+
from typing import Any, Callable, ParamSpec, TypeVar
7+
8+
P = ParamSpec("P")
9+
R = TypeVar("R")
10+
11+
12+
class MonitorMixin:
13+
def __call__(self, fn):
14+
# type: (Callable[P, R]) -> Callable[P, R]
15+
@wraps(fn)
16+
def inner(*args, **kwargs):
17+
# type: (Any, Any) -> Any
18+
with self: # type: ignore[attr-defined]
19+
return fn(*args, **kwargs)
20+
21+
return inner

sentry_sdk/crons/decorator.py

+34-29
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
import sys
2-
3-
from sentry_sdk._compat import contextmanager, reraise
1+
from sentry_sdk._compat import PY2
42
from sentry_sdk._types import TYPE_CHECKING
53
from sentry_sdk.crons import capture_checkin
64
from sentry_sdk.crons.consts import MonitorStatus
75
from sentry_sdk.utils import now
86

97
if TYPE_CHECKING:
10-
from typing import Generator, Optional
8+
from typing import Optional, Type
9+
from types import TracebackType
10+
11+
if PY2:
12+
from sentry_sdk.crons._decorator_py2 import MonitorMixin
13+
else:
14+
# This is in its own module so that we don't make Python 2
15+
# angery over `async def`s.
16+
# Once we drop Python 2, remove the mixin and merge it
17+
# into the main monitor class.
18+
from sentry_sdk.crons._decorator import MonitorMixin
1119

1220

13-
@contextmanager
14-
def monitor(monitor_slug=None):
15-
# type: (Optional[str]) -> Generator[None, None, None]
21+
class monitor(MonitorMixin): # noqa: N801
1622
"""
1723
Decorator/context manager to capture checkin events for a monitor.
1824
@@ -39,32 +45,31 @@ def test(arg):
3945
with sentry_sdk.monitor(monitor_slug='my-fancy-slug'):
4046
print(arg)
4147
```
48+
"""
4249

50+
def __init__(self, monitor_slug=None):
51+
# type: (Optional[str]) -> None
52+
self.monitor_slug = monitor_slug
4353

44-
"""
54+
def __enter__(self):
55+
# type: () -> None
56+
self.start_timestamp = now()
57+
self.check_in_id = capture_checkin(
58+
monitor_slug=self.monitor_slug, status=MonitorStatus.IN_PROGRESS
59+
)
60+
61+
def __exit__(self, exc_type, exc_value, traceback):
62+
# type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None
63+
duration_s = now() - self.start_timestamp
4564

46-
start_timestamp = now()
47-
check_in_id = capture_checkin(
48-
monitor_slug=monitor_slug, status=MonitorStatus.IN_PROGRESS
49-
)
65+
if exc_type is None and exc_value is None and traceback is None:
66+
status = MonitorStatus.OK
67+
else:
68+
status = MonitorStatus.ERROR
5069

51-
try:
52-
yield
53-
except Exception:
54-
duration_s = now() - start_timestamp
5570
capture_checkin(
56-
monitor_slug=monitor_slug,
57-
check_in_id=check_in_id,
58-
status=MonitorStatus.ERROR,
71+
monitor_slug=self.monitor_slug,
72+
check_in_id=self.check_in_id,
73+
status=status,
5974
duration=duration_s,
6075
)
61-
exc_info = sys.exc_info()
62-
reraise(*exc_info)
63-
64-
duration_s = now() - start_timestamp
65-
capture_checkin(
66-
monitor_slug=monitor_slug,
67-
check_in_id=check_in_id,
68-
status=MonitorStatus.OK,
69-
duration=duration_s,
70-
)

tests/crons/__init__.py

Whitespace-only changes.

tests/test_crons.py renamed to tests/crons/test_crons.py

+25-26
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
import uuid
33

44
import sentry_sdk
5-
from sentry_sdk.crons import capture_checkin
6-
75
from sentry_sdk import Hub, configure_scope, set_level
6+
from sentry_sdk.crons import capture_checkin
87

98
try:
109
from unittest import mock # python 3.3 and above
@@ -39,95 +38,95 @@ def test_decorator(sentry_init):
3938

4039
with mock.patch(
4140
"sentry_sdk.crons.decorator.capture_checkin"
42-
) as fake_capture_checking:
41+
) as fake_capture_checkin:
4342
result = _hello_world("Grace")
4443
assert result == "Hello, Grace"
4544

4645
# Check for initial checkin
47-
fake_capture_checking.assert_has_calls(
46+
fake_capture_checkin.assert_has_calls(
4847
[
4948
mock.call(monitor_slug="abc123", status="in_progress"),
5049
]
5150
)
5251

5352
# Check for final checkin
54-
assert fake_capture_checking.call_args[1]["monitor_slug"] == "abc123"
55-
assert fake_capture_checking.call_args[1]["status"] == "ok"
56-
assert fake_capture_checking.call_args[1]["duration"]
57-
assert fake_capture_checking.call_args[1]["check_in_id"]
53+
assert fake_capture_checkin.call_args[1]["monitor_slug"] == "abc123"
54+
assert fake_capture_checkin.call_args[1]["status"] == "ok"
55+
assert fake_capture_checkin.call_args[1]["duration"]
56+
assert fake_capture_checkin.call_args[1]["check_in_id"]
5857

5958

6059
def test_decorator_error(sentry_init):
6160
sentry_init()
6261

6362
with mock.patch(
6463
"sentry_sdk.crons.decorator.capture_checkin"
65-
) as fake_capture_checking:
64+
) as fake_capture_checkin:
6665
with pytest.raises(ZeroDivisionError):
6766
result = _break_world("Grace")
6867

6968
assert "result" not in locals()
7069

7170
# Check for initial checkin
72-
fake_capture_checking.assert_has_calls(
71+
fake_capture_checkin.assert_has_calls(
7372
[
7473
mock.call(monitor_slug="def456", status="in_progress"),
7574
]
7675
)
7776

7877
# Check for final checkin
79-
assert fake_capture_checking.call_args[1]["monitor_slug"] == "def456"
80-
assert fake_capture_checking.call_args[1]["status"] == "error"
81-
assert fake_capture_checking.call_args[1]["duration"]
82-
assert fake_capture_checking.call_args[1]["check_in_id"]
78+
assert fake_capture_checkin.call_args[1]["monitor_slug"] == "def456"
79+
assert fake_capture_checkin.call_args[1]["status"] == "error"
80+
assert fake_capture_checkin.call_args[1]["duration"]
81+
assert fake_capture_checkin.call_args[1]["check_in_id"]
8382

8483

8584
def test_contextmanager(sentry_init):
8685
sentry_init()
8786

8887
with mock.patch(
8988
"sentry_sdk.crons.decorator.capture_checkin"
90-
) as fake_capture_checking:
89+
) as fake_capture_checkin:
9190
result = _hello_world_contextmanager("Grace")
9291
assert result == "Hello, Grace"
9392

9493
# Check for initial checkin
95-
fake_capture_checking.assert_has_calls(
94+
fake_capture_checkin.assert_has_calls(
9695
[
9796
mock.call(monitor_slug="abc123", status="in_progress"),
9897
]
9998
)
10099

101100
# Check for final checkin
102-
assert fake_capture_checking.call_args[1]["monitor_slug"] == "abc123"
103-
assert fake_capture_checking.call_args[1]["status"] == "ok"
104-
assert fake_capture_checking.call_args[1]["duration"]
105-
assert fake_capture_checking.call_args[1]["check_in_id"]
101+
assert fake_capture_checkin.call_args[1]["monitor_slug"] == "abc123"
102+
assert fake_capture_checkin.call_args[1]["status"] == "ok"
103+
assert fake_capture_checkin.call_args[1]["duration"]
104+
assert fake_capture_checkin.call_args[1]["check_in_id"]
106105

107106

108107
def test_contextmanager_error(sentry_init):
109108
sentry_init()
110109

111110
with mock.patch(
112111
"sentry_sdk.crons.decorator.capture_checkin"
113-
) as fake_capture_checking:
112+
) as fake_capture_checkin:
114113
with pytest.raises(ZeroDivisionError):
115114
result = _break_world_contextmanager("Grace")
116115

117116
assert "result" not in locals()
118117

119118
# Check for initial checkin
120-
fake_capture_checking.assert_has_calls(
119+
fake_capture_checkin.assert_has_calls(
121120
[
122121
mock.call(monitor_slug="def456", status="in_progress"),
123122
]
124123
)
125124

126125
# Check for final checkin
127-
assert fake_capture_checking.call_args[1]["monitor_slug"] == "def456"
128-
assert fake_capture_checking.call_args[1]["status"] == "error"
129-
assert fake_capture_checking.call_args[1]["duration"]
130-
assert fake_capture_checking.call_args[1]["check_in_id"]
126+
assert fake_capture_checkin.call_args[1]["monitor_slug"] == "def456"
127+
assert fake_capture_checkin.call_args[1]["status"] == "error"
128+
assert fake_capture_checkin.call_args[1]["duration"]
129+
assert fake_capture_checkin.call_args[1]["check_in_id"]
131130

132131

133132
def test_capture_checkin_simple(sentry_init):

0 commit comments

Comments
 (0)