Task completion ping
Cron job, training run, ETL, nightly batch. You only want to hear about it when something is wrong — or when something interesting took longer than it should.
When to use
- A job runs for minutes/hours and you don’t want to tail logs to know if it finished.
- Failure has to surface with a traceback, not a silent non-zero exit.
- You want a duration budget enforced separately from “did it crash”.
The recipe
import functools
import time
from collections.abc import Callable
from typing import TypeVar, ParamSpec
import snitchbot
P = ParamSpec("P")
R = TypeVar("R")
snitchbot.init("nightly-etl")
def notify_completion(
name: str,
*,
budget_sec: float | None = None,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Notify on failure (always) and on overrun (if budget_sec is set)."""
def deco(fn: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
t0 = time.monotonic()
try:
return fn(*args, **kwargs)
except BaseException as exc:
snitchbot.notify(
f"{name} failed",
severity="error",
extras={"duration_s": round(time.monotonic() - t0, 1)},
exc_info=exc,
)
raise
finally:
elapsed = time.monotonic() - t0
if budget_sec is not None and elapsed > budget_sec:
snitchbot.notify(
f"{name} over budget",
severity="warning",
extras={
"duration_s": round(elapsed, 1),
"budget_s": budget_sec,
},
)
return wrapper
return deco
@notify_completion("warehouse refresh", budget_sec=15 * 60)
def refresh_warehouse() -> None:
...
Notes
- Failures use
severity="error"(🔴) so they pass anywarning-level mute. The traceback rides onexc_info=exc. - There is no
severity="success"— snitchbot only emitswarning,error,critical. If you genuinely want a “done OK” ping, sendnotify(f"{name} done", severity="warning")and accept the orange icon. Most teams find a no-news-is-good-news contract calmer. - For functions with a hard wall-clock SLO,
@watch_slow(threshold_ms=...)gives you the same overrun signal with sidecar-side dedup; seewatch_slow(). Combine the two when you need both fail-traceback and dedup. - The decorator works for sync functions only. For async tasks, replace
def wrapperwithasync def wrapperandawait fn(...).