Push oder Pull? Spring Boot 4 mit OpenTelemetry

Veröffentlicht:

Mit Spring Boot 4 ist es angenehm einfach geworden, Observability-Daten im OpenTelemetry-Format zu versenden. Logs, Traces und Metriken können nun mit wenigen Zeilen an Konfiguration zu beliebigen OpenTelemetry-Backends (oder einem Collector) gesendet werden. Aber möchte man das überhaupt? Nur weil es geht, heißt es nicht, dass man es auch machen sollte.

Werfen wir einen Blick auf die einzelnen Signale und wie diese übertragen werden können.

Traces

Bei Traces ist die Situation recht klar. Auch ohne OpenTelemetry ist hier der gängige Weg, dass unsere Anwendung den aktiven Part übernimmt. Traces werden automatisch oder über die Micrometer API erstellt und an ein entsprechendes Backend gesendet. Ob es OpenZipkin Brave, Jaeger oder Tempo (über OpenTelemetry) ist, oder unsere Daten zunächst an einen OpenTelemetry Collector gehen, macht für die grundsätzliche Architektur keinen Unterschied.

Je nach Setup kommt dazu einer der beiden Micrometer-Tracer zum Einsatz:

Letzterer ist im Spring Starter spring-boot-starter-opentelemetry bereits enthalten.

Metriken

Für viele Jahre war Prometheus der Standardweg, um Metriken einer Spring Boot Anwendung abzufragen. Dazu ruft ein externer Prometheus-Server regelmäßig den Endpunkt actuator/prometheus unserer Anwendung auf. Die Anwendung selbst stellt also nur die Daten bereit. Prometheus übernimmt hier den aktiven Part (Pull).

Soll OpenTelemetry zum Einsatz kommen, kann unsere Anwendung die Metriken aber auch selbst versenden. So kann auf Prometheus als separaten Server verzichtet werden. Die Daten können stattdessen bei VictoriaMetrics, ClickHouse (ClickStack) oder beim Cloud-Anbieter des Vertrauens landen. Hier ist unsere Anwendung also aktiv in der Verantwortung (Push).

Eine weitere, hybride, Lösung ergibt sich durch den Einsatz des OpenTelemetry Collectors. Durch einen OpenTelemetry Receiver für Prometheus kann dieser den actuator/prometheus Endpunkt abfragen (Pull) und Metriken im OpenTelemetry-Format weitersenden. Auch wenn dieser Receiver noch nicht als stable gekennzeichnet ist (aktuell beta), ist es möglich, ihn produktiv zu nutzen. Bei verteilten Collector-Instanzen sollte jedoch darauf geachtet werden, dass diese keine Anwendungen doppelt auslesen.

Der Push-Ansatz ist attraktiv, da kein Umweg über ein anderes Format gegangen werden muss. Auch können so Dienste, die im Pull-Setup benötigt werden, eventuell entfallen (Prometheus) oder vereinfacht werden (OpenTelemetry Collector).

Gleichzeitig wird dadurch unsere Anwendung selbst mit Aufgaben belastet, für die sie streng genommen gar nicht vorgesehen ist. Das Aufbereiten, Puffern und Versenden von Metriken nimmt CPU und Memory in Anspruch. Ressourcen, die womöglich besser für das Bearbeiten von fachlichen Anfragen verwendet werden.

Bei kurzlebigen Workloads (Serverless, kurze Jobs) oder sehr einfachen Setups kann dieser Ansatz durchaus tragen. Bei anderen Setups ist für Metriken die Pull-Variante über einen Actuator-Endpunkt jedoch der sichere Weg.

Logs

Beim Verarbeiten von Logs zeichnet sich ein ähnliches Bild wie bei Metriken ab.

Der herkömmliche Weg ist es, nach stdout zu loggen. Diese Logs werden dann durch Tools wie Grafana Alloy bzw. den OpenTelemetry Collector ausgelesen und verarbeitet.

Diese Methode entkoppelt die Anwendung vollständig von der Monitoring-Infrastruktur. Die App schreibt lediglich einen Stream von Text oder JSON. Die Verantwortung für den Transport und die Persistierung liegt außerhalb.

Demgegenüber steht erneut der Push-Ansatz via OpenTelemetry. Hierbei kann für die Anwendung ein spezieller Appender (z. B. für Logback oder Log4j2) konfiguriert werden. Dieser schickt die Logs direkt im OpenTelemetry-Format über das Netzwerk an ein Backend oder einen Collector.

Für die meisten Produktionsumgebungen ist der Push-Weg bei Logs jedoch mit zu hohen Risiken für die Fehlerdiagnose behaftet. In kritischen Fehlersituationen, wie einem Crash beim Start, einem OutOfMemoryError, oder Netzwerkfehlern kommen genau die Logs, die die Ursache erklären würden, niemals im Backend an. Per stdout ist das Ganze daher wesentlich robuster.

Erneut: Bei kurzlebigen Workloads oder sehr einfachen Setups kann dieser Ansatz dennoch gewählt werden.