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

This commit is contained in:
2026-08-09 15:46:50 +00:00
parent 0bf9f75bda
commit 29fcba7205

View File

@@ -1,3 +1,117 @@
from app.api import create_app
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from typing import Annotated, AsyncIterator
from uuid import UUID
app = create_app()
from fastapi import Depends, FastAPI, HTTPException, Response, status
from app.checker import EndpointChecker
from app.config import get_settings
from app.logging import configure_logging
from app.models import CurrentStatus, Monitor, MonitorCreate, MonitorUpdate, State
from app.security import DestinationRejected
from app.store import MonitorStore
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
settings = get_settings()
configure_logging(settings.log_level)
app.state.store = MonitorStore()
app.state.checker = EndpointChecker(settings)
app.state.ready = True
yield
app.state.ready = False
app = FastAPI(title="Endpoint Monitor", version="0.1.0", lifespan=lifespan)
def store() -> MonitorStore:
return app.state.store # type: ignore[no-any-return]
def checker() -> EndpointChecker:
return app.state.checker # type: ignore[no-any-return]
Store = Annotated[MonitorStore, Depends(store)]
Checker = Annotated[EndpointChecker, Depends(checker)]
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 readiness() -> dict[str, str]:
if not getattr(app.state, "ready", False):
raise HTTPException(status_code=503, detail="not ready")
return {"status": "ready"}
@app.post("/v1/monitors", response_model=Monitor, status_code=status.HTTP_201_CREATED)
async def create_monitor(payload: MonitorCreate, state: Store) -> Monitor:
return await state.create(payload)
@app.get("/v1/monitors", response_model=list[Monitor])
async def list_monitors(state: Store) -> list[Monitor]:
return await state.list()
@app.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
@app.patch("/v1/monitors/{monitor_id}", response_model=Monitor)
async def update_monitor(monitor_id: UUID, payload: MonitorUpdate, state: Store) -> Monitor:
if not payload.model_fields_set:
raise HTTPException(status_code=422, detail="at least one field is required")
item = await state.update(monitor_id, payload)
if item is None:
raise missing()
return item
@app.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)
@app.get("/v1/monitors/{monitor_id}/status", response_model=CurrentStatus)
async def current_status(monitor_id: UUID, state: Store) -> CurrentStatus:
item = await state.get(monitor_id)
if item is None:
raise missing()
return item.current_status
@app.post("/v1/monitors/{monitor_id}/check", response_model=CurrentStatus)
async def check_monitor(monitor_id: UUID, state: Store, worker: Checker) -> CurrentStatus:
item = await state.get(monitor_id)
if item is None:
raise missing()
try:
result = await worker.check(str(monitor_id), str(item.url))
except DestinationRejected as exc:
result = CurrentStatus(
state=State.ERROR, checked_at=datetime.now(UTC), error=str(exc)[:200]
)
if await state.set_status(monitor_id, result) is None:
raise missing() from exc
raise HTTPException(status_code=400, detail=str(exc)) from exc
if await state.set_status(monitor_id, result) is None:
raise missing()
return result