Files
NexaPG/backend/app/services/alert_notifications.py
nessi ea26ef4d33
All checks were successful
PostgreSQL Compatibility Matrix / PG14 smoke (push) Successful in 8s
PostgreSQL Compatibility Matrix / PG15 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG16 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG17 smoke (push) Successful in 6s
PostgreSQL Compatibility Matrix / PG18 smoke (push) Successful in 6s
Add target owners and alert notification management.
This commit implements the addition of `target_owners` and `alert_notification_events` tables, enabling management of responsible users for targets. Backend and frontend components are updated to allow viewing, assigning, and notifying target owners about critical alerts via email.
2026-02-12 15:22:32 +01:00

161 lines
5.2 KiB
Python

from __future__ import annotations
import asyncio
from datetime import datetime, timedelta, timezone
from email.message import EmailMessage
import smtplib
import ssl
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.models import AlertNotificationEvent, EmailNotificationSettings, TargetOwner, User
from app.schemas.alert import AlertStatusResponse
from app.services.crypto import decrypt_secret
_NOTIFICATION_COOLDOWN = timedelta(minutes=30)
async def _smtp_send(
host: str,
port: int,
username: str | None,
password: str | None,
from_email: str,
recipient: str,
subject: str,
body: str,
use_starttls: bool,
use_ssl: bool,
) -> None:
def _send() -> None:
message = EmailMessage()
message["From"] = from_email
message["To"] = recipient
message["Subject"] = subject
message.set_content(body)
if use_ssl:
with smtplib.SMTP_SSL(host, port, timeout=10, context=ssl.create_default_context()) as smtp:
if username:
smtp.login(username, password or "")
smtp.send_message(message)
return
with smtplib.SMTP(host, port, timeout=10) as smtp:
if use_starttls:
smtp.starttls(context=ssl.create_default_context())
if username:
smtp.login(username, password or "")
smtp.send_message(message)
await asyncio.to_thread(_send)
def _render_subject(item) -> str:
sev = item.severity.upper()
return f"[NexaPG][{sev}] {item.target_name} - {item.name}"
def _render_body(item) -> str:
lines = [
f"Severity: {item.severity}",
f"Target: {item.target_name} (id={item.target_id})",
f"Alert: {item.name}",
f"Category: {item.category}",
f"Checked At: {item.checked_at.isoformat()}",
"",
f"Description: {item.description}",
f"Message: {item.message}",
]
if item.value is not None:
lines.append(f"Current Value: {item.value}")
if item.warning_threshold is not None:
lines.append(f"Warning Threshold: {item.warning_threshold}")
if item.alert_threshold is not None:
lines.append(f"Alert Threshold: {item.alert_threshold}")
lines.append("")
lines.append(f"Alert Key: {item.alert_key}")
lines.append("Sent by NexaPG notification service.")
return "\n".join(lines)
async def process_target_owner_notifications(db: AsyncSession, status: AlertStatusResponse) -> None:
settings = await db.scalar(select(EmailNotificationSettings).limit(1))
if not settings or not settings.enabled:
return
if not settings.smtp_host or not settings.from_email:
return
password = decrypt_secret(settings.encrypted_smtp_password) if settings.encrypted_smtp_password else None
now = datetime.now(timezone.utc)
active_items = status.alerts + status.warnings
if not active_items:
return
target_ids = sorted({item.target_id for item in active_items})
owner_rows = (
await db.execute(
select(TargetOwner.target_id, User.email)
.join(User, User.id == TargetOwner.user_id)
.where(TargetOwner.target_id.in_(target_ids))
)
).all()
owners_map: dict[int, set[str]] = {}
for target_id, email in owner_rows:
owners_map.setdefault(target_id, set()).add(email)
existing_rows = (
await db.scalars(
select(AlertNotificationEvent).where(
AlertNotificationEvent.target_id.in_(target_ids)
)
)
).all()
event_map = {(row.alert_key, row.target_id, row.severity): row for row in existing_rows}
for item in active_items:
recipients = sorted(owners_map.get(item.target_id, set()))
if not recipients:
continue
key = (item.alert_key, item.target_id, item.severity)
existing = event_map.get(key)
should_send = existing is None or (now - existing.last_sent_at) >= _NOTIFICATION_COOLDOWN
if should_send:
subject = _render_subject(item)
body = _render_body(item)
for recipient in recipients:
try:
await _smtp_send(
host=settings.smtp_host,
port=settings.smtp_port,
username=settings.smtp_username,
password=password,
from_email=settings.from_email,
recipient=recipient,
subject=subject,
body=body,
use_starttls=settings.use_starttls,
use_ssl=settings.use_ssl,
)
except Exception:
continue
if existing:
existing.last_seen_at = now
if should_send:
existing.last_sent_at = now
else:
db.add(
AlertNotificationEvent(
alert_key=item.alert_key,
target_id=item.target_id,
severity=item.severity,
last_seen_at=now,
last_sent_at=now if should_send else now,
)
)
await db.commit()