Context and Problem Statement

De communicatiemodule bestaat uit drie zelfstandige .NET 10 Worker Services die via RabbitMQ en een gedeelde database met elkaar communiceren. Een notificatiejob raakt alle drie containers: container 1 ontvangt het event, container 2 plant de notificatie, container 3 verstuurt hem. Zonder gecentraliseerde observability is het onmogelijk om te achterhalen wat er is misgegaan als een notificatie niet aankomt, waar de vertraging zit als het systeem traag is, of welke container verantwoordelijk is voor een fout.

De niet-functionele eisen stellen dat de werking van de communicatiemodule volledig inzichtelijk moet zijn via monitoringtooling op basis van OpenTelemetry, en dat er een real-time dashboard beschikbaar moet zijn waarop OpenMRS-beheerders de status van berichten, throughput en foutmeldingen kunnen volgen.

De centrale vraag is welke observability stack wordt gebruikt, hoe de drie pilaren (logs, metrics, traces) worden geïnstrumenteerd, en hoe trace context wordt doorgegeven tussen de drie containers via RabbitMQ zodat één notificatiejob als één samenhangende trace zichtbaar is.


Considered Options

Optie A: Hosted observability platform (Datadog, New Relic)

Een extern gehost platform dat logs, metrics en traces ontvangt en visualiseert.

Voordelen:

  • Snel op te zetten zonder eigen infrastructuur
  • Uitgebreide dashboards en alerting out of the box
  • Schaal mee zonder configuratie

Nadelen:

  • Patiëntdata en medische afspraakdata verlaat de eigen infrastructuur en belandt op servers van derden
  • Kosten lopen snel op bij hogere volumes
  • Vendor lock-in: instrumentatiecode raakt gekoppeld aan het specifieke platform
  • Onaanvaardbaar gezien de privacyvereisten rondom medische data in dit project

Optie B: Zelfgebouwde LGTM stack

De LGTM-stack van Grafana Labs combineert Loki voor logs, Grafana Tempo voor traces, Prometheus voor metrics, en Grafana voor visualisatie. Alle componenten draaien als Docker containers op de eigen VPS. De applicaties exporteren telemetry via de OpenTelemetry standaard naar een lokale OpenTelemetry Collector die de data distribueert naar de juiste backend.

Voordelen:

  • Alle telemetry data blijft op de eigen infrastructuur, geen patiëntdata naar externe partijen
  • OpenTelemetry is vendor-neutraal: instrumentatiecode is niet gekoppeld aan een specifiek platform
  • De volledige stack is beschikbaar als Docker Compose setup wat aansluit op de bestaande infrastructuur
  • Loki, Tempo en Prometheus zijn volledig integreerbaar in één Grafana dashboard
  • Gratis en open source

Nadelen:

  • Vereist meer configuratie dan een hosted oplossing
  • De stack voegt meerdere extra containers toe aan Docker Compose
  • Vereist beheer van retentieperiodes en opslagcapaciteit op de VPS

Decision Outcome

Gekozen: Optie B, Zelfgebouwde LGTM stack

Justification Een hosted observability platform is onaanvaardbaar omdat medische patiëntdata de eigen infrastructuur niet mag verlaten. De LGTM stack biedt alle benodigde functionaliteit volledig on-premise en sluit direct aan op de bestaande Docker Compose infrastructuur. OpenTelemetry als instrumentatiestandaard voorkomt vendor lock-in: de applicatiecode is gekoppeld aan de OpenTelemetry API, niet aan Loki, Tempo of Prometheus specifiek. Als in de toekomst een component van de stack wordt vervangen hoeft de instrumentatiecode in de applicaties niet te worden aangepast.


De drie pilaren

Logs

Alle drie containers schrijven gestructureerde logs via het .NET ILogger systeem. Logs worden via Promtail (Docker socket) verzameld en doorgestuurd naar Loki. Logs bevatten altijd de volgende velden: timestamp, severity, traceId, spanId, containerName, tenantId, appointmentUuid en een beschrijvend message veld. Patiëntidentificatoren zoals patientName en phoneNumber worden nooit gelogd conform ADR 8.

Logs worden via de OpenTelemetry Collector doorgestuurd naar Loki. In Grafana zijn logs doorzoekbaar op tenantId, appointmentUuid, containerName en traceId. De traceId in een log is klikbaar in Grafana en opent direct de bijbehorende trace in Tempo.

Metrics

Alle drie containers exposen metrics via de OpenTelemetry .NET SDK. De OpenTelemetry Collector ontvangt deze metrics en exposeert ze als Prometheus scrape endpoint. Prometheus scraped elke 15 seconden.

De volgende metrics worden bijgehouden: Container 1: verwerkte berichten, verwerkingsfouten, en databaseschrijfduur. Container 2: gepubliceerde en mislukte notificatiejobs. Container 3: geslaagde en mislukte provider-aanroepen, aanroepduur, en retry-pogingen.

Aanvullend worden .NET runtime metrics (GC, thread pool, exceptions) en infrastructuurmetrics van de containers en de VPS host automatisch verzameld.

Traces

