Skip to content

Rename heartbeats to cron check-ins #219

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

Merged
merged 3 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changesets/deprecate-heartbeats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
bump: patch
type: deprecate
---

Calls to `appsignal.heartbeat` and to the `appsignal.Heartbeat` constructor will emit a deprecation warning.
24 changes: 24 additions & 0 deletions .changesets/rename-heartbeats-to-cron-check-ins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
bump: patch
type: change
---

Rename heartbeats to cron check-ins. Calls to `appsignal.heartbeat` and `appsignal.Heartbeat` should be replaced with calls to `appsignal.check_in.cron` and `appsignal.check_in.Cron`, for example:

```python
# Before
from appsignal import heartbeat

def do_something():
pass

heartbeat("do_something", do_something)

# After
from appsignal.check_in import cron

def do_something():
pass

cron("do_something", do_something)
```
9 changes: 9 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from appsignal import probes
from appsignal.agent import agent
from appsignal.client import _reset_client
from appsignal.heartbeat import _heartbeat_class_warning, _heartbeat_helper_warning
from appsignal.internal_logger import _reset_logger
from appsignal.opentelemetry import METRICS_PREFERRED_TEMPORALITY

Expand Down Expand Up @@ -110,3 +111,11 @@ def stop_agent():
working_dir = os.path.join(tmp_path, "appsignal")
if os.path.isdir(working_dir):
os.system(f"rm -rf {working_dir}")


@pytest.fixture(scope="function")
def reset_heartbeat_warnings():
_heartbeat_class_warning.reset()
_heartbeat_helper_warning.reset()

yield
2 changes: 2 additions & 0 deletions src/appsignal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
from runpy import run_path

from . import check_in
from .client import Client as Appsignal
from .heartbeat import Heartbeat, heartbeat
from .metrics import add_distribution_value, increment_counter, set_gauge
Expand Down Expand Up @@ -45,6 +46,7 @@
"add_distribution_value",
"Heartbeat",
"heartbeat",
"check_in",
"start",
]

Expand Down
94 changes: 94 additions & 0 deletions src/appsignal/check_in.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from __future__ import annotations

from binascii import hexlify
from os import urandom
from time import time
from typing import Any, Callable, Literal, TypedDict, TypeVar, Union

from . import internal_logger as logger
from .client import Client
from .config import Config
from .transmitter import transmit


T = TypeVar("T")

EventKind = Union[Literal["start"], Literal["finish"]]


class Event(TypedDict):
identifier: str
digest: str
kind: EventKind
timestamp: int
check_in_type: Literal["cron"]


class Cron:
identifier: str
digest: str

def __init__(self, identifier: str) -> None:
self.identifier = identifier
self.digest = hexlify(urandom(8)).decode("utf-8")

def _event(self, kind: EventKind) -> Event:
return Event(
identifier=self.identifier,
digest=self.digest,
kind=kind,
timestamp=int(time()),
check_in_type="cron",
)

def _transmit(self, event: Event) -> None:
config = Client.config() or Config()

if not config.is_active():
logger.debug("AppSignal not active, not transmitting cron check-in event")
return

url = f"{config.option('logging_endpoint')}/check_ins/json"
try:
response = transmit(url, json=event)
if 200 <= response.status_code <= 299:
logger.debug(
f"Transmitted cron check-in `{event['identifier']}` "
f"({event['digest']}) {event['kind']} event"
)
else:
logger.error(
f"Failed to transmit cron check-in {event['kind']} event: "
f"status code was {response.status_code}"
)
except Exception as e:
logger.error(f"Failed to transmit cron check-in {event['kind']} event: {e}")

def start(self) -> None:
self._transmit(self._event("start"))

def finish(self) -> None:
self._transmit(self._event("finish"))

def __enter__(self) -> None:
self.start()

def __exit__(
self, exc_type: Any = None, exc_value: Any = None, traceback: Any = None
) -> Literal[False]:
if exc_type is None:
self.finish()

