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