3b75075426
Add optional TLS certificate expiry checks for website monitors and update product, package, environment, Docker, and documentation naming.
159 lines
5.1 KiB
Python
159 lines
5.1 KiB
Python
from datetime import UTC, datetime
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.auth.dependencies import get_current_user, require_role
|
|
from app.db.session import get_db
|
|
from app.models import AlertRule, Asset, CheckResult, Incident, Monitor, User
|
|
from app.schemas.core import CheckResultRead, MonitorCreate, MonitorRead, MonitorUpdate, WebsiteMonitorCreate
|
|
|
|
router = APIRouter(prefix="/monitors", tags=["monitors"])
|
|
|
|
|
|
@router.get("", response_model=list[MonitorRead])
|
|
def list_monitors(_: User = Depends(get_current_user), db: Session = Depends(get_db)) -> list[Monitor]:
|
|
return list(db.scalars(select(Monitor).order_by(Monitor.name)).all())
|
|
|
|
|
|
@router.post("", response_model=MonitorRead)
|
|
def create_monitor(
|
|
payload: MonitorCreate,
|
|
_: User = Depends(require_role("admin")),
|
|
db: Session = Depends(get_db),
|
|
) -> Monitor:
|
|
monitor = Monitor(**payload.model_dump())
|
|
db.add(monitor)
|
|
db.commit()
|
|
db.refresh(monitor)
|
|
return monitor
|
|
|
|
|
|
@router.post("/website", response_model=MonitorRead)
|
|
def create_website_monitor(
|
|
payload: WebsiteMonitorCreate,
|
|
_: User = Depends(require_role("admin")),
|
|
db: Session = Depends(get_db),
|
|
) -> Monitor:
|
|
asset_id: int | None = None
|
|
if payload.create_asset:
|
|
asset = Asset(name=payload.name, asset_type="website", address=payload.url, status="unknown", extra={})
|
|
db.add(asset)
|
|
db.flush()
|
|
asset_id = asset.id
|
|
|
|
monitor = Monitor(
|
|
asset_id=asset_id,
|
|
name=payload.name,
|
|
monitor_type="http",
|
|
target=payload.url,
|
|
config={
|
|
"expected_status": payload.expected_status,
|
|
"expected_text": payload.expected_text,
|
|
"unexpected_text": payload.unexpected_text,
|
|
"timeout_seconds": payload.timeout_seconds,
|
|
"check_tls_expiry": payload.check_tls_expiry,
|
|
"tls_warning_days": payload.tls_warning_days,
|
|
},
|
|
interval_seconds=payload.interval_seconds,
|
|
status="unknown",
|
|
)
|
|
db.add(monitor)
|
|
db.flush()
|
|
|
|
if payload.alert_enabled:
|
|
db.add(
|
|
AlertRule(
|
|
monitor_id=monitor.id,
|
|
name=f"{payload.name} website failure",
|
|
severity=payload.alert_severity,
|
|
condition={"type": "status_not_up"},
|
|
failure_threshold=payload.failure_threshold,
|
|
cooldown_seconds=300,
|
|
is_enabled=True,
|
|
)
|
|
)
|
|
|
|
db.commit()
|
|
db.refresh(monitor)
|
|
return monitor
|
|
|
|
|
|
@router.get("/{monitor_id}", response_model=MonitorRead)
|
|
def get_monitor(monitor_id: int, _: User = Depends(get_current_user), db: Session = Depends(get_db)) -> Monitor:
|
|
monitor = db.get(Monitor, monitor_id)
|
|
if monitor is None:
|
|
raise HTTPException(status_code=404, detail="Monitor not found")
|
|
return monitor
|
|
|
|
|
|
@router.patch("/{monitor_id}", response_model=MonitorRead)
|
|
def update_monitor(
|
|
monitor_id: int,
|
|
payload: MonitorUpdate,
|
|
_: User = Depends(require_role("admin")),
|
|
db: Session = Depends(get_db),
|
|
) -> Monitor:
|
|
monitor = db.get(Monitor, monitor_id)
|
|
if monitor is None:
|
|
raise HTTPException(status_code=404, detail="Monitor not found")
|
|
|
|
for field, value in payload.model_dump(exclude_unset=True).items():
|
|
setattr(monitor, field, value)
|
|
|
|
db.commit()
|
|
db.refresh(monitor)
|
|
return monitor
|
|
|
|
|
|
@router.delete("/{monitor_id}", status_code=204)
|
|
def delete_monitor(
|
|
monitor_id: int,
|
|
cleanup_orphan_website_asset: bool = True,
|
|
_: User = Depends(require_role("admin")),
|
|
db: Session = Depends(get_db),
|
|
) -> None:
|
|
monitor = db.get(Monitor, monitor_id)
|
|
if monitor is None:
|
|
raise HTTPException(status_code=404, detail="Monitor not found")
|
|
|
|
asset_id = monitor.asset_id
|
|
now = datetime.now(UTC)
|
|
open_incidents = db.scalars(select(Incident).where(Incident.monitor_id == monitor_id, Incident.status == "open")).all()
|
|
for incident in open_incidents:
|
|
incident.status = "resolved"
|
|
incident.resolved_at = now
|
|
incident.details = {**(incident.details or {}), "resolution_reason": "monitor_deleted"}
|
|
|
|
db.delete(monitor)
|
|
db.flush()
|
|
|
|
if cleanup_orphan_website_asset and asset_id is not None:
|
|
remaining = db.scalar(select(func.count(Monitor.id)).where(Monitor.asset_id == asset_id))
|
|
asset = db.get(Asset, asset_id)
|
|
if remaining == 0 and asset is not None and asset.asset_type == "website":
|
|
db.delete(asset)
|
|
|
|
db.commit()
|
|
|
|
|
|
@router.get("/{monitor_id}/results", response_model=list[CheckResultRead])
|
|
def list_monitor_results(
|
|
monitor_id: int,
|
|
limit: int = 20,
|
|
_: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db),
|
|
) -> list[CheckResult]:
|
|
monitor = db.get(Monitor, monitor_id)
|
|
if monitor is None:
|
|
raise HTTPException(status_code=404, detail="Monitor not found")
|
|
return list(
|
|
db.scalars(
|
|
select(CheckResult)
|
|
.where(CheckResult.monitor_id == monitor_id)
|
|
.order_by(CheckResult.observed_at.desc())
|
|
.limit(min(limit, 100))
|
|
).all()
|
|
)
|