Skip to content

ManojKumarChunduru/interface-sentinel

Repository files navigation

InterfaceSentinel

HL7 v2 validation gateway for hospital interface teams. Speaks MLLP, validates every message against configurable per-type profiles, ACK/NAKs with exact field positions, dead-letters rejects for replay, and converts clean ADT messages to FHIR R4.

CI Tests Recall License Transport

What this solves

  • Hospitals run dozens of HL7 v2 feeds between ancillary systems and the EHR. When a lab system sends a DFT charge message with a non-numeric amount or a missing charge code, the worst outcome is not rejection, it is silence: the message dies in an interface engine, nobody is notified, and the charge never reaches billing. On the synthetic replay corpus below, rejected DFT messages carried $1.03M in charges; without a gateway those dollars are invisible until month-end reconciliation.
  • InterfaceSentinel sits at the boundary. Every message gets parsed tolerantly, validated against a per-type profile (required segments, typed field checks, code sets, staleness, duplicates), and answered with a proper HL7 ACK: AA when clean, AE with the first error when invalid, AR when structurally unusable. Everything rejected lands whole in a dead-letter queue with a replay endpoint, so remediation is a workflow, not an archaeology dig.
  • Validation rules are YAML configuration an interface analyst can edit, not code. Findings carry exact positions (FT1[2]-11) so the NAK tells the upstream team what to fix.

Data disclosure: every message in this repository is synthetic, produced by a seeded generator in datagen/. No real patient, provider, or payer data appears anywhere. All numbers below were produced by scripts committed in this repo, and raw outputs are committed beside them.

Architecture

flowchart LR
    A[Ancillary systems<br/>LIS / RIS / ADT feeds] -- "MLLP :2575" --> B[Asyncio listener<br/>frame reassembly]
    T[Test / ops clients] -- "HTTP :8100" --> P
    B --> P[Pipeline]
    P --> C{Parse}
    C -- no MSH --> AR[ACK AR] & DL[(dead_letter)]
    C -- ok --> V[Profile validator<br/>YAML per message type]
    V -- errors --> AE[ACK AE + position] & DL
    V -- clean/warnings --> AA[ACK AA]
    AA & AE --> DB[(PostgreSQL<br/>messages + findings)]
    DL -- "fix upstream, POST /replay" --> P
    AA -. "ADT only" .-> F[FHIR R4 mapper<br/>Patient + Encounter]
    DB --> SQL[SQL views] --> BI[Power BI<br/>interface health dashboard]
Loading

Tech stack

Technology Why it is here
Python 3.12 asyncio MLLP frames arrive split across arbitrary TCP reads; stream buffering is the natural fit. See ADR-0002
Purpose-built HL7 parser Triage requires parsing past errors and reporting exact positions; strict library parsers throw where this gateway must describe. See ADR-0001
YAML validation profiles Interface contracts change per site and per feed; analysts edit configuration, deployments stay out of the loop
FastAPI Ops surface: dead-letter listing and replay, metrics, stateless FHIR conversion; shares one Pipeline with MLLP so transports cannot drift
FHIR R4 mapping (PID/PV1) Downstream analytics ask for Patient and Encounter first; deliberately narrow. See ADR-0003
PostgreSQL (SQLAlchemy) Message + findings commit atomically; SQLite serves local runs and CI with zero setup
structlog One JSON line per message with control ID, status, and latency

Two-minute setup

git clone <repo> && cd interface-sentinel
pip install -r requirements.txt -r requirements-dev.txt

PYTHONPATH=. uvicorn app.main:app --port 8100   # MLLP starts on 2575 in-process

Or with Postgres: docker compose up --build

Send a deliberately broken charge message over HTTP (same pipeline as MLLP):

curl -s -X POST localhost:8100/messages -H "Content-Type: application/json" -d '{
 "message": "MSH|^~\\&|LAB_SYS|FACILITY_A|SENTINEL|GATEWAY|20260716083000||DFT^P03^DFT_P03|C1001|P|2.5.1\rEVN|P03|20260716083000\rPID|1||MRN445566^^^SENTINEL^MR||LOPEZ^CARLOS||19701104|M\rPV1|1|O|ED^EMERGENCY\rFT1|1|||20260716||CG||||1|12O.50|||||ED^EMERGENCY"}'

The ACK comes back MSA|AE|C1001 citing the first finding; GET /messages/{id} shows both: FIELD_MISSING at FT1[1]-7 (no charge code) and FIELD_FORMAT at FT1[1]-11 (12O.50 has a letter O, a classic keying error). GET /dead-letter lists it for replay after the upstream fix. Update the MSH-7 timestamp if you are running this more than 7 days after the date shown, or the staleness check will fire too, which is rather the point.

Tests: PYTHONPATH=. pytest tests/ -v (25 tests, including real MLLP round-trips over TCP against an ephemeral-port listener).

Validation effectiveness (synthetic corpus, 20k messages)

The replay harness (benchmark/effectiveness_replay.py) runs a labeled corpus with a known defect taxonomy through the gateway and scores detection per category. Raw output: benchmark/effectiveness_report.json.

Injected defect category Injected Caught Recall
FIELD_MISSING (MRN, charge code, control ID) 568 568 100%
FIELD_FORMAT (malformed DOB, non-numeric amount) 343 343 100%
CODESET (invalid patient class, transaction type) 319 319 100%
STALE (replayed feed, 10 to 60 days old) 250 250 100%
STRUCT (no MSH segment) 240 240 100%
SEG_MISSING (dropped PV1/OBR) 239 239 100%
False rejects on 18,041 clean messages 0 0.0%

