From 696ac7182cd8b837192f771d33b726a3174540e0 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 1 Mar 2023 17:47:10 +0100 Subject: [PATCH 1/9] Tinkering around --- sentry_sdk/crons.py | 59 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 sentry_sdk/crons.py diff --git a/sentry_sdk/crons.py b/sentry_sdk/crons.py new file mode 100644 index 0000000000..5cbc45c37a --- /dev/null +++ b/sentry_sdk/crons.py @@ -0,0 +1,59 @@ +from functools import wraps +import sys +import uuid + +from sentry_sdk import Hub +from sentry_sdk._compat import reraise + + +class MONITOR_STATUS: + IN_PROGRESS = "in_progress" + OK = "ok" + ERROR = "error" + + +def _create_checkin_event(monitor_id=None, status=None): + event = { + "type": "check_in", + "monitor_id": monitor_id, + "check_in_id": uuid.uuid4().hex, + "status": status, + } + + return event + + +def _capture_monitor_event(monitor_id=None, status=None): + hub = Hub.current + monitor_event = _create_checkin_event(monitor_id, status) + hub.capture_event(monitor_event) + + # self.capture_envelope(Envelope(items=[client_report])) + + +def monitor(monitor_id=None): + def decorate(func): + if not monitor_id: + return func + + @wraps(func) + def wrapper(*args, **kwargs): + _capture_monitor_event( + monitor_id=monitor_id, status=MONITOR_STATUS.IN_PROGRESS + ) + + try: + result = func(*args, **kwargs) + except Exception: + _capture_monitor_event( + monitor_id=monitor_id, status=MONITOR_STATUS.ERROR + ) + exc_info = sys.exc_info() + reraise(*exc_info) + + _capture_monitor_event(monitor_id=monitor_id, status=MONITOR_STATUS.OK) + return result + + return wrapper + + return decorate From 3ec337d429395f2cb16840063e49fd6124738fab Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 1 Mar 2023 17:56:08 +0100 Subject: [PATCH 2/9] Made flake8 happy --- sentry_sdk/crons.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/crons.py b/sentry_sdk/crons.py index 5cbc45c37a..ba57c66bd0 100644 --- a/sentry_sdk/crons.py +++ b/sentry_sdk/crons.py @@ -6,7 +6,7 @@ from sentry_sdk._compat import reraise -class MONITOR_STATUS: +class MonitorStatus: IN_PROGRESS = "in_progress" OK = "ok" ERROR = "error" @@ -39,19 +39,19 @@ def decorate(func): @wraps(func) def wrapper(*args, **kwargs): _capture_monitor_event( - monitor_id=monitor_id, status=MONITOR_STATUS.IN_PROGRESS + monitor_id=monitor_id, status=MonitorStatus.IN_PROGRESS ) try: result = func(*args, **kwargs) except Exception: _capture_monitor_event( - monitor_id=monitor_id, status=MONITOR_STATUS.ERROR + monitor_id=monitor_id, status=MonitorStatus.ERROR ) exc_info = sys.exc_info() reraise(*exc_info) - _capture_monitor_event(monitor_id=monitor_id, status=MONITOR_STATUS.OK) + _capture_monitor_event(monitor_id=monitor_id, status=MonitorStatus.OK) return result return wrapper From 311f3b885489d676f0ffc4a9b7e03532901e6a68 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 2 Mar 2023 14:13:49 +0100 Subject: [PATCH 3/9] Sending checkin envelop --- sentry_sdk/client.py | 7 ++++++- sentry_sdk/crons.py | 23 ++++++++--------------- sentry_sdk/envelope.py | 6 ++++++ 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 990cce7547..acc5f9eda0 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -430,7 +430,9 @@ def capture_event( .pop("dynamic_sampling_context", {}) ) - # Transactions or events with attachments should go to the /envelope/ + is_checkin = event_opt.get("type") == "check_in" + + # Transactions, events with attachments, and checkins should go to the /envelope/ # endpoint. if is_transaction or attachments: @@ -448,11 +450,14 @@ def capture_event( if profile is not None: envelope.add_profile(profile.to_json(event_opt, self.options)) envelope.add_transaction(event_opt) + elif is_checkin: + envelope.add_checkin(event_opt) else: envelope.add_event(event_opt) for attachment in attachments or (): envelope.add_item(attachment.to_envelope_item()) + self.transport.capture_envelope(envelope) else: # All other events go to the /store/ endpoint. diff --git a/sentry_sdk/crons.py b/sentry_sdk/crons.py index ba57c66bd0..12caa64895 100644 --- a/sentry_sdk/crons.py +++ b/sentry_sdk/crons.py @@ -13,22 +13,19 @@ class MonitorStatus: def _create_checkin_event(monitor_id=None, status=None): - event = { - "type": "check_in", + checkin = { "monitor_id": monitor_id, "check_in_id": uuid.uuid4().hex, "status": status, } - return event + return checkin -def _capture_monitor_event(monitor_id=None, status=None): +def capture_checkin(monitor_id=None, status=None): hub = Hub.current - monitor_event = _create_checkin_event(monitor_id, status) - hub.capture_event(monitor_event) - - # self.capture_envelope(Envelope(items=[client_report])) + checkin_event = _create_checkin_event(monitor_id, status) + hub.capture_event(checkin_event) def monitor(monitor_id=None): @@ -38,20 +35,16 @@ def decorate(func): @wraps(func) def wrapper(*args, **kwargs): - _capture_monitor_event( - monitor_id=monitor_id, status=MonitorStatus.IN_PROGRESS - ) + capture_checkin(monitor_id=monitor_id, status=MonitorStatus.IN_PROGRESS) try: result = func(*args, **kwargs) except Exception: - _capture_monitor_event( - monitor_id=monitor_id, status=MonitorStatus.ERROR - ) + capture_checkin(monitor_id=monitor_id, status=MonitorStatus.ERROR) exc_info = sys.exc_info() reraise(*exc_info) - _capture_monitor_event(monitor_id=monitor_id, status=MonitorStatus.OK) + capture_checkin(monitor_id=monitor_id, status=MonitorStatus.OK) return result return wrapper diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index 24eb87b91f..a3b7fabd83 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -68,6 +68,12 @@ def add_profile( # type: (...) -> None self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) + def add_checkin( + self, checkin # type: Any + ): + # type: (...) -> None + self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in")) + def add_session( self, session # type: Union[Session, Any] ): From 3d294c0ed96c5fc983b264a2e85d3bba367cc6ad Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 2 Mar 2023 16:25:46 +0100 Subject: [PATCH 4/9] Some polishing --- sentry_sdk/client.py | 2 +- sentry_sdk/crons.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index acc5f9eda0..4c0f2ce417 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -434,7 +434,7 @@ def capture_event( # Transactions, events with attachments, and checkins should go to the /envelope/ # endpoint. - if is_transaction or attachments: + if is_transaction or is_checkin or attachments: headers = { "event_id": event_opt["event_id"], diff --git a/sentry_sdk/crons.py b/sentry_sdk/crons.py index 12caa64895..899630595f 100644 --- a/sentry_sdk/crons.py +++ b/sentry_sdk/crons.py @@ -14,6 +14,7 @@ class MonitorStatus: def _create_checkin_event(monitor_id=None, status=None): checkin = { + "type": "check_in", "monitor_id": monitor_id, "check_in_id": uuid.uuid4().hex, "status": status, @@ -29,6 +30,25 @@ def capture_checkin(monitor_id=None, status=None): def monitor(monitor_id=None): + """ + Decorator to capture checkin events for a monitor. + + Usage: + ``` + from sentry_sdk.crons import monitor + + app = Celery() + + @app.task + @monitor(monitor_id='3475c0de-0258-44fc-8c88-07350cb7f9af') + def test(arg): + print(arg) + ``` + + This does not have to be used with Celery, but if you do use it with celery, + put the `monitor` decorator under Celery's `@app.task` decorator. + """ + def decorate(func): if not monitor_id: return func From cdaf88261c6eb3b553223bae6c1738b733fe8a30 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 9 Mar 2023 09:40:58 +0100 Subject: [PATCH 5/9] Added more data to crons --- sentry_sdk/__init__.py | 3 ++ sentry_sdk/crons.py | 62 +++++++++++++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 4d40efacce..1cd2e089c6 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -7,6 +7,9 @@ from sentry_sdk.consts import VERSION # noqa +from sentry_sdk.crons import monitor # noqa + + __all__ = [ # noqa "Hub", "Scope", diff --git a/sentry_sdk/crons.py b/sentry_sdk/crons.py index 899630595f..85d7ec25bd 100644 --- a/sentry_sdk/crons.py +++ b/sentry_sdk/crons.py @@ -4,6 +4,7 @@ from sentry_sdk import Hub from sentry_sdk._compat import reraise +from sentry_sdk.utils import nanosecond_time class MonitorStatus: @@ -12,59 +13,94 @@ class MonitorStatus: ERROR = "error" -def _create_checkin_event(monitor_id=None, status=None): +def _create_checkin_event( + monitor_slug=None, check_in_id=None, status=None, duration=None +): + options = Hub.current.client.options + check_in_id = check_in_id or uuid.uuid4().hex + # convert nanosecond to millisecond + duration = int(duration * 0.000001) if duration is not None else duration + checkin = { "type": "check_in", - "monitor_id": monitor_id, - "check_in_id": uuid.uuid4().hex, + "monitor_id": monitor_slug, + "check_in_id": check_in_id, "status": status, + "duration": duration, + "environment": options["environment"], + "release": options["release"], } return checkin -def capture_checkin(monitor_id=None, status=None): +def capture_checkin(monitor_slug=None, check_in_id=None, status=None, duration=None): hub = Hub.current - checkin_event = _create_checkin_event(monitor_id, status) + + check_in_id = check_in_id or uuid.uuid4().hex + checkin_event = _create_checkin_event( + monitor_slug=monitor_slug, + check_in_id=check_in_id, + status=status, + duration=duration, + ) hub.capture_event(checkin_event) + return checkin_event["check_in_id"] -def monitor(monitor_id=None): + +def monitor(monitor_slug=None): """ Decorator to capture checkin events for a monitor. Usage: ``` - from sentry_sdk.crons import monitor + import sentry_sdk app = Celery() @app.task - @monitor(monitor_id='3475c0de-0258-44fc-8c88-07350cb7f9af') + @sentry_sdk.monitor(monitor_slug='3475c0de-0258-44fc-8c88-07350cb7f9af') def test(arg): print(arg) ``` This does not have to be used with Celery, but if you do use it with celery, - put the `monitor` decorator under Celery's `@app.task` decorator. + put the `@sentry_sdk.monitor` decorator under Celery's `@app.task` decorator. """ def decorate(func): - if not monitor_id: + if not monitor_slug: return func @wraps(func) def wrapper(*args, **kwargs): - capture_checkin(monitor_id=monitor_id, status=MonitorStatus.IN_PROGRESS) + start_timestamp = nanosecond_time() + check_in_id = capture_checkin( + monitor_slug=monitor_slug, status=MonitorStatus.IN_PROGRESS + ) try: result = func(*args, **kwargs) except Exception: - capture_checkin(monitor_id=monitor_id, status=MonitorStatus.ERROR) + duration = nanosecond_time() - start_timestamp + capture_checkin( + monitor_slug=monitor_slug, + check_in_id=check_in_id, + status=MonitorStatus.ERROR, + duration=duration, + ) exc_info = sys.exc_info() reraise(*exc_info) - capture_checkin(monitor_id=monitor_id, status=MonitorStatus.OK) + duration = nanosecond_time() - start_timestamp + capture_checkin( + monitor_slug=monitor_slug, + check_in_id=check_in_id, + status=MonitorStatus.OK, + duration=duration, + ) + return result return wrapper From 1607ef1aa0c818572d585db2bfc099470de322d3 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 9 Mar 2023 10:24:19 +0100 Subject: [PATCH 6/9] Preparations for monitor_config --- sentry_sdk/crons.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/crons.py b/sentry_sdk/crons.py index 85d7ec25bd..755fd4d19c 100644 --- a/sentry_sdk/crons.py +++ b/sentry_sdk/crons.py @@ -24,6 +24,10 @@ def _create_checkin_event( checkin = { "type": "check_in", "monitor_id": monitor_slug, + # "monitor_config": { + # "schedule": "*/10 0 0 0 0", + # "schedule_type": "cron", + # }, "check_in_id": check_in_id, "status": status, "duration": duration, @@ -31,6 +35,9 @@ def _create_checkin_event( "release": options["release"], } + print("Checkin") + print(checkin) + return checkin @@ -49,7 +56,7 @@ def capture_checkin(monitor_slug=None, check_in_id=None, status=None, duration=N return checkin_event["check_in_id"] -def monitor(monitor_slug=None): +def monitor(monitor_slug=None, app=None): """ Decorator to capture checkin events for a monitor. From 8945ad1bd41047f19296ee775e8699b4829d1b1e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 13 Mar 2023 11:57:15 +0100 Subject: [PATCH 7/9] Renamed monitor_id to monitor_slug --- sentry_sdk/crons.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/crons.py b/sentry_sdk/crons.py index 755fd4d19c..144d8dc6d1 100644 --- a/sentry_sdk/crons.py +++ b/sentry_sdk/crons.py @@ -23,7 +23,8 @@ def _create_checkin_event( checkin = { "type": "check_in", - "monitor_id": monitor_slug, + "monitor_slug": monitor_slug, + # TODO: Add schedule and schedule_type to monitor config # "monitor_config": { # "schedule": "*/10 0 0 0 0", # "schedule_type": "cron", From fe26bf999c6d427313ab08c8136d572c702b0183 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 13 Mar 2023 14:36:02 +0100 Subject: [PATCH 8/9] Added tests and types --- sentry_sdk/crons.py | 17 ++++++--- tests/test_crons.py | 88 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 tests/test_crons.py diff --git a/sentry_sdk/crons.py b/sentry_sdk/crons.py index 144d8dc6d1..9faee8928f 100644 --- a/sentry_sdk/crons.py +++ b/sentry_sdk/crons.py @@ -4,9 +4,14 @@ from sentry_sdk import Hub from sentry_sdk._compat import reraise +from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.utils import nanosecond_time +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional + + class MonitorStatus: IN_PROGRESS = "in_progress" OK = "ok" @@ -16,8 +21,9 @@ class MonitorStatus: def _create_checkin_event( monitor_slug=None, check_in_id=None, status=None, duration=None ): - options = Hub.current.client.options - check_in_id = check_in_id or uuid.uuid4().hex + # type: (Optional[str], Optional[str], Optional[str], Optional[float]) -> Dict[str, Any] + options = Hub.current.client.options if Hub.current.client else {} + check_in_id = check_in_id or uuid.uuid4().hex # type: str # convert nanosecond to millisecond duration = int(duration * 0.000001) if duration is not None else duration @@ -36,13 +42,11 @@ def _create_checkin_event( "release": options["release"], } - print("Checkin") - print(checkin) - return checkin def capture_checkin(monitor_slug=None, check_in_id=None, status=None, duration=None): + # type: (Optional[str], Optional[str], Optional[str], Optional[float]) -> str hub = Hub.current check_in_id = check_in_id or uuid.uuid4().hex @@ -58,6 +62,7 @@ def capture_checkin(monitor_slug=None, check_in_id=None, status=None, duration=N def monitor(monitor_slug=None, app=None): + # type: (Optional[str], Any) -> Callable[..., Any] """ Decorator to capture checkin events for a monitor. @@ -78,11 +83,13 @@ def test(arg): """ def decorate(func): + # type: (Callable[..., Any]) -> Callable[..., Any] if not monitor_slug: return func @wraps(func) def wrapper(*args, **kwargs): + # type: (*Any, **Any) -> Any start_timestamp = nanosecond_time() check_in_id = capture_checkin( monitor_slug=monitor_slug, status=MonitorStatus.IN_PROGRESS diff --git a/tests/test_crons.py b/tests/test_crons.py new file mode 100644 index 0000000000..dd632a315a --- /dev/null +++ b/tests/test_crons.py @@ -0,0 +1,88 @@ +import mock +import pytest +import uuid + +import sentry_sdk +from sentry_sdk.crons import capture_checkin + + +@sentry_sdk.monitor(monitor_slug="abc123") +def _hello_world(name): + return "Hello, {}".format(name) + + +@sentry_sdk.monitor(monitor_slug="def456") +def _break_world(name): + 1 / 0 + return "Hello, {}".format(name) + + +def test_decorator(sentry_init): + sentry_init() + + with mock.patch("sentry_sdk.crons.capture_checkin") as fake_capture_checking: + result = _hello_world("Grace") + assert result == "Hello, Grace" + + # Check for initial checkin + fake_capture_checking.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"] + + +def test_decorator_error(sentry_init): + sentry_init() + + with mock.patch("sentry_sdk.crons.capture_checkin") as fake_capture_checking: + with pytest.raises(Exception): + result = _break_world("Grace") + + assert "result" not in locals() + + # Check for initial checkin + fake_capture_checking.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"] + + +def test_capture_checkin_simple(sentry_init): + sentry_init() + + check_in_id = capture_checkin( + monitor_slug="abc123", + check_in_id="112233", + status=None, + duration=None, + ) + assert check_in_id == "112233" + + +def test_capture_checkin_new_id(sentry_init): + sentry_init() + + with mock.patch("uuid.uuid4") as mock_uuid: + mock_uuid.return_value = uuid.UUID("a8098c1a-f86e-11da-bd1a-00112444be1e") + check_in_id = capture_checkin( + monitor_slug="abc123", + check_in_id=None, + status=None, + duration=None, + ) + + assert check_in_id == "a8098c1af86e11dabd1a00112444be1e" From 5fc4c9a40efd1d16d77c53ba47afd2b09d0cba4f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 13 Mar 2023 14:40:29 +0100 Subject: [PATCH 9/9] Documentation --- sentry_sdk/crons.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/crons.py b/sentry_sdk/crons.py index 9faee8928f..e652460df4 100644 --- a/sentry_sdk/crons.py +++ b/sentry_sdk/crons.py @@ -73,13 +73,13 @@ def monitor(monitor_slug=None, app=None): app = Celery() @app.task - @sentry_sdk.monitor(monitor_slug='3475c0de-0258-44fc-8c88-07350cb7f9af') + @sentry_sdk.monitor(monitor_slug='my-fancy-slug') def test(arg): print(arg) ``` This does not have to be used with Celery, but if you do use it with celery, - put the `@sentry_sdk.monitor` decorator under Celery's `@app.task` decorator. + put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. """ def decorate(func):