Zum Hauptinhalt springen

Monitoring

Jeder Java-Dienst der Plattform stellt Verwaltungs-Schnittstellen bereit, über die sich sein Zustand abfragen und seine Messwerte abholen lassen (Spring Boot Actuator). Damit beantworten Sie ohne Zugriff auf die Datenbank drei Fragen: Läuft der Dienst? Darf er Anfragen bekommen? Was tut er gerade?

Diese Schnittstellen sind die Grundlage für die Startreihenfolge im lokalen Stack, für die Proben im Cluster und für den Prometheus-Abruf.

Der Verwaltungsport​

Alle Verwaltungs-Schnittstellen liegen auf einem eigenen Port, 8181, nicht auf dem Anwendungsport. Das hat zwei Gründe, und beide sind der Grund, warum Sie sie nicht im Browser erreichen:

  • Der Reverse Proxy veröffentlicht einen ganzen Port, keinen Pfad. Ein Port, der dort nicht eingetragen ist, ist von außen nicht erreichbar.
  • Die Anwendungspfade des core-backend liegen unter /api — genau dem Präfix, das der Reverse Proxy weiterleitet. Der Verwaltungsserver erbt dieses Präfix nicht.

Der Port ist über die Umgebungsvariable MANAGEMENT_PORT einstellbar, wird aber in keiner Compose-Datei zum Host durchgereicht. Der Zugriff erfolgt deshalb von innerhalb des Containers (siehe Beispiele); im Cluster greifen ausschließlich der Kubelet und Prometheus darauf zu.

Den Port nicht veröffentlichen

Tragen Sie 8181 nicht in VIRTUAL_PORT und in keine Ingress-Regel ein. Die Endpunkte antworten ohne Anmeldung — das ist beabsichtigt, weil Proben und Abruf keinen Token mitbringen, und trägt genau so weit, wie der Port intern bleibt.

Welche Dienste beteiligt sind​

Dienst (Compose-Name)Aufgabe
core-backendZentrale REST-API
ts-serviceZeitreihen-API auf der Zeitreihendatenbank
core-schedulerGeplante Läufe und Workflow-Ausführungen
timeseries-ingestion-consumerSchreibt eingehende Messwerte
notification-consumerSchreibt eingehende Meldungen
mqtt-clientVerbindung zum MQTT-Broker
opcua-clientVerbindung zu den OPC-UA-Servern

Nicht beteiligt sind Keycloak, der hetida designer, der Reverse Proxy und die Datenbanken: das sind Fremdkomponenten mit eigenen Mechanismen. Die Datenbankcontainer melden ihren Zustand über pg_isready, Kafka über sein eigenes Werkzeug — im Compose-Statusfeld sehen Sie deshalb trotzdem für nahezu alles ein healthy.

Welche Schnittstellen es gibt​

Freigegeben ist in jedem Dienst genau diese Liste — es handelt sich um eine Positivliste, alles andere antwortet mit 401, auch wenn es die Schnittstelle grundsätzlich gäbe.

PfadWas Sie bekommen
/actuator/healthGesamtbild inklusive aller geprüften Abhängigkeiten
/actuator/health/readinessDarf diese Instanz Anfragen bekommen?
/actuator/health/livenessLebt diese Instanz, oder hilft nur ein Neustart?
/actuator/infoEckdaten zur Anwendung (derzeit ohne Inhalt)
/actuator/metricsListe der Messwerte, einzeln abrufbar unter /actuator/metrics/<name>
/actuator/prometheusAlle Messwerte in einem Abruf, im Prometheus-Format
Bewusst gesperrt

env, configprops, beans, heapdump und threaddump sind ausdrücklich ausgeschlossen: sie würden Datenbank- und Keycloak-Konfiguration beziehungsweise ein vollständiges Speicherabbild herausgeben. Ebenfalls nicht freigegeben ist loggers — Protokollstufen werden über Umgebungsvariablen und einen Neustart gesetzt, nicht schreibend über eine offene Schnittstelle.

Die drei Gesundheitssichten​

Sie sagen Verschiedenes, und der Unterschied entscheidet im Störungsfall, ob ein Dienst neu gestartet wird oder nicht.

