4a7f24348c
- Add instance_events and health_checks tables with Alembic migration - InstanceEventBus: typed pub/sub singleton with wildcard support - HealthMonitor: async background loop polling containers every 15s - SSE endpoint GET /events/stream with auth and connection limits - Lifecycle hooks in tool_instances.py (create/start/stop/restart/delete) - Structured JSON logging with correlation IDs - 15 new unit tests (EventBus, HealthMonitor, MonitoringModels) Quality gates: pytest 15 new passed, ruff clean
226 lines
12 KiB
Markdown
226 lines
12 KiB
Markdown
# Explore: Container Monitoring & Notification System
|
||
|
||
## 1. Current Container Lifecycle Flow
|
||
|
||
### Start → Run → Stop → Cleanup
|
||
|
||
1. **Create** (`POST /projects/{pid}/repositories/{rid}/instances`)
|
||
- Generates `instance_name`, finds free port, builds image (Dockerfile) or renders compose template.
|
||
- Writes `docker-compose.yml`, `.env`, config files to `instance_dir`.
|
||
- DB record created with `status = "pending"`.
|
||
- **File:** `apps/api/src/api/tool_instances.py` (lines ~300–600)
|
||
|
||
2. **Start** (`POST /.../instances/{id}/start`)
|
||
- `status` set to `"building"`.
|
||
- Applies config profile (env vars, git mounts, port override, start command).
|
||
- Runs `docker compose up -d` via `execute_compose_command()`.
|
||
- Retrieves `container_id` and `container_name` via `docker ps` filters.
|
||
- Connects container to `"backend"` network.
|
||
- `status` set to `"starting"`, then polls `docker inspect` every 2s for up to 30s (`wait_for_container_running`).
|
||
- If container exits → `status = "error"`, logs captured.
|
||
- Executes readiness probe (configurable per `ToolType`, default `curl` for web tools).
|
||
- Probe succeeds → `status = "running"`; fails → `status = "unhealthy"`.
|
||
- For web tools, starts `cloudflared` tunnel and stores `tunnel_id` + `public_url`.
|
||
- **File:** `apps/api/src/api/tool_instances.py` (lines ~1100–1500)
|
||
|
||
3. **Stop** (`POST /.../instances/{id}/stop`)
|
||
- Kills cloudflared tunnel by PID (`stop_cloudflared_tunnel`).
|
||
- Runs `docker compose stop`.
|
||
- `status = "stopped"`, clears `url`/`public_url`/`tunnel_id`, sets `last_stopped_at`.
|
||
- **File:** `apps/api/src/api/tool_instances.py` (lines ~1500–1550)
|
||
|
||
4. **Restart** (`POST /.../instances/{id}/restart`)
|
||
- Stops old tunnel, re-applies config profile, runs `docker compose restart`, recreates tunnel.
|
||
- **File:** `apps/api/src/api/tool_instances.py` (lines ~1550–1650)
|
||
|
||
5. **Delete** (`DELETE /.../instances/{id}`)
|
||
- Stops tunnel, runs `docker compose down -v`, deletes `instance_dir` (includes clone + SSH keys).
|
||
- Removes DB row.
|
||
- **File:** `apps/api/src/api/tool_instances.py` (lines ~1650–1720)
|
||
|
||
6. **Health Check Endpoint** (`GET /.../instances/{id}/health`)
|
||
- Calls `get_container_status()` (docker inspect) + `check_tunnel_health()` (curl to public URL).
|
||
- Returns composite `healthy` flag: container running AND tunnel healthy for web tools.
|
||
- Includes `probe_status` and `last_probe_output` from DB `probe_result` JSON column.
|
||
- **File:** `apps/api/src/api/tool_instances.py` (lines ~1800–1870)
|
||
|
||
### Background Patterns
|
||
- **TerminalManager** runs an `_idle_check_loop()` every 60s to close idle terminal sessions.
|
||
- **File:** `apps/api/src/services/terminal_manager.py` (lines ~40–80)
|
||
- No general container-level background monitor or reaper exists.
|
||
|
||
---
|
||
|
||
## 2. Health Check Gaps
|
||
|
||
| What's Present | What's Missing |
|
||
|---|---|
|
||
| One-time readiness probe at startup (`execute_probe`) | **No continuous health monitoring** after startup |
|
||
| `GET /health` returns container + tunnel status on demand | **No periodic background polling** of container state |
|
||
| `docker inspect` reads `State.Status`, `ExitCode`, `Health.Status` | **No liveness probe** (only readiness at start) |
|
||
| Tunnel health checked via HTTP curl | **No automatic recovery** on container crash |
|
||
| `probe_result` JSON stored in DB | **No health history / time-series** |
|
||
| Frontend polls health every 30s for running instances | **No server-side event push** when health changes |
|
||
|
||
### Critical Gaps
|
||
1. **Container crash goes unnoticed** until a user manually refreshes or the frontend poll happens.
|
||
2. **Tunnel failure is only detected on-demand** (health endpoint or user action). No proactive retry or notification.
|
||
3. **No OOM or exit-code tracking** beyond the immediate startup phase.
|
||
4. **No health state transitions** (e.g., `running → degraded → unhealthy → stopped`).
|
||
5. **Readiness probe is fire-and-forget**; if it fails, status becomes `"unhealthy"` but no further action is taken.
|
||
|
||
---
|
||
|
||
## 3. Logging Gaps
|
||
|
||
### Current Logging (`apps/api/src/logging_config.py`)
|
||
- Plain text format: `%(asctime)s [%(levelname)s] %(name)s: %(message)s`
|
||
- Request/response middleware logs timing and status codes.
|
||
- Exception middleware logs unhandled tracebacks.
|
||
- **No structured logging** (JSON), **no correlation IDs**, **no container event stream persistence**.
|
||
|
||
### What's Logged (Container-Related)
|
||
- Docker compose up/down/start/stop return codes and stderr snippets.
|
||
- Container startup success/failure and wait time.
|
||
- Readiness probe attempts and results.
|
||
- Tunnel creation/failure.
|
||
- Permission fix warnings.
|
||
|
||
### What's NOT Logged
|
||
- **Container stdout/stderr is not persisted** — only fetched on-demand via `docker logs`.
|
||
- **No lifecycle event log** (audit trail of who started/stopped/restarted what and when).
|
||
- **No structured container events** (create, start, die, oom, kill) from Docker daemon.
|
||
- **No log aggregation** — logs are ephemeral console output.
|
||
- **No log levels per instance** — all logs go through root logger.
|
||
|
||
---
|
||
|
||
## 4. Notification Gaps
|
||
|
||
### Current State: **No notification system exists.**
|
||
|
||
| Area | Finding |
|
||
|---|---|
|
||
| **Backend events/pub-sub** | None. No event bus, message queue, or broadcast mechanism. |
|
||
| **WebSocket (non-terminal)** | None. Only terminal uses WS (`/ws/terminal/{instance_id}`). |
|
||
| **SSE** | Not implemented. |
|
||
| **Polling** | Frontend polls instance list and health every 30s. |
|
||
| **Frontend toast/alert** | No toast, snackbar, or global notification component found. |
|
||
| **Error display** | Inline `error-message` divs and `ErrorState` component (`data-states.tsx`). |
|
||
|
||
### Frontend Evidence
|
||
- `instance-list.tsx` polls health every 30s for running instances and shows a `"tunnel error"` badge inline.
|
||
- `session-card.tsx` displays `Tunnel Error` / `App Error` badges but no push notification.
|
||
- `api/client.ts` has an axios interceptor for 401 redirect and retry logic, but no toast on errors.
|
||
- No `toast`, `notification`, `snackbar`, or `alert` components exist in `apps/web/src/components/`.
|
||
|
||
---
|
||
|
||
## 5. Database: Instance State Tracking
|
||
|
||
### Table: `tool_instances`
|
||
**File:** `apps/api/src/models/tool_instance.py`
|
||
|
||
| Column | Purpose |
|
||
|---|---|
|
||
| `status` | `pending → building → starting → probing → running → unhealthy → stopped → error` |
|
||
| `container_id` | Docker container ID (nullable) |
|
||
| `container_name` | Docker container name (nullable) |
|
||
| `compose_path` | Path to `docker-compose.yml` |
|
||
| `port` | Host port mapped to container |
|
||
| `url` / `public_url` | Cloudflare tunnel URL |
|
||
| `tunnel_id` | cloudflared PID |
|
||
| `last_started_at` / `last_stopped_at` | Timestamps |
|
||
| `probe_result` | JSON blob with last probe outcome |
|
||
| `selected_config_profile_id` | FK to config profile |
|
||
|
||
### Migrations
|
||
- `0006_tool_instances.py` — base table with `status`, `container_id`, `url`, `port`.
|
||
- `0007_instance_container_name.py` — adds `container_name`.
|
||
- `0011_tool_instance_tunnel_fields.py` — adds `public_url`, `tunnel_id`.
|
||
- `0013_add_probe_result.py` — adds `probe_result` (JSON).
|
||
|
||
### Gaps
|
||
- **No `health_history` table** — can't track uptime, downtime, or flapping.
|
||
- **No `instance_events` table** — no audit log of state transitions.
|
||
- **No `notification_preferences` or `user_notifications` table**.
|
||
|
||
---
|
||
|
||
## 6. Key Files and Their Roles
|
||
|
||
| File | Role |
|
||
|---|---|
|
||
| `apps/api/src/services/docker.py` | All Docker CLI interactions: compose up/down, container status/logs, tunnel management, port finding. |
|
||
| `apps/api/src/api/tool_instances.py` | CRUD + lifecycle endpoints for instances (create, start, stop, restart, delete, health, logs, proxy, tunnel recreate). |
|
||
| `apps/api/src/services/terminal_manager.py` | In-memory session registry + 60s idle cleanup loop. Pattern to emulate for container monitoring. |
|
||
| `apps/api/src/services/readiness_probe.py` | `execute_probe()` — runs a command inside a container with retry logic. |
|
||
| `apps/api/src/logging_config.py` | Plain-text logging setup, request/response middleware, exception middleware. |
|
||
| `apps/api/src/models/tool_instance.py` | SQLAlchemy model for `tool_instances` table. |
|
||
| `apps/api/src/models/tool_type.py` | SQLAlchemy model for `tool_types`, includes `readiness_probe` JSON config. |
|
||
| `apps/api/src/api/health.py` | System health endpoint (DB + disk), **not** per-instance health. |
|
||
| `apps/web/src/components/instance-list.tsx` | Displays instances with status dots, polls health every 30s, inline tunnel error badges. |
|
||
| `apps/web/src/components/session-list.tsx` | Grouped list of sessions (active vs recent), receives `tunnelHealth` prop. |
|
||
| `apps/web/src/components/session-card.tsx` | Card UI with status badges, stop/delete confirm, tunnel error display. |
|
||
| `apps/web/src/api/sessions.ts` | API client for instance CRUD and `checkInstanceHealth()`. |
|
||
|
||
---
|
||
|
||
## 7. Risks and Unknowns
|
||
|
||
1. **Docker CLI dependency** — All container operations shell out to `docker` / `docker compose`. No Docker SDK or lib used. This is slow and brittle under load.
|
||
2. **Tunnel PID fragility** — `tunnel_id` is a process ID string. If the API restarts, PIDs are lost and tunnels may leak.
|
||
3. **No instance-level auto-restart** — If a container exits (crash, OOM), it stays `error` or `stopped` until a user manually restarts it.
|
||
4. **Probe result is a single JSON blob** — Overwritten on every start. No history.
|
||
5. **Polling load** — Frontend polls every 30s per running instance. With many users + many instances, this generates significant health-check load.
|
||
6. **No auth on WebSocket upgrade** — Terminal WS endpoint may not validate session ownership on connection (not verified in this scout).
|
||
7. **Cloudflared process leaks** — If `stop_cloudflared_tunnel` fails or the API crashes, tunnel processes may become orphaned.
|
||
8. **Log retention** — `docker logs` is the only source; no rotation or persistence strategy.
|
||
9. **Scaling limitation** — In-memory `TerminalManager` and any future in-memory event bus won't work across multiple API replicas.
|
||
|
||
---
|
||
|
||
## 8. Recommended Architecture Approach
|
||
|
||
### Short-Term (MVP): In-Memory Events + Server-Sent Events (SSE)
|
||
|
||
**Rationale:**
|
||
- The project already uses FastAPI. SSE is natively supported and simpler than WebSockets for one-way server→client push.
|
||
- No new infrastructure (message queue) needed.
|
||
- Matches the existing polling use case but eliminates 30s latency.
|
||
|
||
**Components:**
|
||
1. **InstanceEventBus** (in-memory singleton, similar to `TerminalManager`)
|
||
- Publishes events: `instance.created`, `instance.started`, `instance.stopped`, `instance.health_changed`, `instance.error`.
|
||
2. **Background Monitor Task** (asyncio loop, like `TerminalManager._idle_check_loop`)
|
||
- Every 10–30s, inspect running containers and tunnels.
|
||
- On state change, update DB + publish event to bus.
|
||
3. **SSE Endpoint** (`GET /events`)
|
||
- Stream JSON events to connected clients.
|
||
- Frontend subscribes once, receives real-time updates.
|
||
4. **Frontend Toast Layer**
|
||
- New lightweight toast component subscribed to SSE.
|
||
- Shows notifications for errors, tunnel failures, successful starts.
|
||
|
||
### Medium-Term: Persistent Event Log + Health History
|
||
|
||
1. **`instance_events` table** — append-only audit log of all lifecycle transitions.
|
||
2. **`health_checks` table** — periodic snapshots of container + tunnel health for trend analysis.
|
||
3. **Structured logging** — Switch to JSON format; include `instance_id`, `event_type`, `correlation_id`.
|
||
|
||
### Long-Term: Message Queue (if multi-replica)
|
||
|
||
- If the API needs to scale horizontally, replace in-memory bus with **Redis Pub/Sub** or **NATS**.
|
||
- Background monitor becomes a separate worker process or scheduled task.
|
||
|
||
### Decision Summary
|
||
|
||
| Concern | Recommended Path |
|
||
|---|---|
|
||
| Real-time status updates | **SSE** from in-memory event bus |
|
||
| Health monitoring | Background asyncio task polling Docker + tunnels |
|
||
| User notifications | Lightweight toast component fed by SSE |
|
||
| Audit / history | New `instance_events` and `health_checks` tables |
|
||
| Log aggregation | JSON structured logs + optional log shipping |
|
||
| Multi-replica safety | Deferred to future; add Redis/NATS when needed |
|