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 are pending
ci / test (push) Has started running

This commit is contained in:
2026-08-09 16:00:54 +00:00
parent 79bd2939c3
commit 74f73a5866

View File

@@ -1,13 +1,13 @@
import asyncio import asyncio
from dataclasses import dataclass from dataclasses import dataclass
from datetime import UTC, datetime from datetime import datetime, timezone
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from app.models import Monitor, MonitorCreate, MonitorUpdate, StatusSnapshot from app.models import CheckStatus, Monitor, MonitorCreate, MonitorUpdate
@dataclass @dataclass(frozen=True)
class _Record: class Snapshot:
monitor: Monitor monitor: Monitor
revision: int revision: int
@@ -15,61 +15,54 @@ class _Record:
class MonitorStore: class MonitorStore:
def __init__(self) -> None: def __init__(self) -> None:
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
self._records: dict[UUID, _Record] = {} self._items: dict[UUID, tuple[Monitor, int]] = {}
async def create(self, data: MonitorCreate) -> Monitor: async def create(self, data: MonitorCreate) -> Monitor:
now = datetime.now(UTC) now = datetime.now(timezone.utc)
monitor = Monitor( item = Monitor(id=uuid4(), name=data.name, url=data.url, created_at=now,
id=uuid4(), name=data.name, url=data.url, created_at=now, updated_at=now, status=CheckStatus())
updated_at=now, status=StatusSnapshot(),
)
async with self._lock: async with self._lock:
self._records[monitor.id] = _Record(monitor, 0) self._items[item.id] = (item, 1)
return monitor.model_copy(deep=True) return item.model_copy(deep=True)
async def list(self) -> list[Monitor]: async def list(self) -> list[Monitor]:
async with self._lock: async with self._lock:
values = [item.monitor for item in self._records.values()] values = [item.model_copy(deep=True) for item, _ in self._items.values()]
return [value.model_copy(deep=True) for value in values] return sorted(values, key=lambda item: (item.created_at, str(item.id)))
async def get(self, monitor_id: UUID) -> Monitor | None: async def snapshot(self, monitor_id: UUID) -> Snapshot | None:
async with self._lock: async with self._lock:
record = self._records.get(monitor_id) stored = self._items.get(monitor_id)
return record.monitor.model_copy(deep=True) if record else None if stored is None:
return None
item, revision = stored
return Snapshot(item.model_copy(deep=True), revision)
async def update(self, monitor_id: UUID, data: MonitorUpdate) -> Monitor | None: async def update(self, monitor_id: UUID, data: MonitorUpdate) -> Monitor | None:
async with self._lock: async with self._lock:
record = self._records.get(monitor_id) stored = self._items.get(monitor_id)
if not record: if stored is None:
return None return None
url_changed = record.monitor.url != data.url item, revision = stored
status = StatusSnapshot() if url_changed else record.monitor.status changes = data.model_dump(exclude_none=True)
record.monitor = record.monitor.model_copy(update={ if "url" in changes:
"name": data.name, "url": data.url, "updated_at": datetime.now(UTC), changes["status"] = CheckStatus()
"status": status, changes["updated_at"] = datetime.now(timezone.utc)
}) updated = item.model_copy(update=changes)
record.revision += 1 self._items[monitor_id] = (updated, revision + 1)
return record.monitor.model_copy(deep=True) return updated.model_copy(deep=True)
async def delete(self, monitor_id: UUID) -> bool: async def delete(self, monitor_id: UUID) -> bool:
async with self._lock: async with self._lock:
return self._records.pop(monitor_id, None) is not None return self._items.pop(monitor_id, None) is not None
async def begin_check(self, monitor_id: UUID) -> tuple[Monitor, int] | None: async def apply_status(self, monitor_id: UUID, revision: int,
status: CheckStatus) -> bool:
async with self._lock: async with self._lock:
record = self._records.get(monitor_id) stored = self._items.get(monitor_id)
if not record: if stored is None or stored[1] != revision:
return None
return record.monitor.model_copy(deep=True), record.revision
async def finish_check(
self, monitor_id: UUID, revision: int, status: StatusSnapshot,
) -> bool:
async with self._lock:
record = self._records.get(monitor_id)
if not record or record.revision != revision:
return False return False
record.monitor = record.monitor.model_copy(update={ item, current_revision = stored
"status": status, "updated_at": datetime.now(UTC), updated = item.model_copy(update={"status": status})
}) self._items[monitor_id] = (updated, current_revision)
return True return True