Skip to content
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

fix(crons): Make monitor async friendly #2912

Merged
merged 12 commits into from
Mar 28, 2024
38 changes: 38 additions & 0 deletions sentry_sdk/crons/_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from functools import wraps
from inspect import iscoroutinefunction

from sentry_sdk._types import TYPE_CHECKING

if TYPE_CHECKING:
from typing import (

Check warning on line 7 in sentry_sdk/crons/_decorator.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/_decorator.py#L7

Added line #L7 was not covered by tests
Awaitable,
Callable,
ParamSpec,
TypeVar,
Union,
)

P = ParamSpec("P")
R = TypeVar("R")

Check warning on line 16 in sentry_sdk/crons/_decorator.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/_decorator.py#L15-L16

Added lines #L15 - L16 were not covered by tests


class MonitorMixin:
def __call__(self, fn):
# type: (Callable[P, R]) -> Callable[P, Union[R, Awaitable[R]]]
if iscoroutinefunction(fn):

@wraps(fn)
async def inner(*args: "P.args", **kwargs: "P.kwargs"):
# type: (...) -> R
with self: # type: ignore[attr-defined]
return await fn(*args, **kwargs)

else:

@wraps(fn)
def inner(*args: "P.args", **kwargs: "P.kwargs"):
# type: (...) -> R
with self: # type: ignore[attr-defined]
return fn(*args, **kwargs)

return inner
21 changes: 21 additions & 0 deletions sentry_sdk/crons/_decorator_py2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from functools import wraps

Check warning on line 1 in sentry_sdk/crons/_decorator_py2.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/_decorator_py2.py#L1

Added line #L1 was not covered by tests

from sentry_sdk._types import TYPE_CHECKING

Check warning on line 3 in sentry_sdk/crons/_decorator_py2.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/_decorator_py2.py#L3

Added line #L3 was not covered by tests

if TYPE_CHECKING:
from typing import Any, Callable, ParamSpec, TypeVar

Check warning on line 6 in sentry_sdk/crons/_decorator_py2.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/_decorator_py2.py#L6

Added line #L6 was not covered by tests

P = ParamSpec("P")
R = TypeVar("R")

Check warning on line 9 in sentry_sdk/crons/_decorator_py2.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/_decorator_py2.py#L8-L9

Added lines #L8 - L9 were not covered by tests


class MonitorMixin:
def __call__(self, fn):

Check warning on line 13 in sentry_sdk/crons/_decorator_py2.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/_decorator_py2.py#L13

Added line #L13 was not covered by tests
# type: (Callable[P, R]) -> Callable[P, R]
@wraps(fn)

Check warning on line 15 in sentry_sdk/crons/_decorator_py2.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/_decorator_py2.py#L15

Added line #L15 was not covered by tests
def inner(*args, **kwargs):
# type: (Any, Any) -> Any
with self: # type: ignore[attr-defined]
return fn(*args, **kwargs)

Check warning on line 19 in sentry_sdk/crons/_decorator_py2.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/_decorator_py2.py#L19

Added line #L19 was not covered by tests

return inner

Check warning on line 21 in sentry_sdk/crons/_decorator_py2.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/_decorator_py2.py#L21

Added line #L21 was not covered by tests
63 changes: 34 additions & 29 deletions sentry_sdk/crons/decorator.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import sys

from sentry_sdk._compat import contextmanager, reraise
from sentry_sdk._compat import PY2
from sentry_sdk._types import TYPE_CHECKING
from sentry_sdk.crons import capture_checkin
from sentry_sdk.crons.consts import MonitorStatus
from sentry_sdk.utils import now

if TYPE_CHECKING:
from typing import Generator, Optional
from typing import Optional, Type
from types import TracebackType

Check warning on line 9 in sentry_sdk/crons/decorator.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/decorator.py#L8-L9

Added lines #L8 - L9 were not covered by tests

if PY2:
from sentry_sdk.crons._decorator_py2 import MonitorMixin

Check warning on line 12 in sentry_sdk/crons/decorator.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/crons/decorator.py#L12

Added line #L12 was not covered by tests
else:
# This is in its own module so that we don't make Python 2
# angery over `async def`s.
# Once we drop Python 2, remove the mixin and merge it
# into the main monitor class.
from sentry_sdk.crons._decorator import MonitorMixin


@contextmanager
def monitor(monitor_slug=None):
# type: (Optional[str]) -> Generator[None, None, None]
class monitor(MonitorMixin): # noqa: N801
"""
Decorator/context manager to capture checkin events for a monitor.

Expand All @@ -39,32 +45,31 @@
with sentry_sdk.monitor(monitor_slug='my-fancy-slug'):
print(arg)
```
"""

def __init__(self, monitor_slug=None):
# type: (Optional[str]) -> None
self.monitor_slug = monitor_slug

