Skip to content
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: 3 additions & 3 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
37 changes: 29 additions & 8 deletions backend/app/services/alerting.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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'
Loading