Skip to content
LambogeniusDocs

HL7 Edge Gateway

Several sources and destinations

One route for several senders and several systems: a queue per destination, and filters on any field.

Runs onWindows · Linux · macOS · DockerFree developer license for test traffic. Production traffic needs a paid license.

A route can take messages from several sources and deliver them to several destinations, in any mix of types: MLLP, HTTP(S) REST and SOAP, Service Bus and SFTP. Use it when one feed goes to several systems, or several senders feed the same systems.

[[route]]
name = "adt-feed"
mode = "store_and_forward"
on_unmatched = "accept"                   # no filter matches: "accept" (AA, counted) or "reject" (AR)
sources = [
  { type = "mllp", listen = "10.10.10.10:7890" },
  { type = "http", listen = "0.0.0.0:8443", path = "/hl7/adt", tls = { … }, auth = { type = "api_key", keys = { … } } },
]

[[route.destinations]]
name = "lab"                              # required with several destinations
type = "http"
url = "https://lab.example.org/hl7/admissions"
filter = { message_type = ["ADT^A01", "ADT^A04", "ADT^A08"], fields = { "PV1-2" = ["I", "E"] } }

[[route.destinations]]
name = "his"
type = "mllp"
address = "his.hospital.local:6661"
store_and_forward = { on_reject = "dead_letter" }   # this destination's own settings

[[route.destinations]]
name = "archive"
type = "sftp"
host = "sftp.archive.local"
username = "hl7"
private_key = "keys/archive"
host_key = "SHA256:…"
directory = "inbox"
batch_format = "hl7_batch"

Sources

Every source feeds the route's queues, in the order messages arrive, whichever source they came by. Each source answers its own sender with the ACK, as on a route with one source. Listening sources each take their own address, and HTTP sources can still share a port by path.

Destinations

  • A queue each. Every destination has a queue of its own, so one that is down or blocked holds up no other. The sender gets AA once the message is stored for every destination it goes to.
  • Their own settings. Retries, on_reject and max_attempts come from the route's store_and_forward, or from the destination's own, which replaces them for that destination. Dead letters, pause and Retry now are per destination too.
  • Names. With several destinations each needs a name (letters, digits, - and _): the console, the logs, the metrics and the admin API use it.
  • Upgrading. A route with one source and one destination works as before, and keeps its queue.

Filters

A destination with a filter gets only the messages the filter matches; without one it gets every message. A filter names MSH fields and segment field paths, each with the values it accepts:

filter = { message_type = ["ADT"] }                       # any ADT
filter = { message_type = ["ADT^A01", "ADT^A08"] }        # these events
filter = { message_type = ["ORU^R01"], sending_facility = ["NORTH*"] }
filter = { fields = { "PV1-2" = ["I", "E"] } }            # inpatient or emergency
filter = { fields = { "OBR-4.1" = ["GLU", "K", "NA"] } }  # any OBR with one of these tests
filter = { fields = { "PID-3.4.1" = ["HOSP"], "ZPI-1" = ["*"] } }   # assigning authority, and a ZPI segment
FieldMatches
message_typeMSH-9, by component: ADT is any ADT, ADT^A08 that trigger event. The message structure in MSH-9.3 isn't compared.
sending_application, sending_facility, receiving_application, receiving_facility, processing_idMSH-3, 4, 5, 6 and 11, by their first component: MAIN^1.2.3^ISO matches MAIN.
fields = { "SEG-n.c.s" = [ … ] }Any segment field, component or subcomponent: PV1-2, OBR-4.1, PID-3.4.1, Z-segments too. It matches if any occurrence of the segment, and any repetition of the field, has a matching value; OBR[2]-4.1 and PID-3[2].1 pick one.
  • And, or: the values of one field are alternatives (OR); every field the filter names must match (AND).
  • Values compare without regard to case, with * and ? wildcards. An empty or missing field has the value ""; a segment that isn't in the message never matches.
  • No match: when no destination takes a message, on_unmatched decides: "accept" (the default) acknowledges it and counts it as unmatched; "reject" answers AR.

Any and not

For an OR across different fields, list whole conditions under any: at least one of them must match. To exclude messages, put a condition under not: a message that matches it is left out. Each condition is a filter of its own, so it can have its own any and not, and both join the filter's other fields with AND. A filter can be just a not: every message except those.

# NOT: every ADT except A03
filter = { message_type = ["ADT"], not = { message_type = ["ADT^A03"] } }

# OR across fields: an A08, or anything from NORTH
filter = { any = [ { message_type = ["ADT^A08"] }, { sending_facility = ["NORTH"] } ] }

# All together, as a table of its own:
# (an ADT) AND (an emergency patient OR an inpatient on ward 4W) AND NOT (a test message)
[route.destinations.filter]
message_type = ["ADT"]
any = [
  { fields = { "PV1-2" = ["E"] } },
  { fields = { "PV1-2" = ["I"], "PV1-3.1" = ["4W"] } },
]
not = { processing_id = ["T"] }

The full syntax, recipes, and how to check a filter against sample messages: Filters.

Pass-through with several destinations

Mark one destination primary = true. It is delivered while the sender waits, and its answer is the ACK, as on a route with one destination. The other destinations get a copy of what the primary accepted (AA), through queues of their own; a message the primary rejects isn't copied. The primary gets every message, so it can't have a filter. See the Results to the EHR, alerts by order example.

[[route]]
name = "orders"
mode = "passthrough"
source = { type = "mllp", listen = "10.10.10.10:7891" }

[[route.destinations]]
name = "meditech"
type = "mllp"
address = "meditech.hospital.local:6661"
primary = true                            # its ACK goes back to the sender

[[route.destinations]]
name = "audit"
type = "http"
url = "https://audit.example.org/hl7"     # gets a copy of what MEDITECH accepted

In the console, the API and the metrics

The console lists a route's sources, and a row for each destination with its filter, state, queue, dead letters and last error; select a destination to see its queue. The admin API takes ?destination=<name> on the message, pause, resume and retry-now calls; without it, pause, resume and retry-now apply to every destination of the route. Queue metrics carry a destination label, and hl7edge_unmatched_total counts messages no filter took. In the Helm chart, give each source its own port. See the One ADT feed to three systems example.