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.
Some checks failed
ci / validate (push) Has been cancelled
Some checks failed
ci / validate (push) Has been cancelled
This commit is contained in:
96
src/endpoint_monitor/api.py
Normal file
96
src/endpoint_monitor/api.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user