103 lines
4.5 KiB
Python
103 lines
4.5 KiB
Python
from collections.abc import AsyncIterator
|
|
from contextlib import asynccontextmanager
|
|
from typing import cast
|
|
from uuid import UUID
|
|
|
|
from fastapi import FastAPI, HTTPException, Request, Response, status
|
|
|
|
from app.checker import EndpointChecker
|
|
from app.config import Settings, get_settings
|
|
from app.logging_config import configure_logging
|
|
from app.models import CurrentStatus, Health, Monitor, MonitorCreate, MonitorUpdate
|
|
from app.security import UnsafeTarget
|
|
from app.store import MonitorNotFound, MonitorStore
|
|
|
|
|
|
def create_app(settings: Settings | None = None, checker: EndpointChecker | None = None) -> FastAPI:
|
|
runtime_settings = settings or get_settings()
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
configure_logging(runtime_settings.log_level)
|
|
app.state.store = MonitorStore()
|
|
app.state.checker = checker or EndpointChecker(runtime_settings)
|
|
app.state.ready = True
|
|
yield
|
|
app.state.ready = False
|
|
|
|
app = FastAPI(title="Endpoint Monitor", version="1.0.0", lifespan=lifespan)
|
|
|
|
def store(request: Request) -> MonitorStore:
|
|
return cast(MonitorStore, request.app.state.store)
|
|
|
|
@app.post("/monitors", response_model=Monitor, status_code=status.HTTP_201_CREATED)
|
|
async def create_monitor(data: MonitorCreate, request: Request) -> Monitor:
|
|
return await store(request).create(data)
|
|
|
|
@app.get("/monitors", response_model=list[Monitor])
|
|
async def list_monitors(request: Request) -> list[Monitor]:
|
|
return await store(request).list()
|
|
|
|
@app.get("/monitors/{monitor_id}", response_model=Monitor)
|
|
async def get_monitor(monitor_id: UUID, request: Request) -> Monitor:
|
|
try:
|
|
return await store(request).get(monitor_id)
|
|
except MonitorNotFound as exc:
|
|
raise HTTPException(status_code=404, detail="monitor not found") from exc
|
|
|
|
@app.put("/monitors/{monitor_id}", response_model=Monitor)
|
|
async def update_monitor(monitor_id: UUID, data: MonitorUpdate, request: Request) -> Monitor:
|
|
try:
|
|
return await store(request).update(monitor_id, data)
|
|
except MonitorNotFound as exc:
|
|
raise HTTPException(status_code=404, detail="monitor not found") from exc
|
|
|
|
@app.delete("/monitors/{monitor_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_monitor(monitor_id: UUID, request: Request) -> Response:
|
|
try:
|
|
await store(request).delete(monitor_id)
|
|
except MonitorNotFound as exc:
|
|
raise HTTPException(status_code=404, detail="monitor not found") from exc
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
|
@app.post("/monitors/{monitor_id}/check", response_model=CurrentStatus)
|
|
async def check_monitor(monitor_id: UUID, request: Request) -> CurrentStatus:
|
|
monitor_store = store(request)
|
|
try:
|
|
monitor = await monitor_store.get(monitor_id)
|
|
except MonitorNotFound as exc:
|
|
raise HTTPException(status_code=404, detail="monitor not found") from exc
|
|
active_checker = cast(EndpointChecker, request.app.state.checker)
|
|
try:
|
|
result = await active_checker.check(str(monitor.id), str(monitor.target_url))
|
|
except UnsafeTarget as exc:
|
|
blocked = EndpointChecker._status(0 if False else __import__("time").monotonic(), monitor.current_status.state, error=None)
|
|
del blocked # Policy failures are mapped to 400 and do not overwrite the last completed status.
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
try:
|
|
return await monitor_store.set_status(monitor_id, result)
|
|
except MonitorNotFound as exc:
|
|
raise HTTPException(status_code=404, detail="monitor deleted while check was running") from exc
|
|
|
|
@app.get("/monitors/{monitor_id}/status", response_model=CurrentStatus)
|
|
async def get_monitor_status(monitor_id: UUID, request: Request) -> CurrentStatus:
|
|
try:
|
|
return (await store(request).get(monitor_id)).current_status
|
|
except MonitorNotFound as exc:
|
|
raise HTTPException(status_code=404, detail="monitor not found") from exc
|
|
|
|
@app.get("/health/live", response_model=Health)
|
|
async def liveness() -> Health:
|
|
return Health(status="ok")
|
|
|
|
@app.get("/health/ready", response_model=Health)
|
|
async def readiness(request: Request) -> Health:
|
|
if not getattr(request.app.state, "ready", False):
|
|
raise HTTPException(status_code=503, detail="not ready")
|
|
return Health(status="ready")
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|