diff --git a/backend/app/models.py b/backend/app/models.py index 425ef59..efd9400 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -14,9 +14,6 @@ class User(Base): password_hash: Mapped[str] = mapped_column(String(255), nullable=False) role: Mapped[str] = mapped_column(String(20), nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) - alert_delivery_status: Mapped[str | None] = mapped_column(String(30), nullable=True) - alert_delivered_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - alert_error_message: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) last_login_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) @@ -124,4 +121,7 @@ class WatchRun(Base): status: Mapped[str] = mapped_column(String(20), nullable=False) changed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) summary: Mapped[str] = mapped_column(Text, nullable=False) + alert_delivery_status: Mapped[str | None] = mapped_column(String(30), nullable=True) + alert_delivered_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + alert_error_message: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) diff --git a/backend/app/services/alerting.py b/backend/app/services/alerting.py index 3fd4dd7..728edd7 100644 --- a/backend/app/services/alerting.py +++ b/backend/app/services/alerting.py @@ -1,9 +1,20 @@ -import httpx -import hmac, hashlib, json from datetime import datetime, timezone +import hashlib +import hmac +import json + +import httpx + from app.config import settings -def send_watch_webhook(payload: dict) -> tuple[str,str|None]: + +MAX_ALERT_ERROR_MESSAGE_LENGTH = 500 + +def _trim_error_message(exc: Exception) -> str: + return str(exc).strip()[:MAX_ALERT_ERROR_MESSAGE_LENGTH] + + +def send_watch_webhook(payload: dict) -> tuple[str, str | None]: if not settings.alert_webhook_enabled: return 'skipped_disabled', None if not settings.alert_webhook_url: @@ -14,8 +25,18 @@ def send_watch_webhook(payload: dict) -> tuple[str,str|None]: if settings.alert_webhook_secret: sig=hmac.new(settings.alert_webhook_secret.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest() headers['X-RouteForge-Signature']=f'sha256={sig}' - try: - httpx.post(settings.alert_webhook_url,data=body,headers={**headers,'Content-Type':'application/json'},timeout=settings.alert_webhook_timeout_seconds) - return 'sent', None - except Exception as exc: - return 'failed', str(exc) + last_error: Exception | None = None + attempts = max(1, settings.alert_webhook_max_retries + 1) + for _ in range(attempts): + try: + response = httpx.post( + settings.alert_webhook_url, + data=body, + headers={**headers, 'Content-Type': 'application/json'}, + timeout=settings.alert_webhook_timeout_seconds, + ) + response.raise_for_status() + return 'sent', None + except Exception as exc: + last_error = exc + return 'failed', _trim_error_message(last_error) if last_error else 'webhook delivery failed'