diff --git a/docs/SERVICE_DESIGN.md b/docs/SERVICE_DESIGN.md index 23a2486..304410c 100644 --- a/docs/SERVICE_DESIGN.md +++ b/docs/SERVICE_DESIGN.md @@ -1,31 +1,33 @@ -# Service contract and architecture +# Endpoint Monitor service contract -## Resource -A monitor has an immutable UUID `id`, `name`, HTTP(S) `url`, timestamps, and current status. `status` is `unknown` until checked, `up` for a final HTTP 200–399 response, `down` for a final HTTP 400–599 response, and `error` when no accepted HTTP response is obtained. Check metadata comprises `checked_at`, non-negative `latency_ms`, optional `http_status`, and a bounded, non-sensitive `error` category. +## Resource and lifecycle -## API -All JSON is UTF-8. Routes are under `/api/v1` except operational probes. +A monitor has an immutable UUID, name, HTTP(S) URL, per-check timeout, timestamps, and current status (`unknown`, `up`, or `down`). Status also records the most recent check time, HTTP status, latency, and a sanitized error. New and materially updated monitors are `unknown`. State is process-local and protected by one `asyncio.Lock`; it is lost at restart and is not shared between workers. -* `POST /api/v1/monitors` -> 201 and monitor; duplicate names are allowed. -* `GET /api/v1/monitors` -> 200 list sorted by creation time and id. -* `GET /api/v1/monitors/{id}` -> 200 or 404. -* `PUT /api/v1/monitors/{id}` -> 200 or 404. Updating name/URL resets check state to `unknown`. -* `DELETE /api/v1/monitors/{id}` -> 204 or 404. -* `POST /api/v1/monitors/{id}/check` -> 200 and atomically persisted check result, or 404 if absent/deleted during the check. -* `GET /api/v1/monitors/{id}/status` -> 200 current status projection or 404. -* `GET /healthz` -> liveness 200; `GET /readyz` -> readiness 200 after app initialization. +## HTTP API -FastAPI's validation errors use 422. Application errors use `{"detail":"..."}` and do not expose network exception text. There is intentionally no authentication. +| Method | Path | Meaning | +|---|---|---| +| POST | `/monitors` | Create; `201` | +| GET | `/monitors` | List | +| GET | `/monitors/{id}` | Retrieve | +| PATCH | `/monitors/{id}` | Partial update | +| DELETE | `/monitors/{id}` | Delete; `204` | +| POST | `/monitors/{id}/check` | Run a check and atomically store its result | +| GET | `/monitors/{id}/status` | Current status only | +| GET | `/health/live` | Process liveness | +| GET | `/health/ready` | Readiness for traffic | -## Checker and SSRF policy -Only HTTP(S), non-credentialed URLs are accepted. Before every request and after every redirect, the host is resolved and every returned address must be globally routable. Loopback, private, link-local, multicast, reserved, unspecified, and non-global addresses are rejected. Redirects are followed manually up to the configured bound. Timeouts and response-body streaming are bounded. This validation substantially reduces SSRF risk, but DNS validation and connection are separate operations in the standard HTTP stack; production deployments should additionally enforce an egress proxy/firewall to eliminate DNS-rebinding TOCTOU risk. +A final 200–399 response is `up`; other responses and transport failures are `down`. A transport failure is a completed check and returns `200` with a down result. A policy-rejected target returns `400` after recording a sanitized down result. A monitor changed or deleted while its check is in flight produces `409`/`404`, preventing a stale result from overwriting newer state. -Checks use a monotonic timer. Store mutation occurs under one asyncio lock. A delete that races with a check wins and the result is not resurrected. +Errors use `{"error":{"code":"...","message":"..."}}`; request validation adds `details`. No authentication is provided. -## Lifecycle and deployment -`MonitorStore` is process-local and starts empty on every process start. It is concurrency-safe within one event loop, not shared across workers, and has no persistence. Run exactly one worker. Readiness describes initialization, not durability or external dependency health. +## Outbound security and observability -Configuration is environment-driven and validated at startup. Logs are one-line JSON. Logged URLs have credentials, query, and fragment removed; only controlled error categories are logged. +Only HTTP(S), hostnames without credentials, and configured ports are accepted. Every initial and redirect target is resolved immediately before its request; all returned addresses must be globally routable. Loopback, private, link-local, multicast, reserved, and unspecified addresses are blocked. Redirects are followed manually up to a bounded limit. Timeouts and response-body reads are bounded (checks stream no body). Operators should still enforce egress policy at the network layer because application DNS checks cannot eliminate every DNS rebinding/TOCTOU risk. -## Project layout -`app/models.py` defines the contract; `store.py` owns state; `security.py` validates targets; `checker.py` performs checks; `main.py` wires HTTP routes and lifecycle. Tests inject DNS and HTTP transports so outbound calls are deterministic. +Logs are one-line JSON. URLs are normalized to remove user info, query strings, and fragments; exception text and response bodies are never logged. + +## Layout + +`app/` contains configuration, models, locked storage, SSRF policy, checking, logging, and FastAPI composition. `tests/` exercises API and checker policy. Packaging is in `pyproject.toml`, `Dockerfile`, and `compose.yaml`; exact root `.gitignore` and `.dockerignore` files prevent local/build artifacts from entering source control or build context.