Observability für Hilla-Anwendungen. Teil 1: Backend

René Wilby | 12.11.2024 Min. Lesezeit

Artikelreihe

Dies ist der erste von zwei Artikeln, die verschiedene Aspekte zu Observability von Hilla-Anwendungen beschreiben:

“Everything fails, all the time”

Dieses bekannte Zitat von AWS CTO Werner Vogels beschreibt kurz und knapp eine zentrale Motivation für die kontinuierliche Beobachtung von Systemen und Anwendungen. Ausfälle müssen frühzeitig erkannt und mit Hilfe möglichst vieler Informationen analysiert werden. Observability kann dabei ein hilfreicher Ansatz sein. Observability ist die Fähigkeit, den internen Zustand einer laufenden Anwendung von außen zu beobachten. Um dies zu erreichen, besteht Observability aus drei Bereichen: Logging, Traces und Metriken.

Observability für Spring Boot

Das Backend einer Hilla-Anwendung basiert standardmäßig auf Spring Boot. Erfreulicherweise existieren eine ganze Reihe von Tools, um ein Spring Boot basiertes Backend nach dem Observability-Ansatz kontinuierlich zu beobachten. Im Rahmen dieses Artikels kommen dafür OpenTelemetry und Micrometer zum Einsatz.

OpenTelemetry ist eine quelloffene und herstellerunabhängige Sammlung von APIs, SDKs und Tools. Mit Hilfe von OpenTelemetry können Telemetrie-Daten wie Logs, Traces und Metriken generiert, gesammelt und exportiert werden. OpenTelemetry lässt sich in viele beliebte Bibliotheken und Frameworks integrieren, darunter auch Spring Boot und React. Java-Anwendungen können OpenTelemetry per Java-Agent ohne zusätzliche Codeänderungen nutzen. Da der Java-Agent jedoch keine Native Images unterstützt, wird im weiteren Verlauf der Code-basierte Ansatz zur Instrumentierung betrachtet.

Micrometer ist ebenfalls Open Source und Hersteller-neutral. Es bietet eine Fassade für die gängigsten Observability-Systeme und ermöglicht es, JVM-basierten Anwendungen zu instrumentieren.

OpenTelemetry und Micrometer ergänzen sich sehr gut und sind daher eine gute Wahl für die kontinuierliche Beobachtung eines Spring Boot Backends.

Setup

Das Einbinden von OpenTelemetry in das Spring Boot Backend einer Hilla-Anwendung beginnt mit dem Hinzufügen der erforderlichen Dependencies:

<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry.semconv</groupId>
    <artifactId>opentelemetry-semconv</artifactId>
    <version>1.27.0-alpha</version>
</dependency>

Zusätzliche Konfigurationen sind für den Moment nicht erforderlich und folgen erst im weiteren Verlauf.

Tracing

Tracing ist hilfreich, um den Request-Flow in einer Hilla-Anwendung zu verstehen: Angefangen von einer HTTP-Anfrage, über Methodenaufrufe bis hin zu Datenbankabfragen. Trace-IDs und Span-IDs spielen in diesem Zusammenhang eine wichtige Rolle, da sie hilfreich sind, um zusammenhängende Aktionen zu erkennen.

Tracing kann für das Backend einer Hilla-Anwendung mit Hilfe von Spring Boot Actuator und Micrometer Tracing implementiert werden. Dafür müssen zunächst einige weitere Dependencies zum Hilla-Projekt hinzugefügt werden:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

Die erste Abhängigkeit ist für Spring Boot Actuator. Erfreulicherweise bietet Spring Boot Actuator eine Autokonfiguration für Micrometer Tracing. Das bedeutet u.a., dass Micrometer Tracing die Trace-IDs und Span-IDs als MDC-Attribute zur Verfügung stellt, was im weiteren Verlauf sehr nützlich ist, um Traces und Log-Ausgaben zusammenführen zu können.

Mit Hilfe der beiden weiteren Abhängigkeiten wird gewährleistet, dass die von Micrometer gesammelten Traces an OpenTelemetry übergeben und mit Hilfe des OpenTelemetry-Protokoll (OTLP) in ein geeignetes Backend exportiert werden können.

Im Anschluss kann das Tracing weiter konfiguriert werden:

management.tracing.sampling.probability=1.0
management.otlp.tracing.endpoint=http://corporate.observability.example.com:4317/v1/traces