Traces volgen één notificatiejob van het moment dat de OpenMRS plugin een event publiceert tot het moment dat container 3 de bevestiging van de provider ontvangt. Een trace bestaat uit de volgende spans: plugin publiceert event naar RabbitMQ, container 1 ontvangt en verwerkt het event, container 1 schrijft naar de database, container 2 leest de database en publiceert een notificatiejob, container 3 ontvangt de notificatiejob en roept de provider aan.

Traces worden opgeslagen in Grafana Tempo en zijn doorzoekbaar op traceId, tenantId en appointmentUuid.


Context propagation tussen containers

Omdat de drie containers via RabbitMQ communiceren en niet via directe HTTP aanroepen moet de trace context expliciet worden doorgegeven via RabbitMQ berichtheaders volgens de W3C Trace Context standaard.

Wanneer container 1 een notificatiejob publiceert naar notification.queue injecteert het de W3C traceparent header in de berichtheaders. De traceparent header heeft het formaat 00-{traceId}-{parentSpanId}-{flags}, bijvoorbeeld 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01. De traceId blijft ongewijzigd door alle containers heen zodat de volledige keten als één trace zichtbaar is in Tempo.

Wanneer container 2 het bericht van notification.queue consumeert extraheert het de traceparent header en continueert de trace. Wanneer container 2 een nieuw notificatiejob publiceert naar de interne queue injecteert het de bijgewerkte traceparent met zijn eigen spanId als nieuwe parentId. Container 3 herhaalt dit patroon.

In de .NET implementatie wordt de traceparent header handmatig geïnjecteerd in de RabbitMQ BasicProperties.Headers bij het publiceren (via IMessageSender.InjectTraceContext) en handmatig uitgelezen bij het consumeren. Volledige automatische context propagation over alle drie containers is een bekende beperking van de huidige implementatie; spans zijn per container geïsoleerd en niet als één samenhangende trace zichtbaar in Tempo.


Grafana Dashboard

Het real-time dashboard voor OpenMRS-beheerders toont de volgende panelen: Notificatiestatus: een tabel met de meest recente notificaties uitgesplitst per tenantId met de status (verstuurd, mislukt, in wachtrij) en de bijbehorende appointmentUuid.

Throughput: een tijdreeksgrafiek van het aantal verwerkte notificaties per minuut over de afgelopen 24 uur uitgesplitst per provider.

Foutrate: een tijdreeksgrafiek van het aantal mislukte provider-aanroepen per minuut met een drempelwaarde alerting als de foutrate boven 5 procent komt.

Dead-letter queue: een getal dat het huidige aantal berichten in notification.dlq toont met een rood signaal als dit groter dan nul is. (DLQ niet geïmplementeerd in huidige versie.)

Provider latency: een histogram van de gemiddelde responstijd per provider over de afgelopen 24 uur (via communicatie_mfs_provider_call_duration_milliseconds).

PENDING_CANCELLATION timeout: een tabel van afspraken die langer dan 5 minuten in PENDING_CANCELLATION status staan. (Timeout-monitoring niet geïmplementeerd in huidige versie.)


Consequences

Good, because:

  • Alle telemetry data blijft op de eigen infrastructuur en verlaat nooit de VPS
  • OpenTelemetry instrumentatie is vendor-neutraal en niet gekoppeld aan Loki, Tempo of Prometheus specifiek
  • De traceId koppelt logs, metrics en traces aan elkaar zodat een beheerder vanuit een foutmelding in het dashboard direct de volledige trace kan openen
  • Context propagation via W3C Trace Context maakt de volledige keten van één notificatiejob door alle drie containers zichtbaar als één samenhangende trace

Neutraal, because:

  • W3C Trace Context propagation via RabbitMQ berichtheaders moet expliciet worden geïmplementeerd in alle drie containers; dit is gedocumenteerd als implementatieaandachtspunt
  • Retentieperiodes voor logs en traces moeten worden geconfigureerd op de VPS om opslagcapaciteit te beheersen; standaard wordt 30 dagen aangehouden voor traces en 90 dagen voor logs

Bad, because:

  • De LGTM stack voegt meerdere extra containers toe aan Docker Compose wat de opstartcomplexiteit vergroot
  • Prometheus scraping elke 15 seconden genereert extra netwerkverkeer tussen de containers; bij hoge load kan dit worden verhoogd naar 30 seconden

More Information

Implementatieaandachtspunten:

  • De OTel .NET SDK wordt bij het opstarten geconfigureerd met OTLP export naar de Collector
  • Logs worden via Promtail (Docker socket) naar Loki gestuurd in plaats van via de OTel Collector
  • Grafana is bereikbaar op poort 3000
  • W3C Trace Context wordt geïnjecteerd in RabbitMQ berichtheaders bij het publiceren; volledige end-to-end trace over alle drie containers is een bekende beperking van de huidige versie
  • De service name per container wordt geconfigureerd via een environment variabele
  • Zie ADR 6 voor de drie-container architectuur waarop deze observability stack van toepassing is
  • Zie ADR 8 voor de logging restricties rondom patiëntdata die ook van toepassing zijn op de telemetry pipeline