"""
def __enter__(self):
# type: () -> None
self.start_timestamp = now()
self.check_in_id = capture_checkin(
monitor_slug=self.monitor_slug, status=MonitorStatus.IN_PROGRESS
)

def __exit__(self, exc_type, exc_value, traceback):
# type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None
duration_s = now() - self.start_timestamp

start_timestamp = now()
check_in_id = capture_checkin(
monitor_slug=monitor_slug, status=MonitorStatus.IN_PROGRESS
)
if exc_type is None and exc_value is None and traceback is None:
status = MonitorStatus.OK
else:
status = MonitorStatus.ERROR

try:
yield
except Exception:
duration_s = now() - start_timestamp
capture_checkin(
monitor_slug=monitor_slug,
check_in_id=check_in_id,
status=MonitorStatus.ERROR,
monitor_slug=self.monitor_slug,
check_in_id=self.check_in_id,
status=status,
duration=duration_s,
)
exc_info = sys.exc_info()
reraise(*exc_info)

duration_s = now() - start_timestamp
capture_checkin(
monitor_slug=monitor_slug,
check_in_id=check_in_id,
status=MonitorStatus.OK,
duration=duration_s,
)
Empty file added tests/crons/__init__.py
Empty file.
51 changes: 25 additions & 26 deletions tests/test_crons.py → tests/crons/test_crons.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
import uuid

import sentry_sdk
from sentry_sdk.crons import capture_checkin

from sentry_sdk import Hub, configure_scope, set_level
from sentry_sdk.crons import capture_checkin

try:
from unittest import mock # python 3.3 and above
Expand Down Expand Up @@ -39,95 +38,95 @@ def test_decorator(sentry_init):

with mock.patch(
"sentry_sdk.crons.decorator.capture_checkin"
) as fake_capture_checking:
) as fake_capture_checkin:
result = _hello_world("Grace")
assert result == "Hello, Grace"

# Check for initial checkin
fake_capture_checking.assert_has_calls(
fake_capture_checkin.assert_has_calls(
[
mock.call(monitor_slug="abc123", status="in_progress"),
]
)

# Check for final checkin
assert fake_capture_checking.call_args[1]["monitor_slug"] == "abc123"
assert fake_capture_checking.call_args[1]["status"] == "ok"
assert fake_capture_checking.call_args[1]["duration"]
assert fake_capture_checking.call_args[1]["check_in_id"]
assert fake_capture_checkin.call_args[1]["monitor_slug"] == "abc123"
assert fake_capture_checkin.call_args[1]["status"] == "ok"
assert fake_capture_checkin.call_args[1]["duration"]
assert fake_capture_checkin.call_args[1]["check_in_id"]


def test_decorator_error(sentry_init):
sentry_init()

with mock.patch(
"sentry_sdk.crons.decorator.capture_checkin"
) as fake_capture_checking:
) as fake_capture_checkin:
with pytest.raises(ZeroDivisionError):
result = _break_world("Grace")

assert "result" not in locals()

# Check for initial checkin
fake_capture_checking.assert_has_calls(
fake_capture_checkin.assert_has_calls(
[
mock.call(monitor_slug="def456", status="in_progress"),
]
)

# Check for final checkin
assert fake_capture_checking.call_args[1]["monitor_slug"] == "def456"
assert fake_capture_checking.call_args[1]["status"] == "error"
assert fake_capture_checking.call_args[1]["duration"]
assert fake_capture_checking.call_args[1]["check_in_id"]
assert fake_capture_checkin.call_args[1]["monitor_slug"] == "def456"
assert fake_capture_checkin.call_args[1]["status"] == "error"
assert fake_capture_checkin.call_args[1]["duration"]
assert fake_capture_checkin.call_args[1]["check_in_id"]


def test_contextmanager(sentry_init):
sentry_init()

with mock.patch(
"sentry_sdk.crons.decorator.capture_checkin"
) as fake_capture_checking:
) as fake_capture_checkin:
result = _hello_world_contextmanager("Grace")
assert result == "Hello, Grace"

# Check for initial checkin
fake_capture_checking.assert_has_calls(
fake_capture_checkin.assert_has_calls(
[
mock.call(monitor_slug="abc123", status="in_progress"),
]
)

# Check for final checkin
assert fake_capture_checking.call_args[1]["monitor_slug"] == "abc123"
assert fake_capture_checking.call_args[1]["status"] == "ok"
assert fake_capture_checking.call_args[1]["duration"]
assert fake_capture_checking.call_args[1]["check_in_id"]
assert fake_capture_checkin.call_args[1]["monitor_slug"] == "abc123"
assert fake_capture_checkin.call_args[1]["status"] == "ok"
assert fake_capture_checkin.call_args[1]["duration"]
assert fake_capture_checkin.call_args[1]["check_in_id"]


def test_contextmanager_error(sentry_init):
sentry_init()

with mock.patch(
"sentry_sdk.crons.decorator.capture_checkin"
) as fake_capture_checking:
) as fake_capture_checkin:
with pytest.raises(ZeroDivisionError):
result = _break_world_contextmanager("Grace")

assert "result" not in locals()

# Check for initial checkin
fake_capture_checking.assert_has_calls(
fake_capture_checkin.assert_has_calls(
[
mock.call(monitor_slug="def456", status="in_progress"),
]
)

# Check for final checkin
assert fake_capture_checking.call_args[1]["monitor_slug"] == "def456"
assert fake_capture_checking.call_args[1]["status"] == "error"
assert fake_capture_checking.call_args[1]["duration"]
assert fake_capture_checking.call_args[1]["check_in_id"]
assert fake_capture_checkin.call_args[1]["monitor_slug"] == "def456"
assert fake_capture_checkin.call_args[1]["status"] == "error"
assert fake_capture_checkin.call_args[1]["duration"]
assert fake_capture_checkin.call_args[1]["check_in_id"]


def test_capture_checkin_simple(sentry_init):
Expand Down
Loading
Loading