An honest note on those numbers: deterministic validation against a defect taxonomy it was built to catch should score 100%, and a miss would be a bug, not a shortfall. So this replay is framed as what it is: a regression harness at corpus scale, and CI runs it as a merge gate (recall below 100% or any false reject fails the build). The claim is not "catches everything wrong with HL7"; it is "provably catches its documented edit catalog, and provably does not reject clean traffic."

Performance under load

MLLP round-trip (send framed message, wait for framed ACK) over persistent connections, single process, SQLite backend, 4 vCPU container. HL7 senders serialize on ACKs, so realistic concurrency is a handful of feed connections, not hundreds. Reproduce with benchmark/mllp_load_test.py; raw output: benchmark/results.json.

xychart-beta
    title "MLLP ACK round-trip latency (ms, 2000 messages per stage)"
    x-axis "persistent connections" [1, 2, 4, 8]
    y-axis "latency (ms)" 0 --> 420
    line "p50" [5.4, 4.62, 4.77, 19.97]
    line "p95" [7.2, 7.74, 61.12, 92.55]
    line "p99" [8.9, 110.17, 336.65, 386.27]
Loading
Connections Throughput p50 p95 p99
1 177.7 msg/s 5.40 ms 7.20 ms 8.90 ms
2 205.6 msg/s 4.62 ms 7.74 ms 110.17 ms
4 204.4 msg/s 4.77 ms 61.12 ms 336.65 ms
8 190.1 msg/s 19.97 ms 92.55 ms 386.27 ms

Reading it honestly: throughput is flat past two connections because the pipeline is synchronous and SQLite serializes writes, so added connections buy queueing, not parallelism; the Postgres deployment relieves the write lock. The absolute numbers are the real story: a busy hospital's combined ADT, lab, and charge feeds run single-digit messages per second, and one process sustains about 200, roughly 40x headroom, before anyone tunes anything.

Analytics layer

analytics/sql/views.sql ships five Power BI-facing views: hourly interface health by sending system, error taxonomy by code and position, rejected-DFT charge exposure in dollars (the revenue cycle number), the dead-letter remediation queue with aging, and duplicate pressure by sender (repeated control IDs usually mean an upstream system replaying after restarts).

Security and compliance

  • No PHI exists here; the corpus is synthetic by construction. Deployed against real feeds this is a HIPAA system: TLS-wrapped MLLP (or stunnel/VPN as most sites actually do), Postgres encryption at rest, and access to raw messages restricted, since dead-lettered messages contain full PHI by nature.
  • Configuration and credentials arrive via environment variables (IS_ prefix); the compose password is marked local-only and deployed credentials come from a platform secrets manager.
  • Structured logs carry control IDs, types, and finding codes, never patient names or DOBs, so log aggregation does not become a shadow PHI store.

Failure modes

Failure Behavior
Structurally unusable message (no MSH) ACK AR; raw bytes preserved in dead_letter; nothing silently dropped
Validation errors ACK AE citing the first error with position; message stored with all findings AND dead-lettered whole for replay
Duplicate control ID ACK AE with DUP finding; guards against upstream replays double-posting charges
Stale message (interface restart replays old feed) ACK AE with STALE finding; window configurable via IS_MAX_MESSAGE_AGE_DAYS
Database down Pipeline raises, no ACK is sent, sender retries per MLLP convention; a message is never ACKed without a committed row
Partial TCP frames Buffered until the frame completes; multiple messages per read handled

The pivot: what was intentionally left out

  • Per-message pipeline timeout. A poison message stalls only its own connection today (senders serialize on ACKs), but a watchdog belongs in v1.1.
  • FHIR ChargeItem mapping for DFTs, the obvious next resource given the billing focus (ADR-0003 explains the narrow start).
  • Z-segment profile support and repetition-aware field checks; the checker seam exists, the demand does not yet.
  • Enhanced-mode acknowledgments (commit-level ACKs); original mode covers the feeds this models.

Operationalization path

  1. TLS termination for MLLP and mutual auth per sending system.
  2. Move duplicate detection into a database unique constraint for multi-instance deployments (ADR-0002, consequences).
  3. ChargeItem mapping plus a persisted FHIR store for longitudinal queries.
  4. Wire vw_rejected_charge_exposure into a daily Power BI subscription to revenue cycle leadership: dollars blocked at the interface, by sender, by day.

Repository map

app/hl7/          parser + MLLP listener
app/validation/   profile-driven engine (the edit catalog lives in config/)
app/fhir/         ADT to FHIR R4 mapping
app/pipeline.py   one pipeline, both transports
config/           profiles.yaml: the analyst-editable surface
datagen/          synthetic HL7 generator (seeded, labeled defect taxonomy)
benchmark/        MLLP load test + effectiveness replay, raw results committed
analytics/sql/    Power BI-facing views
docs/adr/         three architecture decision records
tests/            25 unit + integration tests (real TCP MLLP round-trips)

About

HL7 v2 validation gateway with MLLP transport, analyst-editable validation profiles, positional NAKs, dead-letter replay, and FHIR R4 conversion. Asyncio, FastAPI, PostgreSQL. Built for hospital interface and revenue cycle teams.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages