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.
2. Micrometer
2.1. Was ist Micrometer?
-
Java-basierte Bibliothek für Metrik-Sammlung.
-
Unterstützt verschiedene Metrik-Typen (Zähler, Timer, Histogramm).
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.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
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?

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

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.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.
-
Backend builden
-
mvn build --clean
-
-
Frontend builden
-
ng build
-
-
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