SichtBedeutungAntwort im Fehlerfall
healthAlles, was der Dienst prüfen kann: Datenbank, Plattenplatz, eigener Zustand.503, sobald eine Prüfung fehlschlägt
readinessDiese Instanz kann bedient werden.503, solange sie nicht bereit ist
livenessDiese Instanz ist nicht hängengeblieben.503 nur bei einem Zustand, den ein Neustart behebt

readiness und liveness enthalten bewusst keine fremden Abhängigkeiten. Wäre die Datenbank in der Bereitschaft enthalten, fiele bei einem kurzen Aussetzer jede Instanz gleichzeitig aus der Lastverteilung — und zwar genau dann, wenn die Abhängigkeit zurückkommt. Wäre sie in der Lebendigkeit enthalten, würde die Plattform sich im Minutentakt selbst neu starten, ohne dass ein Neustart irgendetwas repariert.

Die einzige Ausnahme ist das core-backend: Seine Bereitschaft enthält zusätzlich tenantDataSources und meldet damit, ob die Instanz ihre Mandanten kennt. Dieser Zustand rastet ein — er ist rot bis zum ersten erfolgreichen Abgleich und bleibt danach grün; spätere Fehler stehen nur noch in den Details unter lastError.

Niemals /actuator/health als Probe verwenden

health wird rot, sobald eine Abhängigkeit kurz weg ist. Als Kubernetes-Probe oder als Compose-Healthcheck eingesetzt, startet es bei jedem Datenbank- oder Broker-Wackler sämtliche Pods neu. Für Proben sind readiness und liveness da; die Compose-Healthchecks der Plattform rufen genau deshalb /actuator/health/readiness auf.

Wie ausführlich geantwortet wird​

Das core-backend liefert die Einzelprüfungen mit — es antwortet auf /actuator/health mit den Komponenten db, diskSpace, ping, ssl und tenantDataSources. Die übrigen sechs Dienste antworten nur mit dem Gesamtstatus:

{"groups":["liveness","readiness"],"status":"UP"}

Das ist die Voreinstellung und für eine Probe ausreichend. Für eine Störungssuche schalten Sie die Details befristet über eine Umgebungsvariable zu — beim core-backend über MANAGEMENT_HEALTH_SHOW_DETAILS, bei den anderen Diensten über MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS, jeweils mit dem Wert always und einem Neustart des Containers.

Beispiele mit Docker Compose​

Alle Beispiele gehen vom Basis-Stack aus; bei mehreren Compose-Dateien hängen Sie die weiteren wie gewohnt mit -f an.

Überblick: Wer ist gesund?​

docker compose -f docker-compose-localhost.yaml ps --format "table {{.Service}}\t{{.Status}}"
SERVICE STATUS
core-backend Up 9 hours (healthy)
core-scheduler Up 10 hours (healthy)
notification-consumer Up 10 hours (healthy)
timeseries-ingestion-consumer Up 10 hours (healthy)
ts-service Up 10 hours (healthy)

Das healthy stammt aus dem Compose-Healthcheck, der im Container alle zehn Sekunden /actuator/health/readiness aufruft. Ein Dienst ohne Klammerzusatz hat keinen Healthcheck — er läuft, über seinen Zustand ist damit nichts gesagt.

Zustand eines einzelnen Dienstes abfragen​

Die Abbilder sind minimal und enthalten weder Shell noch curl; einzig /bin/wget ist zu diesem Zweck darin enthalten.

docker compose -f docker-compose-localhost.yaml exec core-backend \
/bin/wget -q -O - http://localhost:8181/actuator/health
{"status":"UP","groups":["liveness","readiness"],"components":{
"db":{"status":"UP","components":{"default":{"status":"UP","details":{"database":"PostgreSQL"}}}},
"diskSpace":{"status":"UP","details":{"free":902478356480,"threshold":10485760}},
"livenessState":{"status":"UP"},"ping":{"status":"UP"},"readinessState":{"status":"UP"},
"tenantDataSources":{"status":"UP","details":{
"lastSuccessfulSync":"2026-09-16T16:31:05Z","knownTenants":3,"poolsIncludingDefault":4}}}}

