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
7.2 KiB
7.2 KiB
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_tablesdepends on current head. - Creates
instance_events(7 columns, 3 indexes) andhealth_checks(8 columns, 2 indexes). - Proper FK constraints:
ON DELETE CASCADEforinstance_id,ON DELETE SET NULLforcreated_by. upgrade()anddowngrade()both implemented.
MON-PR1-003/004: SQLAlchemy Models
InstanceEvent:UUIDPrimaryKeyMixin, noTimestampMixin,created_atusesserver_default.HealthCheck:UUIDPrimaryKeyMixin,checked_atusesserver_default.- Both exported in
models/__init__.pyfor 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_busin constructor; poll interval15.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()ifpublic_urlpresent. - State-change gating via
HealthSnapshotdataclass; 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/streamauthenticated via existingget_current_user_idcookie/JWT.- Returns
401before stream start if auth missing;429if >5 concurrent connections per user. - Per-connection
asyncio.Queue(maxsize=100)drops oldest on overflow. :pingcomment every 30 seconds.- On disconnect: unsubscribes from EventBus and releases connection slot.
MON-PR1-008: Lifecycle Hooks
lifecycle_hooks.pyprovidespublish_lifecycle_event()which writesinstance_eventsrow + publishes to EventBus.- Instrumented in
tool_instances.py:create_instance→instance.createdstart_instance→instance.started(at "starting"),instance.error(on crash),instance.health_changed(probe success/failure)stop_instance→instance.stoppedrestart_instance→instance.restarteddelete_instance→instance.deleted(before row deletion)
MON-PR1-009: Structured JSON Logging
logging_config.pyreplaced plain-text formatter withJSONFormatter.- Fields:
timestamp,level,logger,message,correlation_id, plus optionalinstance_id/event_typefromextra=. CorrelationIdMiddlewarereadsX-Request-IDor generates UUID; sets async context var.uvicorn.accessremains atWARNING.
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
# 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
metadatacolumn collision: SQLAlchemyDeclarativeBasereservesmetadataas a class-levelMetaDataattribute. Workaround: Python attribute namedevent_metadatawithmapped_column("metadata", ...)to preserve the DB column name.- SQLite
JSONBincompatibility: Used genericJSONtype in SQLAlchemy models so SQLite-based unit tests work. Migration still usessa.JSON()which is portable. - Delete audit row survivability:
ON DELETE CASCADEoninstance_events.instance_idmeans theinstance.deletedaudit row cannot survive the instance deletion. Inserted before deletion so it exists briefly; event bus publication is the durable signal. - Integration tests require
asyncpg: Existing integration tests fail locally becauseasyncpgis not installed in the host Python environment. These are pre-existing infrastructure limitations, not regressions. - EventBus wildcard: Added
"*"support topublish()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