Standardmäßig würde Spring Boot Actuator nur für 10 % aller Anfragen Traces erstellen und exportieren, um das Trace-Backend nicht zu überlasten. Diese Konfiguration kann zum Beispiel auf 100 % geändert werden, so dass zu jeder Anfrage Traces an das Trace-Backend gesendet wird. Dies kann beispielsweise während der lokalen Entwicklung hilfreich sein.

Darüber hinaus wird in der Konfiguration der Endpunkt für ein geeignetes Trace-Backend festgelegt, in das alle Traces exportiert werden sollen. In diesem Beispiel kommt SigNoz als Backend zum Einsatz.

Die gezeigte Konfiguration ist ausreichend, so dass nun Traces für alle HTTP-Anfragen vom Frontend zum Backend einer Hilla-Anwendung in das Trace-Backend exportiert werden und dort analysiert werden können:

All traces

Zu jedem Trace sind Detailinformationen, wie URL, HTTP-Methode und HTTP-Status-Code einsehbar:

Trace details

Darüber hinaus ist auch die Dauer der Anfragen als Metrik verfügbar, so dass zum Beispiel Dashboards mit der Dauer der Anfragen erstellt werden können:

Trace duration

Zusätzlich zu den gezeigten Traces können mit Hilfe der Micrometer Observation API eigene Spans erstellt werden, um damit bestimmte Funktionen in der Hilla-Anwendung überwachen zu können.

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.application.entities.Label;
import com.example.application.repositories.LabelRepository;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.BrowserCallable;
import com.vaadin.hilla.crud.CrudRepositoryService;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;

@BrowserCallable
@AnonymousAllowed
public class LabelService extends CrudRepositoryService<Label, Long, LabelRepository> {

    @Autowired
    private ObservationRegistry observationRegistry;

    public List<Label> getLabels() {
        Observation observation = Observation.start("get-labels", observationRegistry);
        List<Label> labels = super.getRepository().findAll();
        observation.stop();
        return labels;
    }
}

In dem Code-Beispiel wird ein eigener Span mit der Bezeichnung get-labels erstellt. Der Span zeichnet die Ausführung einer Methode eines Spring Data Repositories auf. Im Trace-Backend kann dieser Span dann in Verbindung mit den bereits bekannten Traces analysiert werden.

Custom span

Logging

Logs spielen für Observability eine wichtige Rolle, da sie eine Fülle wertvoller Informationen enthalten können, die für die Überwachung einer Hilla-Anwendung und für die Analyse von Fehlern hilfreich sein können. Im Kontext von Observability geht es beim Logging darum, die bestehenden Log-Ausgaben mit den anderen Datenquellen zu verbinden. Um dies zu erreichen, müssen die Log-Ausgaben zunächst für OpenTelemetry bereitgestellt werden. Dies kann bspw. über einen Log-Appender für Logback erfolgen. Dafür muss zunächst die folgende Dependency zum Hilla-Projekt hinzugefügt werden:

<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-logback-appender-1.0</artifactId>
    <version>2.8.0-alpha</version>
</dependency>

Anschließend kann dieser Appender in die bestehende Logback-Konfiguration hinzugefügt werden:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
    <appender name="OpenTelemetry" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
        <captureExperimentalAttributes>true</captureExperimentalAttributes>
        <!-- Micrometer adds MDC attributes 'traceId' and 'spanId' -->
        <captureMdcAttributes>*</captureMdcAttributes>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="OpenTelemetry"/>
    </root>
</configuration>

Die Konfiguration für den Appender sieht ebenfalls vor, dass vorhandene MDC-Attribute wie Trace-ID und Span-ID berücksichtigt und in die Log-Ausgabe mit aufgenommen werden sollen. Erfreulicherweise stellt Micrometer Tracing die Trace-ID und Span-ID per MDC genau für diese Zwecke zur Verfügung.

Um den Appender korrekt verwenden zu können, muss zunächst das OpenTelemetry SDK für die Hilla-Anwendung initialisiert und für den Appender konfiguriert werden. Dies sollte möglichst frühzeitig passieren und kann beispielsweise in der Datei Application.java im Hilla-Projekt erfolgen:

import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.theme.Theme;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter;
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.ServiceAttributes;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@Theme(value = "hilla-observability-example")
public class Application implements AppShellConfigurator {