knownTenants ist die Zahl der Mandanten, die diese Instanz beim letzten Abgleich gelesen hat, poolsIncludingDefault die Zahl der Verbindungspools einschließlich des Standardpools — deshalb genau eins mehr.

Mit jq auf dem Host wird daraus eine lesbare Kurzfassung. Beim Weiterleiten in eine Pipe braucht exec das Flag -T, sonst versucht es ein Terminal zuzuweisen:

docker compose -f docker-compose-localhost.yaml exec -T core-backend \
/bin/wget -q -O - http://localhost:8181/actuator/health | jq '.components | map_values(.status)'
{"db":"UP","diskSpace":"UP","livenessState":"UP","ping":"UP","readinessState":"UP","ssl":"UP","tenantDataSources":"UP"}

Bereitschaft aller Dienste auf einmal​

for s in core-backend ts-service core-scheduler timeseries-ingestion-consumer \
notification-consumer mqtt-client opcua-client; do
printf '%-32s' "$s"
docker compose -f docker-compose-localhost.yaml exec -T "$s" \
/bin/wget -q -O - http://localhost:8181/actuator/health/readiness 2>/dev/null
echo
done
core-backend {"status":"UP","components":{"readinessState":{"status":"UP"},…}}
ts-service {"status":"UP"}
core-scheduler {"status":"UP"}
timeseries-ingestion-consumer {"status":"UP"}
notification-consumer {"status":"UP"}
mqtt-client {"status":"UP"}
opcua-client {"status":"UP"}

Eine leere Zeile bedeutet, dass der Dienst mit einem Fehlerstatus geantwortet hat — wget gibt dann nichts aus. Lassen Sie 2>/dev/null weg, um den Statuscode zu sehen (server returned error: HTTP/1.1 503).

Warum eine Probe fehlschlägt​

Docker bewahrt die letzten Probenläufe mitsamt ihrer Ausgabe auf. Das ist der schnellste Weg zu der Frage, seit wann ein Container nicht mehr gesund ist:

docker inspect --format '{{range .State.Health.Log}}{{.Start}} {{.ExitCode}} {{.Output}}{{"\n"}}{{end}}' \
hetida-platform-ts-service-1

Startreihenfolge​

Die Healthchecks sind kein reines Anzeigemittel: Dienste, die einen laufenden Vorgänger brauchen, warten über depends_on mit condition: service_healthy auf dessen Bereitschaft, statt nur auf den Start des Containers. Beim core-scheduler ist das die Bereitschaft des core-backend — andernfalls liefe sein erster geplanter Lauf ins Leere.

Eine Einschränkung gibt es dabei: Diese Bedingung steht bislang nur in der Auflage für selbst gebaute Abbilder (docker-compose-dev.yaml). Die Basisdatei zieht das veröffentlichte Release und wartet dort weiterhin nur auf den Start, weil ein Abbild aus der Zeit vor diesen Schnittstellen dauerhaft als unhealthy gälte und den core-scheduler damit nie starten ließe. Sie wird nachgezogen, sobald PLATFORM_VERSION auf ein Release mit den Healthchecks zeigt.

Messwerte abfragen​

docker compose -f docker-compose-localhost.yaml exec core-backend \
/bin/wget -q -O - http://localhost:8181/actuator/metrics/hikaricp.connections.active
{"name":"hikaricp.connections.active","description":"Active connections",
"measurements":[{"statistic":"VALUE","value":0.0}],
"availableTags":[{"tag":"pool","values":["default","tenant-customer"]}]}

availableTags nennt die Merkmale, nach denen sich der Wert aufteilen lässt; anhängen lassen sie sich als ?tag=pool:tenant-customer.

Mandantenpools erscheinen erst bei Benutzung

Ein Verbindungspool meldet seine Messwerte erst an, wenn er seine erste Verbindung öffnet. Kurz nach dem Start steht in /actuator/health deshalb bereits die volle Mandantenzahl, in den Messwerten aber nur pool="default". Das ist richtig so — ein nie benutzter Pool hält auch keine Verbindungen. Welche Mandanten eine Instanz kennt, steht in health, nicht in den Messwerten.

Prometheus und Grafana lokal​

