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.

This commit is contained in:
2026-08-09 15:35:38 +00:00
parent 0205ca46be
commit cfef40c643

92
app/api.py Normal file
View File

@@ -0,0 +1,92 @@
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from app.checker import EndpointChecker
from app.models import CurrentStatus, Monitor, MonitorCreate, MonitorUpdate
from app.store import MonitorStore
router = APIRouter()
def store(request: Request) -> MonitorStore:
return request.app.state.store
def checker(request: Request) -> EndpointChecker:
return request.app.state.checker
Store = Annotated[MonitorStore, Depends(store)]
Checker = Annotated[EndpointChecker, Depends(checker)]
def missing() -> HTTPException:
return HTTPException(status_code=404, detail=("monitor_not_found", "Monitor not found"))
@router.post("/v1/monitors", response_model=Monitor, status_code=status.HTTP_201_CREATED)
async def create_monitor(data: MonitorCreate, state: Store) -> Monitor:
return await state.create(data)
@router.get("/v1/monitors", response_model=list[Monitor])
async def list_monitors(state: Store) -> list[Monitor]:
return await state.list()
@router.get("/v1/monitors/{monitor_id}", response_model=Monitor)
async def get_monitor(monitor_id: UUID, state: Store) -> Monitor:
item = await state.get(monitor_id)
if item is None:
raise missing()
return item
@router.patch("/v1/monitors/{monitor_id}", response_model=Monitor)
async def update_monitor(monitor_id: UUID, data: MonitorUpdate, state: Store) -> Monitor:
item = await state.update(monitor_id, data)
if item is None:
raise missing()
return item
@router.delete("/v1/monitors/{monitor_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_monitor(monitor_id: UUID, state: Store) -> Response:
if not await state.delete(monitor_id):
raise missing()
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.post("/v1/monitors/{monitor_id}/check", response_model=CurrentStatus)
async def check_monitor(monitor_id: UUID, state: Store, outbound: Checker) -> CurrentStatus:
snapshot = await state.snapshot(monitor_id)
if snapshot is None:
raise missing()
item, revision = snapshot
result = await outbound.check(str(item.url))
if await state.record_status(monitor_id, revision, result) is None:
raise HTTPException(status_code=409,
detail=("monitor_changed", "Monitor changed while check was running"))
return result
@router.get("/v1/monitors/{monitor_id}/status", response_model=CurrentStatus)
async def get_status(monitor_id: UUID, state: Store) -> CurrentStatus:
item = await state.get(monitor_id)
if item is None:
raise missing()
return item.current_status
@router.get("/healthz")
async def health() -> dict[str, str]:
return {"status": "ok"}
@router.get("/readyz")
async def readiness(request: Request) -> dict[str, str]:
if not hasattr(request.app.state, "store"):
raise HTTPException(status_code=503, detail=("not_ready", "Service is not ready"))
return {"status": "ready", "storage": "process-local-memory"}