    public static void main(String[] args) {        
        // Initialize OpenTelemetry as early as possible
        OpenTelemetry openTelemetry = initializeOpenTelemetry();

        // Install OpenTelemetry in logback appender
        OpenTelemetryAppender.install(openTelemetry);

        SpringApplication.run(Application.class, args);
    }

    private static OpenTelemetry initializeOpenTelemetry() {
        OpenTelemetrySdk sdk =
            OpenTelemetrySdk.builder()
                .setLoggerProvider(
                    SdkLoggerProvider.builder()
                        .setResource(
                            Resource.getDefault().toBuilder()
                                .put(ServiceAttributes.SERVICE_NAME, "hilla-observability-example")
                                .build())
                        .addLogRecordProcessor(
                            BatchLogRecordProcessor.builder(
                                    OtlpGrpcLogRecordExporter.builder()
                                        .setEndpoint("http://corporate.observability.example.com:4317")
                                        .build())
                                .build())
                        .build())
                .build();

        // Add hook to close SDK, which flushes logs
        Runtime.getRuntime().addShutdownHook(new Thread(sdk::close));

        return sdk;
    }
}

Das OpenTelemetry SDK wird mit einem LoggerProvider erstellt. Dieser erhält einen LogRecordProcessor in Verbindung mit einem LogRecordExporter. Letzterer überträgt die Log-Ausgaben vom Appender an ein geeignetes Backend per OpenTelemetry-Protokoll (OTLP).

In einem passenden Backend, wie bspw. SigNoz, können nun alle Log-Ausgaben des Spring Boot Backends der Hilla-Anwendung analysiert werden.

Logs

Zusätzlich zum Namen der Hilla-Anwendung, der als Resource im LoggerProvider konfiguriert wurde, sind auch die eigentliche Log-Nachricht und die Trace-ID und Span-ID sichtbar.

Auf Grundlage dieser Log-Daten könnten in einem passenden Backend nun bspw. Alerts oder Dashboards erstellt werden.

Metriken

Der dritte und letzte Bereich von Observability für das Spring Boot Backend einer Hilla-Anwendung sind Metriken. Metriken sind hilfreich, um den Zustand verschiedener Bestandteile einer Anwendung zu überwachen. Spring Boot Actuator unterstützt Micrometer Metrics für die Erstellung von Metriken. Spring Boot Actuator erstellt die dafür erforderliche MeterRegistry automatisch. Damit die erhobenen Metriken exportiert werden können, muss folgende Dependency zum Hilla-Projekt hinzugefügt werden:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-otlp</artifactId>
</dependency>

Anschließend kann die URL für ein passendes Backend konfiguriert werden, an das die Metriken mit Hilfe des OpenTelemetry-Protokolls (OTLP) übertragen werden sollen:

management.otlp.metrics.export.url=http://corporate.observability.example.com:4318/v1/metrics

Erfreulicherweise unterstützt Spring Boot Actuator eine Vielzahl verschiedener Metriken ohne zusätzliche Konfiguration. So können zum Beispiel JVM-Metriken, wie Speicherverbrauch, die Anzahl der aktiven Threads oder die Anzahl der geladenen Klassen überwacht werden.

JVM metric

Aber auch für anderen Bestandteile einer Hilla-Anwendung stehen Metriken zur Verfügung, zum Beispiel für Hibernate, JDBC oder Spring Data. Die nachfolgende Grafik zeigt ein Dashboard in SigNoz mit der Anzahl der ausgeführten Datenbank-Queries.

Hibernate metric

Fazit

Für das Spring Boot Backend einer Hilla-Anwendung können Logs, Traces und Metriken mit Hilfe von OpenTelemetry und Micrometer generiert bzw. gesammelt werden. Die gesammelten Informationen können über das OpenTelemetry-Protokoll (OTLP) an ein geeignetes Backend wie SigNoz übertragen und darin analysiert und ausgewertet werden. Somit stehen eine Vielzahl nützlicher Informationen an einem zentralen Ort zur Verfügung, um eine Hilla-Anwendung kontinuierlich zu überwachen und um Fehler und Probleme frühzeitig erkennen zu können.

Teil 2 der Artikelreihe befasst sich mit Observability für das React Frontend einer Hilla-Anwendung.