# Endpoint Monitor service design ## Scope and lifecycle This repository contains one unauthenticated FastAPI service for defining endpoint monitors and running checks on demand. State is held in a lock-protected, process-local `MonitorStore`; it starts empty, is lost on restart, and is not shared between workers. Production deployments of this implementation must therefore use exactly one worker. Persistence and authentication are intentionally out of scope. ## Resource and routes A monitor has a server-generated UUID, name, HTTP(S) URL, creation/update timestamps, and a `current_status`. Status is one of `unknown`, `up`, `down`, or `error`. `unknown` has never been checked; `up` means the final response was 200–399; `down` means it was 400–599; `error` represents a timeout, transport failure, redirect-policy violation, or SSRF rejection. | Method | Path | Meaning | |---|---|---| | POST | `/v1/monitors` | Create (201) | | GET | `/v1/monitors` | List, ordered by creation time | | GET | `/v1/monitors/{id}` | Retrieve | | PATCH | `/v1/monitors/{id}` | Partially update name/URL | | DELETE | `/v1/monitors/{id}` | Delete (204) | | POST | `/v1/monitors/{id}/check` | Run an on-demand check and atomically store it | | GET | `/v1/monitors/{id}/status` | Retrieve current status | | GET | `/healthz` | Liveness | | GET | `/readyz` | Readiness and storage mode | Checks do not download response bodies. Redirects are followed explicitly up to the configured bound. Every hop is parsed, DNS-resolved, policy-checked, and connected through a resolver pinned to the approved addresses. If any answer is loopback, private, link-local, multicast, unspecified, reserved, or otherwise non-global, the hop is rejected. This conservative all-addresses rule and address pinning prevent mixed-answer and DNS-rebinding bypasses. Only HTTP and HTTPS URLs without credentials are accepted. The checker returns a status representation for expected outbound failures rather than turning remote endpoint behavior into a 5xx response. A concurrent monitor edit causes 409 and prevents a result for the old definition from replacing current state. Missing resources return 404. Validation returns 422. Errors use `{"error":{"code":"...","message":"..."}}` and never include outbound exception text. ## Concurrency and logging Store operations and compare-and-set status recording are guarded by one `asyncio.Lock`. Reads return deep copies. A monotonically increasing internal revision makes check updates atomic with respect to edits/deletes. Application events are one-line JSON records. URLs are normalized for logs by removing user information, fragments, and all query values (`?REDACTED`). Outbound exception details are classified, not logged verbatim. API resources retain the configured URL because it is part of their explicit contract; operators should still avoid URL credentials and secrets. ## Project structure - `app/main.py`: application factory, lifespan, handlers - `app/api.py`: REST contract - `app/models.py`: typed request/response models - `app/store.py`: concurrency-safe process-local state - `app/checker.py`: SSRF-safe checker and redacted event logging - `app/settings.py`: validated `MONITOR_` environment configuration - `tests/`: unit and API tests with no real outbound traffic - `Dockerfile`, `compose.yaml`, `pyproject.toml`: packaging and workflows - `docs/verification.md`: validation checklist and observed limitations