Skip to content

Commit 6d925c6

Browse files
Context manager monitor (getsentry#2290)
* Commented a confusing line of code * monitor can now also be used as a contextmanager * fixed so this also works as contextmanager * added unit tests * added type hints * contextmanager docstring * Combine import into one line * Minor changes to docstring
1 parent 7f949f3 commit 6d925c6

File tree

3 files changed

+140
-43
lines changed

3 files changed

+140
-43
lines changed

sentry_sdk/_compat.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import sys
2+
import contextlib
3+
from functools import wraps
24

35
from sentry_sdk._types import TYPE_CHECKING
46

@@ -8,6 +10,7 @@
810
from typing import Any
911
from typing import Type
1012
from typing import TypeVar
13+
from typing import Callable
1114

1215
T = TypeVar("T")
1316

@@ -35,8 +38,44 @@ def implements_str(cls):
3538
cls.__str__ = lambda x: unicode(x).encode("utf-8") # noqa
3639
return cls
3740

41+
# The line below is written as an "exec" because it triggers a syntax error in Python 3
3842
exec("def reraise(tp, value, tb=None):\n raise tp, value, tb")
3943

44+
def contextmanager(func):
45+
# type: (Callable) -> Callable
46+
"""
47+
Decorator which creates a contextmanager that can also be used as a
48+
decorator, similar to how the built-in contextlib.contextmanager
49+
function works in Python 3.2+.
50+
"""
51+
contextmanager_func = contextlib.contextmanager(func)
52+
53+
@wraps(func)
54+
class DecoratorContextManager:
55+
def __init__(self, *args, **kwargs):
56+
# type: (...) -> None
57+
self.the_contextmanager = contextmanager_func(*args, **kwargs)
58+
59+
def __enter__(self):
60+
# type: () -> None
61+
self.the_contextmanager.__enter__()
62+
63+
def __exit__(self, *args, **kwargs):
64+
# type: (...) -> None
65+
self.the_contextmanager.__exit__(*args, **kwargs)
66+
67+
def __call__(self, decorated_func):
68+
# type: (Callable) -> Callable[...]
69+
@wraps(decorated_func)
70+
def when_called(*args, **kwargs):
71+
# type: (...) -> Any
72+
with self.the_contextmanager:
73+
return_val = decorated_func(*args, **kwargs)
74+
return return_val
75+
76+
return when_called
77+
78+
return DecoratorContextManager
4079

4180
else:
4281
import urllib.parse as urlparse # noqa
@@ -59,6 +98,9 @@ def reraise(tp, value, tb=None):
5998
raise value.with_traceback(tb)
6099
raise value
61100

101+
# contextlib.contextmanager already can be used as decorator in Python 3.2+
102+
contextmanager = contextlib.contextmanager
103+
62104

63105
def with_metaclass(meta, *bases):
64106
# type: (Any, *Any) -> Any

sentry_sdk/crons/decorator.py

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
1-
from functools import wraps
21
import sys
32

4-
from sentry_sdk._compat import reraise
3+
from sentry_sdk._compat import contextmanager, reraise
54
from sentry_sdk._types import TYPE_CHECKING
65
from sentry_sdk.crons import capture_checkin
76
from sentry_sdk.crons.consts import MonitorStatus
87
from sentry_sdk.utils import now
98

10-
119
if TYPE_CHECKING:
12-
from typing import Any, Callable, Optional
10+
from typing import Generator, Optional
1311

1412

13+
@contextmanager
1514
def monitor(monitor_slug=None):
16-
# type: (Optional[str]) -> Callable[..., Any]
15+
# type: (Optional[str]) -> Generator[None, None, None]
1716
"""
18-
Decorator to capture checkin events for a monitor.
17+
Decorator/context manager to capture checkin events for a monitor.
1918
20-
Usage:
19+
Usage (as decorator):
2120
```
2221
import sentry_sdk
2322
@@ -31,44 +30,41 @@ def test(arg):
3130
3231
This does not have to be used with Celery, but if you do use it with celery,
3332
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
4033
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)
34+
Usage (as context manager):
35+
```
36+
import sentry_sdk
6137
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-
)
38+
def test(arg):
39+
with sentry_sdk.monitor(monitor_slug='my-fancy-slug'):
40+
print(arg)
41+
```
6942
70-
return result
7143
72-
return wrapper
44+
"""
7345

74-
return decorate
46+
start_timestamp = now()
47+
check_in_id = capture_checkin(
48+
monitor_slug=monitor_slug, status=MonitorStatus.IN_PROGRESS
49+
)
50+
51+
try:
52+
yield
53+
except Exception:
54+
duration_s = now() - start_timestamp
55+
capture_checkin(
56+
monitor_slug=monitor_slug,
57+
check_in_id=check_in_id,
58+
status=MonitorStatus.ERROR,
59+
duration=duration_s,
60+
)
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/test_crons.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ def _break_world(name):
2121
return "Hello, {}".format(name)
2222

2323

24+
def _hello_world_contextmanager(name):
25+
with sentry_sdk.monitor(monitor_slug="abc123"):
26+
return "Hello, {}".format(name)
27+
28+
29+
def _break_world_contextmanager(name):
30+
with sentry_sdk.monitor(monitor_slug="def456"):
31+
1 / 0
32+
return "Hello, {}".format(name)
33+
34+
2435
def test_decorator(sentry_init):
2536
sentry_init()
2637

@@ -69,6 +80,54 @@ def test_decorator_error(sentry_init):
6980
assert fake_capture_checking.call_args[1]["check_in_id"]
7081

7182

83+
def test_contextmanager(sentry_init):
84+
sentry_init()
85+
86+
with mock.patch(
87+
"sentry_sdk.crons.decorator.capture_checkin"
88+
) as fake_capture_checking:
89+
result = _hello_world_contextmanager("Grace")
90+
assert result == "Hello, Grace"
91+
92+
# Check for initial checkin
93+
fake_capture_checking.assert_has_calls(
94+
[
95+
mock.call(monitor_slug="abc123", status="in_progress"),
96+
]
97+
)
98+
99+
# Check for final checkin
100+
assert fake_capture_checking.call_args[1]["monitor_slug"] == "abc123"
101+
assert fake_capture_checking.call_args[1]["status"] == "ok"
102+
assert fake_capture_checking.call_args[1]["duration"]
103+
assert fake_capture_checking.call_args[1]["check_in_id"]
104+
105+
106+
def test_contextmanager_error(sentry_init):
107+
sentry_init()
108+
109+
with mock.patch(
110+
"sentry_sdk.crons.decorator.capture_checkin"
111+
) as fake_capture_checking:
112+
with pytest.raises(Exception):
113+
result = _break_world_contextmanager("Grace")
114+
115+
assert "result" not in locals()
116+
117+
# Check for initial checkin
118+
fake_capture_checking.assert_has_calls(
119+
[
120+
mock.call(monitor_slug="def456", status="in_progress"),
121+
]
122+
)
123+
124+
# Check for final checkin
125+
assert fake_capture_checking.call_args[1]["monitor_slug"] == "def456"
126+
assert fake_capture_checking.call_args[1]["status"] == "error"
127+
assert fake_capture_checking.call_args[1]["duration"]
128+
assert fake_capture_checking.call_args[1]["check_in_id"]
129+
130+
72131
def test_capture_checkin_simple(sentry_init):
73132
sentry_init()
74133

0 commit comments

Comments
 (0)