return False


def cron(identifier: str, fn: Callable[[], T] | None = None) -> None | T:
cron = Cron(identifier)
output = None

if fn is not None:
cron.start()
output = fn()

cron.finish()
return output
107 changes: 42 additions & 65 deletions src/appsignal/heartbeat.py
Original file line number Diff line number Diff line change
@@ -1,87 +1,64 @@
from __future__ import annotations

from binascii import hexlify
from os import urandom
from time import time
from typing import Any, Callable, Literal, TypedDict, TypeVar, Union
from typing import Any, Callable, TypeVar

from . import internal_logger as logger
from .client import Client
from .config import Config
from .transmitter import transmit
from .check_in import Cron, cron


T = TypeVar("T")

EventKind = Union[Literal["start"], Literal["finish"]]

class _Once:
def __init__(self, func: Callable[..., None], *args: Any, **kwargs: Any) -> None:
self.called = False
self.func = func
self.args = args
self.kwargs = kwargs

class Event(TypedDict):
name: str
id: str
kind: EventKind
timestamp: int
def __call__(self) -> None:
if not self.called:
self.called = True
self.func(*self.args, **self.kwargs)

def reset(self) -> None:
self.called = False

class Heartbeat:
name: str
id: str

def __init__(self, name: str) -> None:
self.name = name
self.id = hexlify(urandom(8)).decode("utf-8")
def _warn_logger_and_stdout(msg: str) -> None:
logger.warning(msg)
print(f"appsignal WARNING: {msg}")

def _event(self, kind: EventKind) -> Event:
return Event(name=self.name, id=self.id, kind=kind, timestamp=int(time()))

def _transmit(self, event: Event) -> None:
config = Client.config() or Config()
_heartbeat_helper_warning = _Once(
_warn_logger_and_stdout,
"The helper `heartbeat` has been deprecated. "
"Please update uses of the helper `heartbeat(...)` to `cron(...)`, "
"importing it as `from appsignal.check_in import cron`, "
"in order to remove this message.",
)

if not config.is_active():
logger.debug("AppSignal not active, not transmitting heartbeat event")
return
_heartbeat_class_warning = _Once(
_warn_logger_and_stdout,
"The class `Heartbeat` has been deprecated. "
"Please update uses of the class `Heartbeat(...)` to `Cron(...)`, "
"importing it as `from appsignal.check_in import Cron`, "
"in order to remove this message.",
)

url = f"{config.option('logging_endpoint')}/heartbeats/json"
try:
response = transmit(url, json=event)
if 200 <= response.status_code <= 299:
logger.debug(
f"Transmitted heartbeat `{event['name']}` ({event['id']}) "
f"{event['kind']} event"
)
else:
logger.error(
"Failed to transmit heartbeat event: "
f"status code was {response.status_code}"
)
except Exception as e:
logger.error(f"Failed to transmit heartbeat event: {e}")

def start(self) -> None:
self._transmit(self._event("start"))

def finish(self) -> None:
self._transmit(self._event("finish"))

def __enter__(self) -> None:
self.start()

def __exit__(
self, exc_type: Any = None, exc_value: Any = None, traceback: Any = None
) -> Literal[False]:
if exc_type is None:
self.finish()

return False
def heartbeat(name: str, fn: Callable[[], T] | None = None) -> None | T:
_heartbeat_helper_warning()
return cron(name, fn)


def heartbeat(name: str, fn: Callable[[], T] | None = None) -> None | T:
heartbeat = Heartbeat(name)
output = None
class _MetaHeartbeat(type):
def __instancecheck__(cls, other: Any) -> bool:
_heartbeat_class_warning()
return isinstance(other, Cron)

if fn is not None:
heartbeat.start()
output = fn()

heartbeat.finish()
return output
class Heartbeat(metaclass=_MetaHeartbeat):
def __new__(cls, name: str) -> Cron: # type: ignore[misc]
_heartbeat_class_warning()
return Cron(name)
Loading
Loading