1. Einleitung

Warum Verwendet man Telemetrie? Um diese Frage zu beantworten, müssen wir uns zuerst mit dem Problem beschäftigen, das Telemetrie löst.

1.1. Welches Problem gibt es?

  • Aufgabenstellung

    • Fehlerquellen identifizieren

    • Performance optimieren

  • Herausforderungen

    • Analyse von komplexen Anwendungen (z.B.: k8s).

    • Mangelnde Übersicht und fehlende Kennzahlen.

1.2. Was ist Telemetrie?

  • Definition: Automatische Sammlung von Metriken, Logs und Events.

  • Aufgabe: Systemzustand analysieren und optimieren.

  • Bestandteile: Metriken (Zähler, Timer), Logs, Tracing.

1.3. Wie Telemetrie dieses Problem löst

  • Einblicke in Systemzustände.

  • Erleichtert Fehlerdiagnose und Performance-Monitoring.

    • Option einer grafischen Darstellung

    • Automatisierte Alarme und Benachrichtigungen.

1.4. Beispiel: Kubernetes-Cluster

  • Metriken: CPU- und Speicherauslastung.

  • Logs: Fehlermeldungen und Debug-Informationen.

  • Tracing

    • Nachverfolgung von Requests und wie lange sie wofür brauchen.

    • Bottlenecks identifizieren

2. Micrometer

2.1. Was ist Micrometer?

2.2. Micrometer in der Praxis

  • Integration mit Quarkus:

    • Einfaches Hinzufügen durch Erweiterungen (micrometer-registry-prometheus).

    • Erfassung von JVM-Metriken (Heap, Garbage Collection, Threads).

  • Visualisierung: Daten an Prometheus senden, mit Grafana anzeigen.

2.4. Schritt 1: Micrometer in Quarkus einbinden

quarkus-micrometer-prometheus-Dependency hinzufügen.

2.5. Schritt 2: Metriken sammeln

@Path("/palindrome")
@Produces("application/json")
public class PalindromeResource {
    private final MeterRegistry registry;
    private final LinkedList<String> list = new LinkedList<>();

    public PalindromeResource(MeterRegistry registry) {
        this.registry = registry;
        registry.gaugeCollectionSize("palindrome.list.size", Tags.empty(), list);
    }

    @GET
    @Path("counter/check/{input}")
    public boolean checkPalindromeCounter(@PathParam("input") String input) {
        list.add(input);

        registry.counter("palindrome.counter").increment();
        boolean result = internalCheckPalindrome(input);
        return result;
    }

    @GET
    @Path("timer/check/{input}")
    public boolean checkPalindromeAndTimer(@PathParam("input") String input) {
        list.add(input);

        Timer.Sample sample = Timer.start(registry);
        boolean result = internalCheckPalindrome(input);
        sample.stop(registry.timer("palindrome.timer"));
        return result;
    }

