From a1fe578781e060aa4f0859199a8b6ce37126d73a Mon Sep 17 00:00:00 2001 From: demo-bot Date: Sun, 9 Aug 2026 15:57:46 +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. --- app/main.py | 170 ++++++++++++++++++++++------------------------------ 1 file changed, 71 insertions(+), 99 deletions(-) diff --git a/app/main.py b/app/main.py index 1abc174..e38f135 100644 --- a/app/main.py +++ b/app/main.py @@ -1,107 +1,79 @@ -import logging -from contextlib import asynccontextmanager from uuid import UUID from fastapi import FastAPI, HTTPException, Response, status -from .checker import check_url -from .config import get_settings -from .logging import log_event -from .models import CheckResult, CheckSnapshot, Monitor, MonitorInput -from .security import UnsafeDestination -from .store import MonitorStore, StoreFullError - -settings = get_settings() -logging.basicConfig(level=settings.log_level, format="%(message)s") -logger = logging.getLogger("endpoint_monitor.api") +from app.checker import EndpointChecker +from app.config import Settings, get_settings +from app.logging_config import configure_logging +from app.models import Health, Monitor, MonitorCreate, MonitorUpdate, StatusSnapshot +from app.store import MonitorStore -@asynccontextmanager -async def lifespan(app: FastAPI): # type: ignore[no-untyped-def] - app.state.store = MonitorStore(settings.max_monitors) - log_event(logger, "service_started", storage="process-local-memory") - yield +def create_app( + settings: Settings | None = None, store: MonitorStore | None = None, + checker: EndpointChecker | None = None, +) -> FastAPI: + config = settings or get_settings() + configure_logging(config.log_level) + state = store or MonitorStore() + service = checker or EndpointChecker(config) + api = FastAPI(title=config.app_name, version="1.0.0") + + async def existing(monitor_id: UUID) -> Monitor: + monitor = await state.get(monitor_id) + if monitor is None: + raise HTTPException(status_code=404, detail="monitor not found") + return monitor + + @api.post("/monitors", response_model=Monitor, status_code=201) + async def create_monitor(body: MonitorCreate) -> Monitor: + return await state.create(body) + + @api.get("/monitors", response_model=list[Monitor]) + async def list_monitors() -> list[Monitor]: + return await state.list() + + @api.get("/monitors/{monitor_id}", response_model=Monitor) + async def get_monitor(monitor_id: UUID) -> Monitor: + return await existing(monitor_id) + + @api.put("/monitors/{monitor_id}", response_model=Monitor) + async def update_monitor(monitor_id: UUID, body: MonitorUpdate) -> Monitor: + monitor = await state.update(monitor_id, body) + if monitor is None: + raise HTTPException(status_code=404, detail="monitor not found") + return monitor + + @api.delete("/monitors/{monitor_id}", status_code=status.HTTP_204_NO_CONTENT) + async def delete_monitor(monitor_id: UUID) -> Response: + if not await state.delete(monitor_id): + raise HTTPException(status_code=404, detail="monitor not found") + return Response(status_code=204) + + @api.post("/monitors/{monitor_id}/check", response_model=StatusSnapshot) + async def check_monitor(monitor_id: UUID) -> StatusSnapshot: + pending = await state.begin_check(monitor_id) + if pending is None: + raise HTTPException(status_code=404, detail="monitor not found") + monitor, revision = pending + result = await service.check(str(monitor.id), str(monitor.url)) + await state.finish_check(monitor_id, revision, result) + return result + + @api.get("/monitors/{monitor_id}/status", response_model=StatusSnapshot) + async def current_status(monitor_id: UUID) -> StatusSnapshot: + return (await existing(monitor_id)).status + + @api.get("/health/live", response_model=Health) + async def liveness() -> Health: + return Health(status="ok") + + @api.get("/health/ready", response_model=Health) + async def readiness() -> Health: + await state.list() + return Health(status="ready") + + return api -app = FastAPI(title="Endpoint Monitor", version="0.1.0", lifespan=lifespan) - - -def store() -> MonitorStore: - return app.state.store - - -def missing() -> HTTPException: - return HTTPException(status_code=404, detail="monitor not found") - - -@app.get("/healthz") -async def health() -> dict[str, str]: - return {"status": "ok"} - - -@app.get("/readyz") -async def ready() -> dict[str, str]: - return {"status": "ready", "storage": "process-local-memory"} - - -@app.post("/monitors", response_model=Monitor, status_code=201) -async def create_monitor(data: MonitorInput) -> Monitor: - try: - return await store().create(data) - except StoreFullError as exc: - raise HTTPException(status_code=503, detail="monitor capacity reached") from exc - - -@app.get("/monitors", response_model=list[Monitor]) -async def list_monitors() -> list[Monitor]: - return await store().list() - - -@app.get("/monitors/{monitor_id}", response_model=Monitor) -async def get_monitor(monitor_id: UUID) -> Monitor: - item = await store().get(monitor_id) - if item is None: - raise missing() - return item - - -@app.put("/monitors/{monitor_id}", response_model=Monitor) -async def replace_monitor(monitor_id: UUID, data: MonitorInput) -> Monitor: - item = await store().replace(monitor_id, data) - if item is None: - raise missing() - return item - - -@app.delete("/monitors/{monitor_id}", status_code=204) -async def delete_monitor(monitor_id: UUID) -> Response: - if not await store().delete(monitor_id): - raise missing() - return Response(status_code=status.HTTP_204_NO_CONTENT) - - -@app.get("/monitors/{monitor_id}/status", response_model=CheckSnapshot) -async def current_status(monitor_id: UUID) -> CheckSnapshot: - item = await store().get(monitor_id) - if item is None: - raise missing() - return item.status - - -@app.post("/monitors/{monitor_id}/check", response_model=CheckResult) -async def check_monitor(monitor_id: UUID) -> CheckResult: - item = await store().get(monitor_id) - if item is None: - raise missing() - timeout = item.timeout_seconds or settings.request_timeout_seconds - try: - snapshot = await check_url(str(item.url), timeout, settings) - except UnsafeDestination as exc: - snapshot = CheckSnapshot(state="error", checked_at=__import__( - "datetime").datetime.now(__import__("datetime").UTC), error=str(exc)) - await store().set_status(item.id, item.revision, snapshot) - raise HTTPException(status_code=400, detail=str(exc)) from exc - updated = await store().set_status(item.id, item.revision, snapshot) - if updated is None: - raise HTTPException(status_code=409, detail="monitor changed during check") - return CheckResult(monitor_id=item.id, **snapshot.model_dump()) +app = create_app()