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
133 lines
7.2 KiB
Markdown
133 lines
7.2 KiB
Markdown
# PR-1 Apply Report: Backend Core for Container Monitoring & Notifications
|
|
|
|
## Status: COMPLETE
|
|
|
|
All 11 assigned tasks (MON-PR1-001 through MON-PR1-011) have been implemented and validated.
|
|
|
|
---
|
|
|
|
## Changed Files
|
|
|
|
### New Files (11)
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `apps/api/alembic/versions/2026_05_28_add_monitoring_tables.py` | Alembic migration creating `instance_events` + `health_checks` + 5 indexes |
|
|
| `apps/api/src/models/instance_event.py` | SQLAlchemy `InstanceEvent` model |
|
|
| `apps/api/src/models/health_check.py` | SQLAlchemy `HealthCheck` model |
|
|
| `apps/api/src/services/event_bus.py` | `InstanceEventBus` singleton with typed pub/sub |
|
|
| `apps/api/src/services/health_monitor.py` | `HealthMonitor` background polling task |
|
|
| `apps/api/src/services/correlation.py` | Async `CORRELATION_ID` context var + `CorrelationIdMiddleware` |
|
|
| `apps/api/src/services/lifecycle_hooks.py` | `publish_lifecycle_event` helper |
|
|
| `apps/api/src/api/events.py` | SSE endpoint `GET /events/stream` |
|
|
| `apps/api/tests/unit/test_event_bus.py` | Unit tests for EventBus |
|
|
| `apps/api/tests/unit/test_health_monitor.py` | Unit tests for HealthMonitor |
|
|
| `apps/api/tests/unit/test_monitoring_models.py` | Unit tests for new models |
|
|
|
|
### Modified Files (6)
|
|
| File | Change |
|
|
|------|--------|
|
|
| `apps/api/src/models/__init__.py` | Export `InstanceEvent`, `HealthCheck` |
|
|
| `apps/api/src/api/__init__.py` | Export `events_router` |
|
|
| `apps/api/src/api/tool_instances.py` | Lifecycle hooks at create/start/stop/restart/delete |
|
|
| `apps/api/src/logging_config.py` | JSON formatter + `CorrelationIdFilter` |
|
|
| `apps/api/src/main.py` | Register events router, middleware, HealthMonitor lifespan |
|
|
|
|
---
|
|
|
|
## Implementation Summary
|
|
|
|
### MON-PR1-001/002: Database Migration
|
|
- Single Alembic revision `2026_05_28_add_monitoring_tables` depends on current head.
|
|
- Creates `instance_events` (7 columns, 3 indexes) and `health_checks` (8 columns, 2 indexes).
|
|
- Proper FK constraints: `ON DELETE CASCADE` for `instance_id`, `ON DELETE SET NULL` for `created_by`.
|
|
- `upgrade()` and `downgrade()` both implemented.
|
|
|
|
### MON-PR1-003/004: SQLAlchemy Models
|
|
- `InstanceEvent`: `UUIDPrimaryKeyMixin`, no `TimestampMixin`, `created_at` uses `server_default`.
|
|
- `HealthCheck`: `UUIDPrimaryKeyMixin`, `checked_at` uses `server_default`.
|
|
- Both exported in `models/__init__.py` for Alembic autogenerate.
|
|
|
|
### MON-PR1-005: InstanceEventBus
|
|
- Singleton via `__new__` + module-level `_instance`.
|
|
- `subscribe(event_type, callback)` returns unsubscribe callable.
|
|
- `publish(event_type, payload)` delivers in same event loop iteration.
|
|
- Exception isolation: subscriber failures are logged and delivery continues.
|
|
- Added wildcard `"*"` subscription support for SSE endpoint.
|
|
|
|
### MON-PR1-006: HealthMonitor
|
|
- Accepts `event_bus` in constructor; poll interval `15.0s` (overridable in tests).
|
|
- `start()` is idempotent; `stop()` cancels task and clears `_last_known_state`.
|
|
- Queries instances with `status NOT IN ("pending", "stopped", "error")`.
|
|
- Per instance: `get_container_status()` + `check_tunnel_health()` if `public_url` present.
|
|
- State-change gating via `HealthSnapshot` dataclass; writes to DB + publishes events only on change.
|
|
- Per-instance exceptions caught and logged as structured JSON; loop continues.
|
|
|
|
### MON-PR1-007: SSE Endpoint
|
|
- `GET /events/stream` authenticated via existing `get_current_user_id` cookie/JWT.
|
|
- Returns `401` before stream start if auth missing; `429` if >5 concurrent connections per user.
|
|
- Per-connection `asyncio.Queue(maxsize=100)` drops oldest on overflow.
|
|
- `:ping` comment every 30 seconds.
|
|
- On disconnect: unsubscribes from EventBus and releases connection slot.
|
|
|
|
### MON-PR1-008: Lifecycle Hooks
|
|
- `lifecycle_hooks.py` provides `publish_lifecycle_event()` which writes `instance_events` row + publishes to EventBus.
|
|
- Instrumented in `tool_instances.py`:
|
|
- `create_instance` → `instance.created`
|
|
- `start_instance` → `instance.started` (at "starting"), `instance.error` (on crash), `instance.health_changed` (probe success/failure)
|
|
- `stop_instance` → `instance.stopped`
|
|
- `restart_instance` → `instance.restarted`
|
|
- `delete_instance` → `instance.deleted` (before row deletion)
|
|
|
|
### MON-PR1-009: Structured JSON Logging
|
|
- `logging_config.py` replaced plain-text formatter with `JSONFormatter`.
|
|
- Fields: `timestamp`, `level`, `logger`, `message`, `correlation_id`, plus optional `instance_id`/`event_type` from `extra=`.
|
|
- `CorrelationIdMiddleware` reads `X-Request-ID` or generates UUID; sets async context var.
|
|
- `uvicorn.access` remains at `WARNING`.
|
|
|
|
### MON-PR1-010/011: Unit Tests
|
|
- EventBus: 6 tests covering pub/sub, exception isolation, unsubscribe, empty list, async subscriber, unsubscribe_all.
|
|
- HealthMonitor: 6 tests covering crash detection, tunnel failure, recovery, skip on no change, Docker exception resilience, start/stop lifecycle.
|
|
- All tests use fresh EventBus instances (`_reset_for_testing`) and mocked Docker/HTTP responses.
|
|
|
|
---
|
|
|
|
## Test Commands & Exit Codes
|
|
|
|
```bash
|
|
# Focused new tests
|
|
cd apps/api && python -m pytest tests/unit/test_event_bus.py tests/unit/test_health_monitor.py tests/unit/test_monitoring_models.py -v
|
|
# Exit code: 0 (15 passed)
|
|
|
|
# Full unit suite — no regressions from this PR
|
|
cd apps/api && python -m pytest tests/unit/ -v
|
|
# Exit code: 1 (172 passed, 4 failed — all pre-existing failures in test_config.py and test_git_repository_clone_preflight.py)
|
|
|
|
# Ruff linting
|
|
cd apps/api && python -m ruff check src/services/event_bus.py src/services/health_monitor.py src/services/correlation.py src/services/lifecycle_hooks.py src/api/events.py src/models/instance_event.py src/models/health_check.py src/models/__init__.py src/logging_config.py src/main.py src/api/__init__.py alembic/versions/2026_05_28_add_monitoring_tables.py
|
|
# Exit code: 0 (All checks passed)
|
|
```
|
|
|
|
---
|
|
|
|
## Surprises & Decisions
|
|
|
|
1. **`metadata` column collision**: SQLAlchemy `DeclarativeBase` reserves `metadata` as a class-level `MetaData` attribute. Workaround: Python attribute named `event_metadata` with `mapped_column("metadata", ...)` to preserve the DB column name.
|
|
2. **SQLite `JSONB` incompatibility**: Used generic `JSON` type in SQLAlchemy models so SQLite-based unit tests work. Migration still uses `sa.JSON()` which is portable.
|
|
3. **Delete audit row survivability**: `ON DELETE CASCADE` on `instance_events.instance_id` means the `instance.deleted` audit row cannot survive the instance deletion. Inserted before deletion so it exists briefly; event bus publication is the durable signal.
|
|
4. **Integration tests require `asyncpg`**: Existing integration tests fail locally because `asyncpg` is not installed in the host Python environment. These are pre-existing infrastructure limitations, not regressions.
|
|
5. **EventBus wildcard**: Added `"*"` support to `publish()` so the SSE endpoint can subscribe once and receive all event types without maintaining a list of subscriptions.
|
|
|
|
---
|
|
|
|
## PR Boundary
|
|
|
|
This PR includes the complete backend core for container monitoring. The next PR (PR-2) should cover:
|
|
- Frontend `useEvents()` SSE hook
|
|
- `ToastProvider` + `toast-rules.ts`
|
|
- Real-time badge updates and polling removal
|
|
|
|
The final PR (PR-3) should cover:
|
|
- Integration tests for SSE and lifecycle hooks
|
|
- E2E tests
|
|
- Documentation
|