Für den lokalen Betrieb liegt eine optionale Auflage bei, die Prometheus und Grafana neben den laufenden Stack stellt:

task monitoring:up # beendet wieder mit: task monitoring:down

Ohne Task-Werkzeug ist es derselbe Aufruf von Hand:

docker compose -f docker-compose-localhost.yaml -f docker-compose-monitoring.yaml \
up -d prometheus grafana

Danach erreichen Sie Prometheus unter http://localhost:9090 (Zielübersicht unter /targets) und Grafana unter http://localhost:3000. Beide hören nur auf der lokalen Schleife und sind über den Reverse Proxy nicht erreichbar; Grafana läuft ohne Anmeldung und mit bereits eingerichteter Datenquelle.

Ein Ziel ist immer DOWN

Die Prometheus-Konfiguration führt das core-backend zweimal: einmal als Container im Compose-Netz und einmal über den Docker-Host, weil es im IDE-Betrieb auf dem Rechner selbst läuft. Es ist immer nur eines von beiden vorhanden, das andere steht auf DOWN. Das ist der erwartete Zustand und kein Fehler.

Abgerufen wird lokal nur das core-backend. Die übrigen Dienste liefern dieselbe Schnittstelle — sie sind hier bloß nicht eingetragen; für einen Blick auf einen von ihnen ergänzen Sie in docker-volumes/prometheus/prometheus.yml ein Ziel der Form <compose-name>:8181.

Dashboards liegen bewusst nicht in diesem Stand, sondern beim Betrieb der jeweiligen Umgebung. Zwei gepflegte Sammlungen wären zwei Wahrheiten.

Messwerte, die sich lohnen​

MesswertWofür
http_server_requests_seconds_countAnfragen je Pfad, Methode und Statuscode — die schnellste Sicht auf 4xx- und 5xx-Wellen
hikaricp_connections_active / _pendingAuslastung der Datenbankverbindungen je Pool; anhaltendes pending heißt Warteschlange
jvm_memory_used_bytesSpeicherverbrauch je Bereich
logback_events_totalProtokollzeilen je Stufe; ein Anstieg bei level="error" ist oft das erste Signal
process_uptime_secondsLaufzeit seit dem Start — ein Rückfall auf null ist ein unbemerkter Neustart

Drei Eigenheiten sind beim Formulieren von Abfragen wichtig:

  • Die Marke für den Verbindungspool heißt pool, die Werte lauten default und tenant-<Kennung>.
  • Im Merkmal uri steht der Pfad ohne das /api-Präfix, unter dem ihn die Außenwelt aufruft.
  • http_server_requests_seconds wird ohne Klassengrenzen geführt. Mittelwerte und Zählungen sind damit möglich, Perzentile nicht.

Jeder Messwert erscheint zudem erst, nachdem er einmal vorgekommen ist — vor der ersten Anfrage gibt es keine Anfragemetrik.

Im Cluster​

Dort wird dieselbe Schnittstelle von zwei Seiten genutzt: Der Kubelet fragt readiness und liveness als Proben ab, Prometheus ruft /actuator/prometheus auf dem Verwaltungsport ab. Konfiguriert ist das im Infrastruktur-Repository, nicht in der Anwendung — in der Anwendung steht nur, was die Endpunkte liefern.

Wenn etwas nicht antwortet​

BeobachtungUrsache
exec: "curl": executable file not foundDie Abbilder enthalten keine Shell und kein curl. Verwenden Sie /bin/wget.
401 auf einen Actuator-PfadDer Endpunkt ist nicht freigegeben. Auf dem Verwaltungsport wird Gesperrtes abgewiesen, nicht als „nicht gefunden“ gemeldet.
404 auf /actuator/prometheusDer Container läuft auf einem älteren Abbild, das die Messwert-Schnittstelle noch nicht freigibt. Neu bauen beziehungsweise PLATFORM_VERSION hochziehen.
503 auf /actuator/health, Container trotzdem healthyGenau der beabsichtigte Fall: eine Abhängigkeit fehlt, die Bereitschaft hängt aber nicht an ihr.
Container dauerhaft unhealthy, Dienst antwortetDer Healthcheck greift ins Leere — falscher Port oder eine Startphase, die länger dauert als start_period.