outbreak-signal-service
v0.1.0 · all 7 phases shipped

Statistical outbreak signal detection for DHIS2

Pulls weekly DHIS2 surveillance data, flags statistical aberrations with CDC's EARS and CUSUM methods, and publishes the results as a reviewable alert — over REST, signed webhooks, and FHIR.

An alert is a signal for a person to review — never a confirmed outbreak. That distinction is written into the software on purpose: every alert needs a human to acknowledge, resolve, or dismiss it before anything else happens.

How it works

Three stages, each independently testable and each responsible for one thing.

Weekly
Sync
Pulls aggregate series from a DHIS2 instance, idempotently. Missing weeks are recorded, never silently filled.
→
Automatic
Detection
Five strategies run side by side right after every sync, each one a pluggable Spring bean.
→
Human-gated
Alert lifecycle
Open → acknowledged → resolved or dismissed, enforced as a real state machine — an illegal move returns 409.

Detection strategies, backtested honestly

Before trusting any of them, all five ran against 200 synthetic series with a known, injected outbreak. These are the test's own printed numbers, copied in unedited — see RESULTS.md for the full methodology.

AlgorithmSensitivityFalse alarms /100wkMedian delay
Moving-average Z0.6501.1432.0 wk
EARS C10.5852.1402.0 wk
EARS C20.8052.1552.0 wk
EARS C30.8853.9242.0 wk
CUSUM0.87519.4432.0 wk

CUSUM's raw per-week false-alarm rate looks worse than it is — collapsed into episodes (consecutive alarmed weeks counted once) it's 1.705/100wk, close to EARS C3. Full breakdown in RESULTS.md.

What's built

All seven phases of the original plan, tagged and merged.

✓ Idempotent weekly DHIS2 sync, retry + circuit breaker
✓ Five detection strategies, hand-verified fixtures
✓ OAuth2-secured Alerts API with optimistic locking
✓ Transactional outbox: signed webhooks + email
✓ Read-only FHIR DetectedIssue output
✓ Prometheus metrics, structured logs, correlation IDs
✓ Rate limiting, graceful shutdown, non-root Dockerfile
✓ GHCR image + SBOM on every tagged release

API tour

Every /api/v1/** and /fhir/** call needs a JWT bearer token. Full machine-readable spec at /v3/api-docs.

GET  /api/v1/alerts?status=OPEN&algorithm=EARS_C3&page=0
GET  /api/v1/alerts/{id}
POST /api/v1/alerts/{id}/acknowledge      # ANALYST, ADMIN
POST /api/v1/alerts/{id}/resolve          # ANALYST, ADMIN
POST /api/v1/alerts/{id}/dismiss          # ANALYST, ADMIN -- reason required

POST /api/v1/sync                         # ADMIN
POST /api/v1/detection/run                # ADMIN

GET  /fhir/DetectedIssue/{id}             # application/fhir+json
GET  /fhir/metadata                       # public CapabilityStatement

GET  /actuator/health                     # public

Resources