    private boolean internalCheckPalindrome(String input) {
        int left = 0;
        int right = input.length() - 1;

        while (left < right) {
            if (input.charAt(left) != input.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }

    @DELETE
    @Path("empty-list")
    public void emptyList() {
        list.clear();
    }
}

2.6. Schritt 3: Package erstellen

mvn package

2.7. Schritt 4: Container mit Quarkus, Prometheus und Grafana starten

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    restart: always
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    restart: always
    networks:
      - monitoring

  quarkus:
    build:
      context: ../../../
      dockerfile: ./src/main/docker/Dockerfile.jvm
    container_name: quarkus
    ports:
      - "8080:8080" # Optional, for host access
    restart: always
    networks:
      - monitoring

networks:
  monitoring:
    driver: bridge

2.8. Schritt 5: Prometheus

2.9. Schritt 6: Grafana

2.10. Alternativen zu Micrometer

  • Dropwizard Metrics:

    • Älter, weniger flexibel.

  • Spring Boot Actuator (eingebaut, aber weniger universell).

  • OpenTelemetry (vollständige Lösung für Telemetrie inkl. Tracing).

2.11. Beispiel

Das Beispiel ist in diesem Repository unter quarkus-micrometer-demo zu finden.

3. Tracing

3.1. Welches Problem gibt es?

  • Identifizierung von Performance-Problemen

  • Bottleneck identifizieren

  • Warum dauert ein Request so lange?

3.2. Was ist Tracing?

  • Nachverfolgung von Requests auf der Seite des Servers

  • Analyse von vielen Requests

  • Zeit zwischen einzelnen Schritten messen

3.3. Was ist ein Trace?

spans-traces

Ein Trace besteht aus Aufgaben über eine Zeitspanne, sogenannte Spans.

Theoretisches Beispiel, welches so in LeoVote vorkommen könnte und sich an der obigen Abbildung orientiert:

  • Span A - Wahleinladungen senden

    • Span B - E-Mails vorbereiten

      • Span C - DB Abfrage für E-Mails der Wähler

      • Span D - Link für die E-Mail generieren

    • Span E - Emails versenden

4. LiveDemo: LeoVote - Tracing mit Jaeger

jaeger-logo

4.1. Tracing in Quarkus

4.1.1. OpenTelemetry

OpenTelemetry ist ein Open-Source-Framework zur Sammlung und Verarbeitung von:

  • Metriken

  • Logs

  • Traces

In diesem Beispiel verwenden wir OpenTelemetry Tracing in Quarkus. Jaeger Tracing wird später die Daten von OpenTelemetry visualisieren.

4.2. OpenTelemetry Tracing in Quarkus

4.2.1. Dependencies

Um OpenTelemetry Tracing in Quarkus zu verwenden, müssen wir die folgenden Abhängigkeiten hinzufügen:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-opentelemetry</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-jdbc</artifactId>
</dependency>

4.2.2. Application.properties

In der application.properties-Datei müssen wir die folgenden Einstellungen vornehmen:

# Enable OpenTelemetry tracing

quarkus.otel.exporter.otlp.endpoint=http://localhost:4317 (1)

quarkus.otel.traces.sampler=always_on (2)

quarkus.otel.service.name=quarkus-backend (3)

# For JDBC telemetry
quarkus.datasource.jdbc.telemetry=true (4)
1 Der Endpunkt, an den die Tracing-Daten gesendet werden.
2 Der Traces-Sampler, der bestimmt, ob ein Trace erfasst und exportiert wird. Die Einstellung always_on bedeutet, dass alle Traces erfasst werden.
3 Der Name des Services, der in den Traces angezeigt wird. Dieser Name wird in Jaeger als Source verwendet.
4 Aktiviert die Erfassung von JDBC-Telemetrie. Damit können wir die Dauer von Datenbankabfragen messen.

4.2.3. Jaeger Tracing

apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: simplest

4.3. Live Demo

LeoVote

4.4. Custom Span

public Uni<Response> sendInvite(@PathParam("electionId") Long electionId) {
Span span = tracer.spanBuilder("sendEmails").startSpan();

        Optional<Election> election = Election.findByIdOptional(electionId);

        if (election.isEmpty()) {
            return Uni.createFrom().item(Response.status(Response.Status.NOT_FOUND).build());
        }

        try (Scope scope = span.makeCurrent()) {

            // Perform email sending logic in a background task
            emailService.sendInvitations(election.get()).subscribe().with(
                    success -> System.out.println("Emails sent successfully"),
                    failure -> System.out.println("Emails could not be sent\n" + failure.toString())
            );
        } finally {
            span.end();
        }

        return Uni.createFrom().item(Response.ok().entity("{\"message\": \"Emails are being sent asynchronously.\"}").build());
    }

4.5. Jaeger in Docker

docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.62.0

4.6. Deployment auf Kubernetes

minikube start --cpus 15 --memory=8g --driver=docker

Add ingress.

minikube addons enable ingress
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml
kubectl create namespace observability
export WORKING_DIR=/tmp/jaeger
export NAMESPACE=observability  # Change if needed
rm -rf ${WORKING_DIR}
mkdir -p ${WORKING_DIR}
./cert_generation.sh
kubectl create -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.62.0/jaeger-operator.yaml -n observability
kubectl delete secret jaeger-operator-service-cert -n observability
kubectl create secret tls jaeger-operator-service-cert \
  --cert=${WORKING_DIR}/user.jaeger.crt \
  --key=${WORKING_DIR}/user.jaeger.key \
  -n observability

Im Leovote projekt in den k8s Ordner wechseln und folgendes ausführen:

kubectl apply -f .

Aktuell kommt es noch zu einem Fehler mit dem Zertifikat, vermutlich weil es ein self-signed Certificate ist und nicht von einer CA ist.

In Vergangenheit konnte das Problem hin und wieder durch das Ausführen des folgenden Commands behoben werden:

kubectl apply -f ./jaeger.yaml

Um darauf zuzugreifen:

minikube ip
Das Kubernetes Deployment ist zurzeit noch Work-In-Progress (WIP). Nach dem Beheben des Zertifikatsfehlers und Überprüfen der Nginx-Config sollte es auf Minikube funktionieren.
Für das Deployment auf die LeoCloud muss Jaeger-Tracing angepasst werden, denn dort darf man nur in seinem eigenem Namespace arbeiten und keinen neuen anlegen. Dasselbe gilt auch für den Cert-Manager.

4.7. Deployment auf VM

Das LeoVote Projekt wurde auf eine virtuelle Machine deployed. Es wurde folgendermaßen realisiert.

  1. Backend builden

    • mvn build --clean

  2. Frontend builden

    • ng build

  3. Auf Server kopieren mit "scp"

    • Jar-Datei und frontend Verzeichnis auf den Server kopieren

4.7.1. Automatisches Starten

Damit auch bei einem Server Neustart das Front- und Backend automatisch im Hintergrund startet sind folgende Schritte notwendig:

Shellscripts in /opt/scripts anlegen:

backend.sh

#!/bin/bash
cd /home/lvadm && java -jar ./backend-1.0-SNAPSHOT-runner.jar

jaeger.sh

#!/bin/bash

docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.62.0

In den Auto-Start hinzufügen:

crontab -e

Folgendes hinzufügen:

@reboot screen -dm -S backend /opt/scripts/backend.sh
@reboot screen -dm -S jaeger /opt/scripts/jaeger.sh

nginx.conf in /etc/nginx/sites-enabled/default anpassen:

server {
	index index.html index.htm index.nginx-debian.html;

	server_name leovote.htl-leonding.ac.at;

	location /api/services {
		proxy_pass http://localhost:16686/api/services;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto $scheme;
	}

	location /api/traces {
		proxy_pass http://localhost:16686/api/traces;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto $scheme;
	}

    location /api/dependencies {
        proxy_pass http://localhost:16686/api/dependencies;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /api/metrics {
        proxy_pass http://localhost:16686/api/metrics/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /api/monitor {
        proxy_pass http://localhost:16686/api/monitor;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

	location /api/ {
		proxy_pass http://localhost:8080/; # Quarkus application
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto $scheme;
	}


	location / {
		root /home/lvadm/frontend;
		try_files $uri $uri/ /index.html;
	}

	location /tracing/ {
		proxy_pass http://localhost:16686/;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto $scheme;

		# Fix paths for static files
		sub_filter '/static/' '/tracing/static/';
		sub_filter_once off;

		# Required for sub_filter to work
		proxy_http_version 1.1;
		proxy_set_header Accept-Encoding ""; # Ensure sub_filter processes responses
	}

	location /tracing/static/ {
		proxy_pass http://localhost:16686/static/;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto $scheme;
	}

    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/leovote.htl-leonding.ac.at/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/leovote.htl-leonding.ac.at/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

server {
    if ($host = leovote.htl-leonding.ac.at) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


	listen 80 default_server;
	listen [::]:80 default_server;

	server_name leovote.htl-leonding.ac.at;
    return 404; # managed by Certbot
}

nginx neu starten:

sudo systemctl restart nginx

Alternativ kann auch der Server neu gestartet werden. Das Deployment ist auf https://leovote.htl-leonding.ac.at abrufbar.
Das Jaeger-UI ist hier zu finden: https://leovote.htl-leonding.ac.at/tracing

5. Slides

6. Quellen