goon/app/scheduler/hetzner_monitor.py
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

103 lines
3.7 KiB
Python

"""Hetzner Cloud bandwidth monitor, alert do Sentry zanim transfer przekroczy included.
Hetzner Cloud: ruch WYCHODZĄCY liczy się do `included_traffic` (przychodzący darmowy),
overage = €1/TB. Przy dystrybucji apki (fala instalacji + część playbacku przez
/proxy/<token>) transfer może rosnąć. Spec był w config.py od dawna, ale monitor nigdy
nie powstał, to jego implementacja.
Pyta Hetzner Cloud API o bieżący `outgoing_traffic` vs `included_traffic` i alarmuje do
Sentry przy progach (info/warning/error %, z config). Stabilny fingerprint per poziom →
jedno eskalujące issue, nie nowe co run. Bez `HETZNER_API_TOKEN`/`HETZNER_SERVER_ID` =
no-op (warning w logu raz). Wołane periodycznie przez scheduler (`_job_hetzner_monitor`).
"""
from __future__ import annotations
import logging
from typing import Any
import httpx
from app.config import get_settings
log = logging.getLogger(__name__)
_API = "https://api.hetzner.cloud/v1/servers/{id}"
def run_hetzner_bandwidth_check() -> dict[str, Any]:
"""Sprawdź % zużycia included_traffic i zaalarmuj Sentry przy przekroczeniu progu.
Zwraca {enabled, used_pct, outgoing_gb, included_gb, level} (level=None gdy poniżej
najniższego progu lub monitor wyłączony).
"""
s = get_settings()
token = s.hetzner_api_token
server_id = s.hetzner_server_id
if not token or not server_id:
log.info("hetzner-monitor: wyłączony (brak HETZNER_API_TOKEN/HETZNER_SERVER_ID)")
return {"enabled": False}
try:
r = httpx.get(
_API.format(id=server_id),
headers={"Authorization": f"Bearer {token}"},
timeout=20.0,
)
r.raise_for_status()
srv = r.json()["server"]
except Exception as e:
log.warning("hetzner-monitor: API fetch failed: %s", str(e)[:160])
return {"enabled": True, "error": str(e)[:160]}
included = srv.get("included_traffic") or 0
outgoing = srv.get("outgoing_traffic") or 0
if included <= 0:
log.warning("hetzner-monitor: included_traffic=0 — pomijam")
return {"enabled": True, "error": "included_traffic=0"}
used_pct = round(100.0 * outgoing / included, 1)
out_gb = round(outgoing / 1e9, 1)
inc_gb = round(included / 1e9, 1)
# Najwyższy przekroczony próg → poziom Sentry.
level: str | None = None
if used_pct >= s.hetzner_alert_error_pct:
level = "error"
elif used_pct >= s.hetzner_alert_warning_pct:
level = "warning"
elif used_pct >= s.hetzner_alert_info_pct:
level = "info"
result = {
"enabled": True,
"used_pct": used_pct,
"outgoing_gb": out_gb,
"included_gb": inc_gb,
"level": level,
}
log.info(
"hetzner-monitor: %s%% included (%sGB / %sGB out), level=%s",
used_pct, out_gb, inc_gb, level or "ok",
)
if level:
try:
import sentry_sdk
with sentry_sdk.push_scope() as scope:
scope.level = level
scope.set_tag("hetzner_server_id", str(server_id))
scope.set_extra("used_pct", used_pct)
scope.set_extra("outgoing_gb", out_gb)
scope.set_extra("included_gb", inc_gb)
# Fingerprint per POZIOM → eskalacja info→warning→error to osobne, trwałe
# issue (nie fragmentowane po zmiennym %).
scope.fingerprint = ["hetzner-traffic", level]
sentry_sdk.capture_message(
f"hetzner-monitor: transfer {used_pct}% included "
f"({out_gb}GB / {inc_gb}GB) — poziom {level}"
)
except Exception: # pragma: no cover - Sentry off / brak DSN
log.exception("hetzner-monitor: Sentry capture failed")
return result