from contextlib import asynccontextmanager from uuid import UUID import httpx from fastapi import FastAPI, HTTPException, Response, status from .checker import EndpointChecker from .config import get_settings from .logging_config import configure_logging from .models import Monitor, MonitorCreate, MonitorUpdate, ProbeResponse, StatusResponse from .store import MonitorStore settings = get_settings() configure_logging(settings.log_level) @asynccontextmanager async def lifespan(app: FastAPI): app.state.store = MonitorStore() app.state.ready = True async with httpx.AsyncClient() as client: app.state.checker = EndpointChecker(client, settings.request_timeout_seconds, settings.max_redirects, settings.max_response_bytes) yield app.state.ready = False app = FastAPI(title=settings.app_name, version="1.0.0", lifespan=lifespan) def missing() -> HTTPException: return HTTPException(status_code=404, detail="monitor not found") @app.post("/api/v1/monitors", response_model=Monitor, status_code=status.HTTP_201_CREATED) async def create_monitor(data: MonitorCreate) -> Monitor: return await app.state.store.create(data) @app.get("/api/v1/monitors", response_model=list[Monitor]) async def list_monitors() -> list[Monitor]: return await app.state.store.list() @app.get("/api/v1/monitors/{monitor_id}", response_model=Monitor) async def get_monitor(monitor_id: UUID) -> Monitor: item = await app.state.store.get(monitor_id) if item is None: raise missing() return item @app.put("/api/v1/monitors/{monitor_id}", response_model=Monitor) async def update_monitor(monitor_id: UUID, data: MonitorUpdate) -> Monitor: item = await app.state.store.update(monitor_id, data) if item is None: raise missing() return item @app.delete("/api/v1/monitors/{monitor_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_monitor(monitor_id: UUID) -> Response: if not await app.state.store.delete(monitor_id): raise missing() return Response(status_code=status.HTTP_204_NO_CONTENT) @app.post("/api/v1/monitors/{monitor_id}/check", response_model=Monitor) async def check_monitor(monitor_id: UUID) -> Monitor: item = await app.state.store.get(monitor_id) if item is None: raise missing() result = await app.state.checker.check(str(monitor_id), str(item.url)) updated = await app.state.store.record(monitor_id, result) if updated is None: raise missing() return updated @app.get("/api/v1/monitors/{monitor_id}/status", response_model=StatusResponse) async def monitor_status(monitor_id: UUID) -> StatusResponse: item = await app.state.store.get(monitor_id) if item is None: raise missing() return StatusResponse(id=item.id, status=item.status, checked_at=item.checked_at or item.created_at, latency_ms=item.latency_ms or 0, http_status=item.http_status, error=item.error) @app.get("/healthz", response_model=ProbeResponse) async def health() -> ProbeResponse: return ProbeResponse(status="ok") @app.get("/readyz", response_model=ProbeResponse) async def ready() -> ProbeResponse: if not getattr(app.state, "ready", False): raise HTTPException(status_code=503, detail="not ready") return ProbeResponse(status="ready")