From 625ad314e569e3438357a91b6b4db126c8291cc6 Mon Sep 17 00:00:00 2001 From: demo-bot Date: Sun, 9 Aug 2026 15:52:27 +0000 Subject: [PATCH] decomposer: generate deliverable files for Define the service contract and project architecture for the FastAPI endpoint monitoring service.; Implement the typed monitor CRUD API and concurrency-safe in-memory state according to the service design.; Implement secure on-demand endpoint checks with status updates, latency measurement, robust error handling, and redacted structured logs.; Add operational API endpoints and environment-driven runtime configuration to the monitoring service.; Create automated tests for the monitoring service.; Package the service with Docker and developer documentation.; Validate the complete project. --- src/endpoint_monitor/api.py | 96 +++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/endpoint_monitor/api.py diff --git a/src/endpoint_monitor/api.py b/src/endpoint_monitor/api.py new file mode 100644 index 0000000..e4806e6 --- /dev/null +++ b/src/endpoint_monitor/api.py @@ -0,0 +1,96 @@ +from uuid import UUID + +from fastapi import APIRouter, HTTPException, Request, Response, status + +from .checker import EndpointChecker, TargetBlockedError +from .models import CheckResult, Health, Monitor, MonitorCreate, MonitorUpdate, StatusView +from .store import Conflict, MonitorStore, NotFound + +router = APIRouter() + + +def store(request: Request) -> MonitorStore: + return request.app.state.store + + +def checker(request: Request) -> EndpointChecker: + return request.app.state.checker + + +@router.get("/healthz", response_model=Health, tags=["operations"]) +async def health() -> Health: + return Health(status="ok") + + +@router.get("/readyz", response_model=Health, tags=["operations"]) +async def ready(request: Request) -> Health: + if not hasattr(request.app.state, "store"): + raise HTTPException(status_code=503, detail="store unavailable") + return Health(status="ready") + + +@router.post("/v1/monitors", response_model=Monitor, status_code=201, tags=["monitors"]) +async def create(data: MonitorCreate, request: Request) -> Monitor: + try: + return await store(request).create(data) + except Conflict as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + + +@router.get("/v1/monitors", response_model=list[Monitor], tags=["monitors"]) +async def list_monitors(request: Request) -> list[Monitor]: + return await store(request).list() + + +@router.get("/v1/monitors/{monitor_id}", response_model=Monitor, tags=["monitors"]) +async def get_monitor(monitor_id: UUID, request: Request) -> Monitor: + try: + return await store(request).get(monitor_id) + except NotFound as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@router.patch("/v1/monitors/{monitor_id}", response_model=Monitor, tags=["monitors"]) +async def update_monitor(monitor_id: UUID, data: MonitorUpdate, request: Request) -> Monitor: + try: + return await store(request).update(monitor_id, data) + except NotFound as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except Conflict as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + + +@router.delete("/v1/monitors/{monitor_id}", status_code=204, tags=["monitors"]) +async def delete_monitor(monitor_id: UUID, request: Request) -> Response: + try: + await store(request).delete(monitor_id) + except NotFound as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.post("/v1/monitors/{monitor_id}/check", response_model=CheckResult, tags=["checks"]) +async def check_monitor(monitor_id: UUID, request: Request) -> CheckResult: + try: + item = await store(request).get(monitor_id) + result = await checker(request).check(str(item.url), str(monitor_id)) + await store(request).record_check(monitor_id, result) + return result + except NotFound as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except TargetBlockedError as exc: + result = CheckResult(status="error", checked_at=__import__("datetime").datetime.now(__import__("datetime").UTC), latency_ms=0, final_url=None, detail=str(exc)) + try: + await store(request).record_check(monitor_id, result) + except NotFound as missing: + raise HTTPException(status_code=404, detail=str(missing)) from missing + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@router.get("/v1/monitors/{monitor_id}/status", response_model=StatusView, tags=["checks"]) +async def monitor_status(monitor_id: UUID, request: Request) -> StatusView: + try: + item = await store(request).get(monitor_id) + except NotFound as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + return StatusView(id=item.id, status=item.status, last_check=item.last_check)