16932957b2
Adds ping and TCP monitor creation APIs, worker collectors, network checks UI, dashboard monitor status support, and progress documentation.
246 lines
7.7 KiB
Python
246 lines
7.7 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, PingMonitorCreate, TcpMonitorCreate, 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.post("/ping", response_model=MonitorRead)
|
|
def create_ping_monitor(
|
|
payload: PingMonitorCreate,
|
|
_: 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="host", address=payload.host, status="unknown", extra={})
|
|
db.add(asset)
|
|
db.flush()
|
|
asset_id = asset.id
|
|
|
|
monitor = Monitor(
|
|
asset_id=asset_id,
|
|
name=payload.name,
|
|
monitor_type="ping",
|
|
target=payload.host,
|
|
config={"timeout_seconds": payload.timeout_seconds},
|
|
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} ping 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.post("/tcp", response_model=MonitorRead)
|
|
def create_tcp_monitor(
|
|
payload: TcpMonitorCreate,
|
|
_: User = Depends(require_role("admin")),
|
|
db: Session = Depends(get_db),
|
|
) -> Monitor:
|
|
asset_id: int | None = None
|
|
target = f"{payload.host}:{payload.port}"
|
|
if payload.create_asset:
|
|
asset = Asset(name=payload.name, asset_type="tcp_service", address=target, status="unknown", extra={})
|
|
db.add(asset)
|
|
db.flush()
|
|
asset_id = asset.id
|
|
|
|
monitor = Monitor(
|
|
asset_id=asset_id,
|
|
name=payload.name,
|
|
monitor_type="tcp",
|
|
target=target,
|
|
config={"host": payload.host, "port": payload.port, "timeout_seconds": payload.timeout_seconds},
|
|
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} TCP connection 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_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_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 in {"website", "host", "tcp_service"}:
|
|
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()
|
|
)
|