Spring Boot Aufbau – Tag 9 von 10
Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting

📍 Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| 1 | Auto-Configuration & Custom Starter | Voraussetzung |
| 2 | Spring Data JPA Basics | Voraussetzung |
| 3 | JPA Relationships & Queries | Voraussetzung |
| 4 | Spring Security Part 1 – Authentication | Voraussetzung |
| 5 | Spring Security Part 2 – Authorization | Voraussetzung |
| 6 | Spring Boot Caching & JSON | Voraussetzung |
| 7 | Messaging & Email | Voraussetzung |
| 8 | Testing & Dokumentation | Voraussetzung |
| → 9 | Spring Boot Actuator | 👉 DU BIST HIER! |
| 10 | Template Engines & Microservices | Noch nicht freigeschaltet |
Modul: Spring Boot Aufbau (10 Arbeitstage)
Dein Ziel: Production-ready Monitoring mit Actuator aufbauen
📋 Voraussetzungen
Du brauchst:
- ✅ Spring Boot Basics (Tag 1-8)
- ✅ REST API Verständnis (Tag 2)
- ✅ Testing Basics helfen (Tag 8)
Optional (hilft beim Verständnis):
- Grundlegendes DevOps-Verständnis
- Prometheus/Grafana Kenntnisse (wird aber erklärt)
Tag verpasst? Kein Problem! Dieser Blogbeitrag deckt genau den Stoff von Tag 9 ab. Download das Projekt und arbeite die 8 Stunden durch!
⚡ Was du heute baust:
Ein vollständiges Production-Monitoring-System mit Health Checks, Custom Metriken, Prometheus Integration und einem Grafana Dashboard. Du lernst, wie du deine Spring Boot App in Production überwachst!
🎯 Dein Ziel nach 8 Stunden:
- ✅ Actuator Endpoints verstanden und konfiguriert
- ✅ Custom Health Indicators erstellt
- ✅ Eigene Metriken mit Micrometer erfasst
- ✅ Prometheus Integration aufgebaut
- ✅ Grafana Dashboard für Monitoring erstellt
- ✅ Production-ready Observability implementiert
💻 Los geht’s!
🔍 Warum Spring Boot Actuator? – Meine Production-Horror-Story
Lass mich dir von meinem schlimmsten Production-Incident erzählen. 2022, Freitagabend 18:00 Uhr. Ich wollte gerade ins Wochenende starten.
Dann kam der Anruf: „Die App ist down! Kunden können nicht mehr bestellen!“
Mein erster Gedanke: „Okay, schau ich mir an…“
Das Problem: Ich hatte NULL Monitoring! Keine Health Checks, keine Metriken, nichts! Ich war blind.
Was ich NICHT wusste:
- ❌ Ist die App wirklich down oder nur langsam?
- ❌ Welcher Service ist betroffen?
- ❌ Database-Connection okay?
- ❌ Memory voll? CPU am Limit?
- ❌ Wann hat das Problem angefangen?
Was ich tun musste:
- SSH auf den Server
- Logs manuell durchsuchen
- JVM-Prozess inspizieren
- Database manuell checken
- 4 Stunden debugging bis ich den Bug fand
Der Bug: Memory Leak durch nicht geschlossene DB-Connections. Die App war nach 6 Stunden voll und crashed.
Die Lektion:
Mit Actuator + Monitoring hätte ich:
- ✅ Sofort gesehen: Memory steigt kontinuierlich
- ✅ Alert bekommen: Memory > 90%
- ✅ DB-Connection-Pool gecheckt: Pool exhausted!
- ✅ Problem in 15 Minuten gelöst statt 4 Stunden
Seit diesem Tag: Jede meiner Apps hat Actuator + Monitoring. Keine Ausnahmen!
🟢 GRUNDLAGEN
Schritt 1: Actuator Setup
Was ist Actuator?
Spring Boot Actuator ist wie ein Gesundheits-Check-System für deine App. Es gibt dir Einblick in:
- Health Status
- Metriken (CPU, Memory, Requests)
- Environment Config
- Thread Dumps
- Log Levels
- Und vieles mehr!
Dependency hinzufügen:
<dependencies>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer für Prometheus (später) -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
Was passiert hier?
spring-boot-starter-actuator – Der Kern
Das ist das Haupt-Package für Actuator. Es bringt alle Monitoring-Endpoints mit.
micrometer-registry-prometheus – Metriken-Export
Micrometer ist das Metriken-Framework von Spring Boot. Mit dem Prometheus-Registry können wir Metriken für Prometheus exportieren – dazu später mehr!
Das Prinzip dahinter:
Actuator gibt dir out-of-the-box Monitoring! Einfach Dependency hinzufügen, und du hast sofort Production-ready Endpoints.
Actuator konfigurieren:
# application.properties # Welche Endpoints sollen verfügbar sein? management.endpoints.web.exposure.include=health,info,metrics,prometheus # Base-Path für Actuator-Endpoints management.endpoints.web.base-path=/actuator # Server-Port für Actuator (optional - für Separation) # management.server.port=9090 # Health Details zeigen management.endpoint.health.show-details=always # Info Endpoint konfigurieren management.info.env.enabled=true info.app.name=User Management API info.app.version=1.0.0 info.app.description=Spring Boot Aufbau - Tag 9
Was machen diese Properties im Detail?
management.endpoints.web.exposure.include
Das ist die wichtigste Einstellung! Sie kontrolliert, welche Endpoints öffentlich verfügbar sind. Default ist nur health – alles andere musst du explizit freischalten.
Warum ist das wichtig?
Security! Einige Actuator-Endpoints geben sensitive Informationen preis (Environment-Variablen, Config-Details, Thread-Dumps). Du willst nicht, dass diese öffentlich zugänglich sind!
management.endpoint.health.show-details=always
Default zeigt /health nur „UP“ oder „DOWN“. Mit show-details=always siehst du Details – welche Komponenten sind betroffen?
In der Praxis bedeutet das:
Mit show-details=always siehst du z.B.: „Database ist UP, aber Disk-Space ist LOW“. Das ist Gold wert beim Debugging!
App starten und testen:
# App starten mvn spring-boot:run # Health Check curl http://localhost:8080/actuator/health # Info Endpoint curl http://localhost:8080/actuator/info # Alle verfügbaren Endpoints curl http://localhost:8080/actuator
Response vom Health Endpoint:
{
"status": "UP",
"components": {
"db": {
"status": "UP",
"details": {
"database": "H2",
"validationQuery": "isValid()"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 499963174912,
"free": 198831255552,
"threshold": 10485760,
"exists": true
}
},
"ping": {
"status": "UP"
}
}
}
Was siehst du hier?
status: „UP“ – Gesamt-Status der App
Die App ist gesund und läuft!
components – Einzelne Komponenten
Jede Komponente (Database, Disk-Space, etc.) hat ihren eigenen Status. Das ist wichtig – du siehst welche Komponente ein Problem hat!
details – Detaillierte Informationen
Für jede Komponente gibt es Details. Z.B. bei Disk-Space: Wie viel ist frei? Was ist der Threshold?
Das Prinzip dahinter:
Health Checks sind hierarchisch! Die App ist nur „UP“ wenn alle Komponenten „UP“ sind. Wenn eine Komponente „DOWN“ ist, ist die ganze App „DOWN“.
Wichtig zu verstehen:
Dieser Health-Endpoint ist kritisch für Production! Load-Balancer, Kubernetes, Docker Swarm – alle nutzen /health um zu entscheiden: „Ist diese Instanz gesund? Soll ich Traffic dahin routen?“
Schritt 2: Standard Actuator Endpoints
Die wichtigsten Endpoints im Überblick:
| Endpoint | Beschreibung | Beispiel-Use-Case |
|---|---|---|
/health | Health Status | Load-Balancer Checks |
/info | App-Informationen | Version anzeigen |
/metrics | Metriken | CPU, Memory überwachen |
/env | Environment Config | Config-Debugging |
/loggers | Log-Level | Log-Level zur Laufzeit ändern |
/threaddump | Thread-Dump | Deadlock-Debugging |
/heapdump | Heap-Dump | Memory-Leak-Analyse |
/prometheus | Prometheus-Metriken | Monitoring-Integration |
Lass uns die wichtigsten im Detail anschauen!
1. /health – Der Lebenszeichen-Check:
curl http://localhost:8080/actuator/health | jq
Was macht dieser Endpoint?
Er prüft automatisch:
- ✅ Database-Connection
- ✅ Disk-Space
- ✅ Custom Health-Indicators (die wir gleich erstellen!)
Wann nutzt du das?
Production: Load-Balancer rufen /health alle 10 Sekunden. Wenn „DOWN“ → keine Traffic mehr!
2. /metrics – Die Zahlen-Zentrale:
# Alle verfügbaren Metriken curl http://localhost:8080/actuator/metrics | jq # Spezifische Metrik curl http://localhost:8080/actuator/metrics/jvm.memory.used | jq
Response:
{
"name": "jvm.memory.used",
"description": "The amount of used memory",
"baseUnit": "bytes",
"measurements": [
{
"statistic": "VALUE",
"value": 157286400
}
],
"availableTags": [
{
"tag": "area",
"values": ["heap", "nonheap"]
},
{
"tag": "id",
"values": ["G1 Eden Space", "G1 Old Gen", "G1 Survivor Space"]
}
]
}
Was siehst du hier?
name – Name der Metrik
Eindeutiger Identifier. Hier: JVM Memory Usage.
measurements – Aktuelle Werte
Der aktuelle Memory-Verbrauch: 157 MB.
availableTags – Filter-Möglichkeiten
Du kannst nach „area“ (heap/nonheap) oder „id“ (welcher Memory-Pool?) filtern.
Das Prinzip dahinter:
Metriken sind dimensional! Du hast nicht nur „Memory“, sondern „Memory für Eden Space“ vs „Memory für Old Gen“. Das gibt dir viel mehr Einblick!
Wichtige Standard-Metriken:
# JVM Memory curl http://localhost:8080/actuator/metrics/jvm.memory.used curl http://localhost:8080/actuator/metrics/jvm.memory.max # CPU curl http://localhost:8080/actuator/metrics/system.cpu.usage curl http://localhost:8080/actuator/metrics/process.cpu.usage # HTTP Requests curl http://localhost:8080/actuator/metrics/http.server.requests # Database Connection Pool curl http://localhost:8080/actuator/metrics/hikaricp.connections.active curl http://localhost:8080/actuator/metrics/hikaricp.connections.max
3. /loggers – Log-Level zur Laufzeit ändern:
# Aktuellen Log-Level sehen
curl http://localhost:8080/actuator/loggers/com.example | jq
# Log-Level ändern (POST)
curl -X POST http://localhost:8080/actuator/loggers/com.example \
-H "Content-Type: application/json" \
-d '{"configuredLevel": "DEBUG"}'
Warum ist das genial?
In Production läuft normalerweise INFO Log-Level. Wenn du einen Bug debuggen willst, musst du normalerweise:
- Config ändern
- App neu deployen
- Warten bis Bug wieder auftritt
Mit /loggers: Einfach Log-Level auf DEBUG stellen, Bug beobachten, Log-Level zurück auf INFO. Kein Deployment nötig!
In der Praxis bedeutet das:
Du kannst live in Production debuggen ohne Restart! Das ist ein Game-Changer für knifflige Bugs die nur unter Last auftreten.
Schritt 3: Custom Health Indicators
Das Problem mit Standard Health Checks:
Actuator prüft automatisch Database und Disk-Space. Aber was wenn deine App externe Services nutzt?
Beispiel:
- Email-Service (von Tag 7)
- Payment-Gateway
- External APIs
- Redis Cache
Die Lösung: Custom Health Indicators!
EmailServiceHealthIndicator.java:
package com.example.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;
import jakarta.mail.MessagingException;
@Component
public class EmailServiceHealthIndicator implements HealthIndicator {
private final JavaMailSender mailSender;
public EmailServiceHealthIndicator(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public Health health() {
try {
// SMTP-Connection testen
mailSender.testConnection();
return Health.up()
.withDetail("service", "Email Service")
.withDetail("status", "SMTP connection successful")
.build();
} catch (MessagingException e) {
return Health.down()
.withDetail("service", "Email Service")
.withDetail("error", e.getMessage())
.withException(e)
.build();
}
}
}
Lass uns das aufschlüsseln!
HealthIndicator Interface:
public interface HealthIndicator {
Health health();
}
Was macht dieses Interface?
Es definiert einen Contract: „Implementiere die health() Methode und gib einen Health-Status zurück!“ Das ist alles was Spring braucht.
Das Prinzip dahinter:
Spring findet automatisch alle Beans die HealthIndicator implementieren und fügt sie zu /health hinzu! Du musst nichts konfigurieren – einfach @Component und fertig.
Die health() Methode im Detail:
public Health health() {
try {
mailSender.testConnection();
return Health.up()
.withDetail("service", "Email Service")
.withDetail("status", "SMTP connection successful")
.build();
Was passiert hier?
Phase 1: Test ausführen
Wir rufen testConnection() auf. Das prüft die SMTP-Verbindung.
Phase 2: Health.up() bei Erfolg
Wenn die Connection funktioniert, returnen wir Health.up() – der Service ist gesund!
Phase 3: Details hinzufügen
Mit withDetail() fügen wir Kontext hinzu. Was wurde getestet? Was war das Ergebnis?
Der Error-Case:
} catch (MessagingException e) {
return Health.down()
.withDetail("service", "Email Service")
.withDetail("error", e.getMessage())
.withException(e)
.build();
}
Wichtig zu verstehen:
Bei einem Error returnen wir Health.down() und fügen die Exception hinzu. So siehst du in /health nicht nur „DOWN“, sondern auch warum!
Health Check Response mit Custom Indicator:
{
"status": "UP",
"components": {
"db": {
"status": "UP"
},
"diskSpace": {
"status": "UP"
},
"emailService": {
"status": "UP",
"details": {
"service": "Email Service",
"status": "SMTP connection successful"
}
}
}
}
Siehst du den Unterschied?
Jetzt haben wir einen eigenen Health-Check für den Email-Service! Wenn SMTP down ist, siehst du das sofort in /health.
Weitere Custom Health Indicators:
External API Health Check:
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final RestTemplate restTemplate;
private final String apiUrl;
public ExternalApiHealthIndicator(RestTemplate restTemplate,
@Value("${external.api.url}") String apiUrl) {
this.restTemplate = restTemplate;
this.apiUrl = apiUrl;
}
@Override
public Health health() {
try {
long start = System.currentTimeMillis();
ResponseEntity<String> response = restTemplate.getForEntity(
apiUrl + "/health",
String.class
);
long duration = System.currentTimeMillis() - start;
if (response.getStatusCode().is2xxSuccessful()) {
return Health.up()
.withDetail("api", "External Payment API")
.withDetail("responseTime", duration + "ms")
.withDetail("status", response.getStatusCode())
.build();
} else {
return Health.down()
.withDetail("api", "External Payment API")
.withDetail("status", response.getStatusCode())
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("api", "External Payment API")
.withDetail("error", "Connection failed: " + e.getMessage())
.withException(e)
.build();
}
}
}
Was macht dieser Health Check?
Er prüft eine externe API! Das ist super wichtig – wenn deine App von externen Services abhängt, willst du wissen ob sie erreichbar sind!
Das Prinzip dahinter:
Health Checks sollten alle kritischen Dependencies prüfen. Deine App ist nur gesund wenn alle Dependencies gesund sind!
In der Praxis bedeutet das:
Mit Custom Health Indicators siehst du genau welcher Service Probleme macht. Ist es die Database? Der Email-Service? Die Payment-API? Du weißt es sofort!
Schritt 4: Custom Metriken mit Micrometer
Das Problem mit Standard-Metriken:
Actuator gibt dir JVM-Metriken, HTTP-Metriken, Database-Metriken. Aber was wenn du Business-Metriken willst?
Beispiele:
- Wie viele User haben sich registriert?
- Wie viele Orders wurden erstellt?
- Wie viele Emails wurden versendet?
- Wie lange dauert eine Payment-Transaction?
Die Lösung: Custom Metriken mit Micrometer!
Was ist Micrometer?
Micrometer ist wie SLF4J – aber für Metriken statt Logs! Es ist eine Abstraction-Layer über verschiedene Monitoring-Systeme (Prometheus, Graphite, DataDog, etc.).
UserService mit Custom Metriken:
package com.example.service;
import com.example.model.User;
import com.example.repository.UserRepository;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final Counter userRegistrationCounter;
private final Timer userRegistrationTimer;
public UserService(UserRepository userRepository,
PasswordEncoder passwordEncoder,
MeterRegistry meterRegistry) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
// Counter für User-Registrierungen
this.userRegistrationCounter = Counter.builder("users.registered")
.description("Total number of registered users")
.tag("service", "user-service")
.register(meterRegistry);
// Timer für Registration-Dauer
this.userRegistrationTimer = Timer.builder("users.registration.duration")
.description("Time taken to register a user")
.tag("service", "user-service")
.register(meterRegistry);
}
@Transactional
public User createUser(User user) {
return userRegistrationTimer.recordCallable(() -> {
log.info("Creating user: {}", user.getUsername());
// Validierung
if (!user.isValidUsername()) {
throw new IllegalArgumentException("Invalid username");
}
if (!user.isValidEmail()) {
throw new IllegalArgumentException("Invalid email");
}
// Duplikat-Checks
if (userRepository.existsByUsername(user.getUsername())) {
throw new IllegalStateException("Username already exists");
}
if (userRepository.existsByEmail(user.getEmail())) {
throw new IllegalStateException("Email already exists");
}
// Password hashen
user.setPassword(passwordEncoder.encode(user.getPassword()));
User saved = userRepository.save(user);
// Counter inkrementieren
userRegistrationCounter.increment();
log.info("✅ User created: {}", saved.getId());
return saved;
});
}
}
Lass uns das im Detail verstehen!
MeterRegistry – Die Metriken-Zentrale:
private final MeterRegistry meterRegistry;
public UserService(..., MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
Was ist MeterRegistry?
Das ist das zentrale Registry für alle Metriken. Spring Boot injected das automatisch – du musst nur den Parameter hinzufügen!
Das Prinzip dahinter:
Alle Metriken werden in der MeterRegistry registriert. Von dort aus werden sie exportiert – zu Prometheus, Graphite, oder wohin auch immer du willst.
Counter – Zähler für Events:
this.userRegistrationCounter = Counter.builder("users.registered")
.description("Total number of registered users")
.tag("service", "user-service")
.register(meterRegistry);
Was macht ein Counter?
Ein Counter zählt Events. Er kann nur hoch gehen, nie runter. Perfekt für: „Wie viele X sind passiert?“
Die Builder-API im Detail:
Counter.builder("users.registered") – Name der Metrik
Das ist der eindeutige Name. Convention: lowercase mit dots als Separator.
.description(...) – Beschreibung
Was misst diese Metrik? Das erscheint in Monitoring-Tools als Hilfetext.
.tag("service", "user-service") – Tags/Labels
Tags erlauben Filterung! Du kannst später sagen: „Zeig mir alle Metriken vom user-service!“
.register(meterRegistry) – Registrieren
Das fügt den Counter zur Registry hinzu. Ohne das würde er nicht exportiert!
Wichtig zu verstehen:
Tags sind mächtig! Du kannst eine Metrik haben mit verschiedenen Tags: status=success, status=error. Dann kannst du filtern: „Wie viele erfolgreiche vs fehlgeschlagene Registrierungen?“
Counter nutzen:
userRegistrationCounter.increment();
So einfach! Jeder Aufruf von increment() erhöht den Counter um 1.
Timer – Dauer messen:
this.userRegistrationTimer = Timer.builder("users.registration.duration")
.description("Time taken to register a user")
.tag("service", "user-service")
.register(meterRegistry);
Was macht ein Timer?
Ein Timer misst Dauer! Er tracked automatisch: Min, Max, Average, Percentiles (p50, p95, p99).
Timer nutzen:
return userRegistrationTimer.recordCallable(() -> {
// Dein Code hier
User saved = userRepository.save(user);
return saved;
});
Was passiert hier?
recordCallable() führt den Code aus UND misst die Zeit! Du bekommst automatisch:
- ✅ Wie lange hat es gedauert?
- ✅ Was ist die durchschnittliche Dauer?
- ✅ Was ist das 95th Percentile?
Das Prinzip dahinter:
Percentiles sind wichtig! Average sagt dir: „Normalerweise dauert es 100ms“. Aber 95th Percentile sagt dir: „Für 95% der User dauert es unter 200ms, aber 5% warten länger!“
In der Praxis bedeutet das:
Du siehst Performance-Probleme bevor sie kritisch werden! Wenn das p95 steigt, hast du ein Problem – auch wenn der Average noch okay aussieht.
Metriken abrufen:
# Counter curl http://localhost:8080/actuator/metrics/users.registered | jq # Timer curl http://localhost:8080/actuator/metrics/users.registration.duration | jq
Response vom Timer:
{
"name": "users.registration.duration",
"description": "Time taken to register a user",
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 42
},
{
"statistic": "TOTAL_TIME",
"value": 4.2
},
{
"statistic": "MAX",
"value": 0.15
}
],
"availableTags": [
{
"tag": "service",
"values": ["user-service"]
}
]
}
Was siehst du hier?
- COUNT: 42 Registrierungen
- TOTAL_TIME: 4.2 Sekunden gesamt
- MAX: Längste Registration dauerte 0.15 Sekunden
Average berechnen: 4.2s / 42 = 0.1s = 100ms pro Registration
🟡 PROFESSIONAL
Schritt 5: Prometheus Integration
Warum Prometheus?
Actuator gibt dir Metriken über /metrics – aber das ist nur ein Snapshot! Du siehst: „Aktuell 150MB Memory“. Aber was ist mit:
- Wie war der Memory vor 1 Stunde?
- Wie ist der Trend über die letzten 24 Stunden?
- Wann war der Peak?
Die Lösung: Prometheus!
Prometheus ist eine Time-Series Database für Metriken. Es speichert Metriken über Zeit und lässt dich Trends analysieren.
Wie funktioniert Prometheus?
┌─────────────┐ Scrape every 15s ┌─────────────┐
│ Spring Boot │ ←───────────────────────── │ Prometheus │
│ /actuator │ │ Server │
│ /prometheus│ └─────────────┘
└─────────────┘ ↓
┌─────────────┐
│ Grafana │
│ Dashboard │
└─────────────┘
Das Prinzip dahinter:
Prometheus pullt (scraped) Metriken von deiner App alle 15 Sekunden. Spring Boot stellt sie unter /actuator/prometheus bereit. Grafana visualisiert dann die Daten aus Prometheus.
Dependency ist bereits hinzugefügt:
<!-- Von Schritt 1 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Configuration:
# application.properties # Prometheus Endpoint aktivieren management.endpoints.web.exposure.include=health,info,metrics,prometheus # Prometheus-spezifische Metriken management.metrics.export.prometheus.enabled=true management.metrics.distribution.percentiles-histogram.http.server.requests=true
Was machen diese Properties?
management.metrics.export.prometheus.enabled=true
Aktiviert den Prometheus-Export. Ohne das gibt es kein /actuator/prometheus!
management.metrics.distribution.percentiles-histogram
Aktiviert Histogram-Buckets für HTTP-Requests. Das erlaubt Prometheus Percentiles (p50, p95, p99) zu berechnen!
Wichtig zu verstehen:
Histograms sind teuer (Memory/CPU), aber wichtig! Sie erlauben dir zu sagen: „Zeig mir alle Requests die länger als 500ms dauerten!“
Prometheus Endpoint testen:
curl http://localhost:8080/actuator/prometheus
Response (Auszug):
# HELP users_registered_total Total number of registered users
# TYPE users_registered_total counter
users_registered_total{service="user-service",} 42.0
# HELP users_registration_duration_seconds Time taken to register a user
# TYPE users_registration_duration_seconds summary
users_registration_duration_seconds_count{service="user-service",} 42.0
users_registration_duration_seconds_sum{service="user-service",} 4.2
users_registration_duration_seconds_max{service="user-service",} 0.15
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="G1 Eden Space",} 1.57286400E8
jvm_memory_used_bytes{area="heap",id="G1 Old Gen",} 5.242880E7
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{method="POST",uri="/api/users",status="201",} 42.0
http_server_requests_seconds_sum{method="POST",uri="/api/users",status="201",} 2.1
Was siehst du hier?
Das ist das Prometheus-Format! Nicht JSON, sondern ein spezielles Text-Format das Prometheus versteht.
Die Metrik-Typen:
counter – Zähler (nur hoch)users_registered_total – Gesamt-Anzahl Registrierungen
summary – Zusammenfassung (count, sum, max)users_registration_duration_seconds – Duration-Statistiken
gauge – Messwert (kann hoch/runter)jvm_memory_used_bytes – Aktueller Memory-Verbrauch
Das Prinzip dahinter:
Prometheus scraped diese Metriken alle 15 Sekunden und speichert sie. So entsteht eine Zeitreihe – du siehst wie sich Metriken über Zeit entwickeln!
Prometheus mit Docker starten:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
prometheus.yml – Prometheus Configuration:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['app:8080']
Was macht diese Config?
scrape_interval: 15s – Alle 15 Sekunden Metriken holen
Das ist der Standard. Für Production oft okay, für High-Frequency-Metriken zu langsam.
job_name: ’spring-boot-app‘ – Name des Jobs
Das ist der Label den Prometheus allen Metriken hinzufügt. So kannst du später filtern: „Zeig mir nur Metriken von spring-boot-app!“
metrics_path: ‚/actuator/prometheus‘ – Wo holt Prometheus die Metriken?
Der Pfad zu deinem Prometheus-Endpoint.
targets: [‚app:8080‘] – Welche Instanzen scraped Prometheus?
Die Hostnames der Apps. Im Docker-Compose-Network ist das der Service-Name!
Starten:
docker-compose up -d # Prometheus UI öffnen http://localhost:9090
In Prometheus UI testen:
# Query: User-Registrierungen über Zeit users_registered_total # Query: Durchschnittliche Registration-Dauer (letzte 5 Minuten) rate(users_registration_duration_seconds_sum[5m]) / rate(users_registration_duration_seconds_count[5m]) # Query: 95th Percentile HTTP Response Time histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m]))
In der Praxis bedeutet das:
Mit Prometheus siehst du Trends! Wenn die durchschnittliche Registration-Dauer langsam steigt, hast du ein Problem – auch wenn die aktuellen Werte noch okay aussehen.
Schritt 6: Grafana Dashboard erstellen
Warum Grafana?
Prometheus hat eine UI, aber sie ist funktional, nicht schön. Grafana macht aus deinen Metriken professionelle Dashboards!
Docker Compose erweitern:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-storage:/var/lib/grafana
depends_on:
- prometheus
volumes:
grafana-storage:
Starten:
docker-compose up -d # Grafana öffnen http://localhost:3000 # Login: admin / admin
Grafana konfigurieren:
- Data Source hinzufügen:
- Configuration → Data Sources → Add data source
- Prometheus auswählen
- URL:
http://prometheus:9090 - Save & Test
- Dashboard erstellen:
- Create → Dashboard → Add new panel
Beispiel-Panels für unser User-Management:
Panel 1: User Registrations Over Time
Query: rate(users_registered_total[5m]) * 60 Visualization: Time series Title: User Registrations per Minute
Panel 2: Average Registration Duration
Query: rate(users_registration_duration_seconds_sum[5m]) / rate(users_registration_duration_seconds_count[5m]) Visualization: Gauge Title: Avg Registration Duration (seconds) Unit: seconds (s)
Panel 3: JVM Memory Usage
Query: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} * 100
Visualization: Time series
Title: Heap Memory Usage (%)
Unit: percent (0-100)
Panel 4: HTTP Request Rate
Query: sum(rate(http_server_requests_seconds_count[5m])) by (uri, method)
Visualization: Time series
Title: HTTP Requests per Second by Endpoint
Legend: {{ method }} {{ uri }}
Panel 5: HTTP 95th Percentile Latency
Query: histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri)) Visualization: Time series Title: 95th Percentile Response Time by Endpoint Unit: seconds (s)
Das Ergebnis:
Ein professionelles Dashboard das zeigt:
- ✅ Wie viele User registrieren sich?
- ✅ Wie schnell ist die App?
- ✅ Wie viel Memory wird genutzt?
- ✅ Welche Endpoints sind am langsamsten?
In der Praxis bedeutet das:
Du siehst auf einen Blick den Zustand deiner App! Wenn was schiefgeht, siehst du es im Dashboard bevor Kunden sich beschweren.
Schritt 7: Actuator Security – Production Best Practices
Das Problem:
Actuator-Endpoints geben sensitive Informationen preis! Du willst nicht, dass jeder /actuator/env aufrufen und deine Secrets sehen kann!
Die Lösung: Spring Security!
Dependency (bereits von Tag 4/5):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Security Configuration für Actuator:
package com.example.config;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
// Health & Info sind public (für Load-Balancer)
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
// Prometheus ist public (für Prometheus Server)
.requestMatchers(EndpointRequest.to("prometheus")).permitAll()
// Alle anderen Actuator-Endpoints brauchen ACTUATOR-Rolle
.anyRequest().hasRole("ACTUATOR")
)
.httpBasic(withDefaults());
return http.build();
}
}
Was macht diese Config im Detail?
securityMatcher(EndpointRequest.toAnyEndpoint())
Diese Security-Chain gilt nur für Actuator-Endpoints! Deine normale API (/api/users) ist davon nicht betroffen.
Das Prinzip dahinter:
Du kannst mehrere SecurityFilterChains haben! Eine für Actuator, eine für die API, eine für Static-Resources. Das gibt dir granulare Kontrolle.
permitAll() für health & info:
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
Warum public?
Load-Balancer müssen /health ohne Authentication aufrufen können! Sonst können sie nicht prüfen ob deine App gesund ist.
permitAll() für prometheus:
.requestMatchers(EndpointRequest.to("prometheus")).permitAll()
Warum public?
Prometheus-Server braucht Access ohne Auth. Aber: Prometheus läuft normalerweise im gleichen Netzwerk (Docker, Kubernetes) – nicht öffentlich!
Wichtig zu verstehen:
In Production sollte Prometheus NICHT öffentlich erreichbar sein! Nur im internen Netzwerk. Das ist sicher genug.
hasRole(„ACTUATOR“) für alle anderen:
.anyRequest().hasRole("ACTUATOR")
Alle anderen Endpoints (/env, /loggers, /threaddump, etc.) brauchen die ACTUATOR-Rolle!
User mit ACTUATOR-Rolle erstellen:
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class ActuatorUserConfig {
@Bean
public UserDetailsService actuatorUserDetailsService() {
UserDetails actuatorUser = User.builder()
.username("actuator")
.password(passwordEncoder().encode("actuator-secret-password"))
.roles("ACTUATOR")
.build();
return new InMemoryUserDetailsManager(actuatorUser);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Testen:
# Health - Public, funktioniert ohne Auth curl http://localhost:8080/actuator/health # Metrics - Braucht Auth curl -u actuator:actuator-secret-password \ http://localhost:8080/actuator/metrics # Ohne Auth → 401 Unauthorized curl http://localhost:8080/actuator/metrics
Production Best Practices:
1. Separate Port für Actuator:
# application.properties # App läuft auf 8080 server.port=8080 # Actuator läuft auf 9090 (internes Netzwerk!) management.server.port=9090 management.server.address=127.0.0.1
Warum separate Ports?
- ✅ Actuator ist nicht von außen erreichbar
- ✅ Firewall kann Port 9090 blocken
- ✅ Nur internes Monitoring hat Access
2. Minimal exposure:
# Nur die nötigsten Endpoints management.endpoints.web.exposure.include=health,metrics,prometheus
3. Details nur für authentifizierte User:
# Health Details nur für authentifizierte User management.endpoint.health.show-details=when-authorized
In der Praxis bedeutet das:
Public sieht nur „UP“ oder „DOWN“. Aber mit ACTUATOR-Rolle siehst du Details wie „Database DOWN wegen Connection-Timeout“.
🔵 BONUS: CUSTOM ACTUATOR ENDPOINTS
Schritt 8: Database-Access-Tracker – Custom Actuator Endpoint
Das Problem:
Du willst wissen:
- Wie viele Database-Queries wurden ausgeführt?
- Welche Repositories werden am meisten genutzt?
- Wie lange dauern Queries im Durchschnitt?
- Gibt es Performance-Probleme?
Die Lösung: Custom Actuator Endpoint + AOP!
Das ist Best Practice für Production! Du trackst alle DB-Zugriffe und kannst sie via Actuator abfragen.
Schritt 1: Database Query Tracker erstellen
package com.example.monitoring;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class DatabaseQueryTracker {
private final Map<String, QueryStatistics> queryStats = new ConcurrentHashMap<>();
private final List<QueryExecution> recentQueries = Collections.synchronizedList(new ArrayList<>());
private static final int MAX_RECENT_QUERIES = 100;
public void recordQuery(String repository, String method, long durationMs, boolean success) {
// Update Statistics
QueryStatistics stats = queryStats.computeIfAbsent(
repository + "." + method,
k -> new QueryStatistics(repository, method)
);
stats.incrementCount();
stats.addDuration(durationMs);
if (!success) {
stats.incrementErrors();
}
// Store recent query
QueryExecution execution = new QueryExecution(
repository,
method,
durationMs,
success,
LocalDateTime.now()
);
synchronized (recentQueries) {
recentQueries.add(execution);
if (recentQueries.size() > MAX_RECENT_QUERIES) {
recentQueries.remove(0);
}
}
}
public Map<String, QueryStatistics> getQueryStatistics() {
return new HashMap<>(queryStats);
}
public List<QueryExecution> getRecentQueries() {
synchronized (recentQueries) {
return new ArrayList<>(recentQueries);
}
}
public void reset() {
queryStats.clear();
recentQueries.clear();
}
@Data
public static class QueryStatistics {
private final String repository;
private final String method;
private final AtomicLong totalCount = new AtomicLong(0);
private final AtomicLong totalDuration = new AtomicLong(0);
private final AtomicLong errorCount = new AtomicLong(0);
private long minDuration = Long.MAX_VALUE;
private long maxDuration = 0;
public QueryStatistics(String repository, String method) {
this.repository = repository;
this.method = method;
}
public void incrementCount() {
totalCount.incrementAndGet();
}
public void addDuration(long duration) {
totalDuration.addAndGet(duration);
minDuration = Math.min(minDuration, duration);
maxDuration = Math.max(maxDuration, duration);
}
public void incrementErrors() {
errorCount.incrementAndGet();
}
public double getAverageDuration() {
long count = totalCount.get();
return count > 0 ? (double) totalDuration.get() / count : 0;
}
public double getErrorRate() {
long count = totalCount.get();
return count > 0 ? (double) errorCount.get() / count * 100 : 0;
}
}
@Data
public static class QueryExecution {
private final String repository;
private final String method;
private final long durationMs;
private final boolean success;
private final LocalDateTime timestamp;
}
}
Was macht dieser Tracker?
Phase 1: Query-Statistiken sammeln
Für jede Repository-Methode tracken wir:
- Anzahl Aufrufe
- Gesamte Duration
- Min/Max/Average Duration
- Fehlerrate
Phase 2: Recent Queries speichern
Die letzten 100 Queries werden gespeichert mit Timestamp. So siehst du: „Was wurde zuletzt abgefragt?“
Das Prinzip dahinter:
Wir nutzen Thread-Safe Collections (ConcurrentHashMap, AtomicLong) weil mehrere Threads gleichzeitig Queries ausführen! Ohne Thread-Safety hätten wir Race-Conditions.
Wichtig zu verstehen:
ConcurrentHashMap ist perfekt für high-concurrency Szenarien! Viel besser als synchronized über die ganze Map.
Schritt 2: AOP Aspect für automatisches Tracking
package com.example.monitoring;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class RepositoryMonitoringAspect {
private final DatabaseQueryTracker queryTracker;
@Around("execution(* com.example.repository..*(..))")
public Object monitorRepositoryMethod(ProceedingJoinPoint joinPoint) throws Throwable {
String repository = joinPoint.getTarget().getClass().getSimpleName();
String method = joinPoint.getSignature().getName();
long startTime = System.currentTimeMillis();
boolean success = true;
try {
Object result = joinPoint.proceed();
return result;
} catch (Throwable e) {
success = false;
throw e;
} finally {
long duration = System.currentTimeMillis() - startTime;
queryTracker.recordQuery(repository, method, duration, success);
if (duration > 1000) {
log.warn("⚠️ Slow query detected: {}.{} took {}ms",
repository, method, duration);
}
}
}
}
Was macht dieser Aspect?
@Around Advice:
@Around("execution(* com.example.repository..*(..))")
Das ist AOP Magic! Dieser Aspect wird automatisch vor/nach jeder Repository-Methode ausgeführt!
Das Pointcut erklärt:
execution(...)– Methoden-Ausführung* com.example.repository..*(..)– Alle Methoden incom.example.repository-Package und Sub-Packages
Das Prinzip dahinter:
AOP ist non-invasive! Dein Repository-Code ändert sich nicht – das Monitoring passiert „um ihn herum“. Das ist Clean Code!
Der Monitoring-Flow:
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed(); // Echte Methode ausführen
return result;
} finally {
long duration = System.currentTimeMillis() - startTime;
queryTracker.recordQuery(repository, method, duration, success);
}
Was passiert hier?
- Zeit messen (Start)
- Echte Repository-Methode ausführen
- Zeit messen (Ende)
- Query im Tracker speichern
Wichtig zu verstehen:
Der finally-Block wird immer ausgeführt – auch bei Exceptions! So tracken wir auch fehlgeschlagene Queries.
Slow Query Warning:
if (duration > 1000) {
log.warn("⚠️ Slow query detected: {}.{} took {}ms",
repository, method, duration);
}
Wenn eine Query länger als 1 Sekunde dauert, loggen wir eine Warning! Das ist Gold wert in Production – du siehst Performance-Probleme sofort!
Schritt 3: Custom Actuator Endpoint erstellen
package com.example.monitoring;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.actuate.endpoint.annotation.*;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
@Component
@Endpoint(id = "database-queries")
@RequiredArgsConstructor
public class DatabaseQueriesEndpoint {
private final DatabaseQueryTracker queryTracker;
@ReadOperation
public DatabaseQueriesInfo getDatabaseQueries() {
Map<String, DatabaseQueryTracker.QueryStatistics> stats = queryTracker.getQueryStatistics();
List<DatabaseQueryTracker.QueryExecution> recent = queryTracker.getRecentQueries();
// Top 10 langsamste Queries
List<SlowQuery> slowQueries = stats.values().stream()
.map(s -> new SlowQuery(
s.getRepository() + "." + s.getMethod(),
s.getAverageDuration(),
s.getMaxDuration(),
s.getTotalCount().get()
))
.sorted(Comparator.comparingDouble(SlowQuery::getAverageDuration).reversed())
.limit(10)
.collect(Collectors.toList());
// Top 10 meist genutzte Queries
List<PopularQuery> popularQueries = stats.values().stream()
.map(s -> new PopularQuery(
s.getRepository() + "." + s.getMethod(),
s.getTotalCount().get(),
s.getAverageDuration()
))
.sorted(Comparator.comparingLong(PopularQuery::getCount).reversed())
.limit(10)
.collect(Collectors.toList());
// Gesamt-Statistiken
long totalQueries = stats.values().stream()
.mapToLong(s -> s.getTotalCount().get())
.sum();
long totalErrors = stats.values().stream()
.mapToLong(s -> s.getErrorCount().get())
.sum();
double overallErrorRate = totalQueries > 0 ?
(double) totalErrors / totalQueries * 100 : 0;
return new DatabaseQueriesInfo(
totalQueries,
totalErrors,
overallErrorRate,
stats.size(),
slowQueries,
popularQueries,
recent
);
}
@ReadOperation
@Selector
public Map<String, Object> getQueryDetails(@Selector String repository) {
return queryTracker.getQueryStatistics().entrySet().stream()
.filter(e -> e.getKey().startsWith(repository))
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> Map.of(
"count", e.getValue().getTotalCount().get(),
"avgDuration", e.getValue().getAverageDuration(),
"maxDuration", e.getValue().getMaxDuration(),
"minDuration", e.getValue().getMinDuration(),
"errorRate", e.getValue().getErrorRate()
)
));
}
@WriteOperation
public Map<String, String> resetStatistics() {
queryTracker.reset();
return Map.of("status", "Statistics reset successfully");
}
// DTOs
record DatabaseQueriesInfo(
long totalQueries,
long totalErrors,
double overallErrorRate,
int uniqueQueries,
List<SlowQuery> slowQueries,
List<PopularQuery> popularQueries,
List<DatabaseQueryTracker.QueryExecution> recentQueries
) {}
record SlowQuery(String query, double avgDuration, long maxDuration, long count) {}
record PopularQuery(String query, long count, double avgDuration) {}
}
Was macht dieser Custom Endpoint im Detail?
@Endpoint Annotation:
@Endpoint(id = "database-queries")
Das registriert einen Custom Actuator-Endpoint! Er ist verfügbar unter: /actuator/database-queries
Das Prinzip dahinter:
Spring Boot findet automatisch alle Klassen mit @Endpoint und registriert sie! Du musst nichts konfigurieren – Magic!
@ReadOperation – GET Requests:
@ReadOperation
public DatabaseQueriesInfo getDatabaseQueries() {
Das ist ein Read-Only Endpoint. Er gibt Daten zurück, ändert aber nichts. Wird gemappt auf: GET /actuator/database-queries
Die Business-Logic:
Top 10 langsamste Queries:
Wir sortieren nach Average-Duration (absteigend) und nehmen die Top 10. Das zeigt: „Welche Queries sind am langsamsten?“
Top 10 meist genutzte Queries:
Wir sortieren nach Count (absteigend) und nehmen die Top 10. Das zeigt: „Welche Queries werden am häufigsten aufgerufen?“
Gesamt-Statistiken:
Total Queries, Total Errors, Error-Rate. Das gibt dir einen Überblick über die Database-Health.
Wichtig zu verstehen:
Wir nutzen Java Streams für die Aggregation! Das ist elegant und performant. Alternative wäre Schleifen – aber Streams sind lesbarer.
@Selector – Parameter in URL:
@ReadOperation
@Selector
public Map<String, Object> getQueryDetails(@Selector String repository) {
Das ermöglicht: GET /actuator/database-queries/{repository}
Beispiel: /actuator/database-queries/UserRepository gibt nur Queries von UserRepository zurück!
Das Prinzip dahinter:
Mit @Selector machst du deinen Endpoint flexibel! User können filtern was sie sehen wollen.
@WriteOperation – POST/PUT/DELETE:
@WriteOperation
public Map<String, String> resetStatistics() {
queryTracker.reset();
return Map.of("status", "Statistics reset successfully");
}
Das ist eine Schreib-Operation. Mapped auf: DELETE /actuator/database-queries oder POST /actuator/database-queries
Warum DELETE?
„Reset“ ist konzeptuell eine DELETE-Operation – du löschst die Statistiken!
Configuration aktivieren:
# application.properties # Custom Endpoint aktivieren management.endpoints.web.exposure.include=health,info,metrics,prometheus,database-queries
Endpoint testen:
# Alle Query-Statistiken curl http://localhost:8080/actuator/database-queries | jq # Nur UserRepository-Statistiken curl http://localhost:8080/actuator/database-queries/UserRepository | jq # Statistiken zurücksetzen curl -X DELETE http://localhost:8080/actuator/database-queries
Response-Beispiel:
{
"totalQueries": 156,
"totalErrors": 2,
"overallErrorRate": 1.28,
"uniqueQueries": 8,
"slowQueries": [
{
"query": "UserRepository.findByEmailDomain",
"avgDuration": 245.5,
"maxDuration": 450,
"count": 12
},
{
"query": "UserRepository.findByActiveTrue",
"avgDuration": 89.2,
"maxDuration": 150,
"count": 45
}
],
"popularQueries": [
{
"query": "UserRepository.findById",
"count": 78,
"avgDuration": 12.5
},
{
"query": "UserRepository.save",
"count": 45,
"avgDuration": 25.3
}
],
"recentQueries": [
{
"repository": "UserRepository",
"method": "findById",
"durationMs": 15,
"success": true,
"timestamp": "2025-10-20T15:30:45"
}
]
}
Was siehst du hier?
- ✅ 156 Queries insgesamt ausgeführt
- ✅ 2 Errors (1.28% Error-Rate – sehr gut!)
- ✅ findByEmailDomain ist am langsamsten (245ms average!)
- ✅ findById wird am häufigsten genutzt (78x)
- ✅ Letzte Queries mit Timestamp
In der Praxis bedeutet das:
Du siehst sofort welche Queries Performance-Probleme machen! findByEmailDomain mit 245ms average ist ein Kandidat für Optimierung – vielleicht fehlt ein Index?
Monitoring-Dashboard Integration:
Du kannst diese Daten in Grafana visualisieren:
# Prometheus Exporter für Custom Endpoint (Optional)
# Erstelle Micrometer-Gauges basierend auf den Daten
@Component
@RequiredArgsConstructor
public class DatabaseQueryMetricsExporter {
private final DatabaseQueryTracker queryTracker;
private final MeterRegistry meterRegistry;
@Scheduled(fixedRate = 15000) // Alle 15 Sekunden
public void exportMetrics() {
queryTracker.getQueryStatistics().forEach((key, stats) -> {
Gauge.builder("database.query.count", stats.getTotalCount(), AtomicLong::get)
.tag("query", key)
.register(meterRegistry);
Gauge.builder("database.query.avg_duration", () -> stats.getAverageDuration())
.tag("query", key)
.register(meterRegistry);
});
}
}
Das Ergebnis:
Deine Database-Query-Statistiken erscheinen in Prometheus und können in Grafana visualisiert werden! Professional Production Monitoring! 🚀
Schritt 9: Alerting mit Prometheus (Bonus)
Das Problem:
Du hast jetzt Monitoring – aber du sitzt nicht 24/7 vor Grafana! Wenn nachts um 3 Uhr die App crashed, willst du sofort benachrichtigt werden!
Die Lösung: Prometheus Alertmanager!
alertmanager.yml – Alert Configuration:
global:
resolve_timeout: 5m
route:
group_by: ['alertname']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: 'email'
receivers:
- name: 'email'
email_configs:
- to: 'devops@example.com'
from: 'alertmanager@example.com'
smarthost: 'smtp.gmail.com:587'
auth_username: 'your-email@gmail.com'
auth_password: 'your-app-password'
prometheus-rules.yml – Alert Rules:
groups:
- name: application_alerts
interval: 30s
rules:
# High Memory Usage
- alert: HighMemoryUsage
expr: (jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}) * 100 > 80
for: 5m
labels:
severity: warning
annotations:
summary: "High Memory Usage (instance {{ $labels.instance }})"
description: "Memory usage is above 80% for 5 minutes."
# High Error Rate
- alert: HighErrorRate
expr: (rate(http_server_requests_seconds_count{status=~"5.."}[5m]) / rate(http_server_requests_seconds_count[5m])) * 100 > 5
for: 2m
labels:
severity: critical
annotations:
summary: "High Error Rate (instance {{ $labels.instance }})"
description: "Error rate is above 5% for 2 minutes."
# Slow Database Queries
- alert: SlowDatabaseQueries
expr: database_query_avg_duration > 1000
for: 5m
labels:
severity: warning
annotations:
summary: "Slow Database Queries detected"
description: "Query {{ $labels.query }} average duration is {{ $value }}ms."
# Application Down
- alert: ApplicationDown
expr: up{job="spring-boot-app"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Application is DOWN"
description: "Application {{ $labels.instance }} is not responding."
Docker Compose mit Alertmanager:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./prometheus-rules.yml:/etc/prometheus/rules.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
alertmanager:
image: prom/alertmanager:latest
ports:
- "9093:9093"
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml
command:
- '--config.file=/etc/alertmanager/alertmanager.yml'
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-storage:/var/lib/grafana
volumes:
grafana-storage:
In der Praxis bedeutet das:
Wenn Memory über 80% geht → Email-Alert! Wenn Error-Rate über 5% → Email-Alert! Du wirst proaktiv benachrichtigt statt reaktiv zu debuggen.
Schritt 10: Complete Production Stack (Bonus)
Der komplette Stack:
# docker-compose.prod.yml
version: '3.8'
services:
app:
build: .
restart: unless-stopped
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/appdb
- SPRING_DATASOURCE_USERNAME=appuser
- SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
depends_on:
- db
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 3s
retries: 3
db:
image: postgres:15-alpine
restart: unless-stopped
environment:
- POSTGRES_DB=appdb
- POSTGRES_USER=appuser
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser"]
interval: 10s
timeout: 5s
retries: 5
prometheus:
image: prom/prometheus:latest
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- ./monitoring/prometheus-rules.yml:/etc/prometheus/rules.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
alertmanager:
image: prom/alertmanager:latest
restart: unless-stopped
ports:
- "9093:9093"
volumes:
- ./monitoring/alertmanager.yml:/etc/alertmanager/alertmanager.yml
grafana:
image: grafana/grafana:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource
volumes:
- grafana-data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources
volumes:
postgres-data:
prometheus-data:
grafana-data:
.env File für Secrets:
# .env DB_PASSWORD=super-secret-password GRAFANA_PASSWORD=admin-password-change-me
Starten:
docker-compose -f docker-compose.prod.yml up -d
Das hast du jetzt:
- ✅ Spring Boot App mit Actuator
- ✅ PostgreSQL Database
- ✅ Prometheus für Metriken
- ✅ Alertmanager für Alerts
- ✅ Grafana für Dashboards
- ✅ Health Checks für alle Services
- ✅ Persistent Volumes
- ✅ Auto-Restart
Production-ready! 🚀
✅ Checkpoint: Hast du Tag 9 geschafft?
Kontrolliere:
- [ ] Actuator Dependency hinzugefügt
- [ ] Health-Endpoint konfiguriert
- [ ] Standard-Metriken verstanden
- [ ] Custom Health Indicator erstellt
- [ ] Custom Metriken mit Micrometer erfasst
- [ ] Prometheus Integration aufgebaut
- [ ] Grafana Dashboard erstellt
- [ ] Actuator Security konfiguriert
- [ ] Custom Actuator Endpoint (Database-Queries) implementiert
- [ ] Docker Compose Stack gestartet
Alles ✅? Du bist bereit für Tag 10 – den letzten Tag!
Nicht alles funktioniert?
- Actuator-Endpoints nicht erreichbar? →
management.endpoints.web.exposure.includeprüfen - Prometheus findet keine Metriken? →
/actuator/prometheuserreichbar? - Grafana zeigt keine Daten? → Data Source auf
http://prometheus:9090gesetzt? - Custom Endpoint nicht sichtbar? →
@EndpointAnnotation vergessen? - AOP funktioniert nicht? →
spring-boot-starter-aopDependency fehlt? - Advanced Spring Boot Courses – https://medium.com/javarevisited/10-advanced-spring-boot-courses-for-experienced-java-developers-5e57606816bd Kuratierte Liste für erfahrene Java-Entwickler (Actuator, Testing, Cloud-Deployment)
🔥 Elyndras Real Talk:
Mein Monitoring-Disaster von 2022:
Erinnerst du dich an meine Horror-Story vom Anfang? Freitagabend, App down, 4 Stunden debugging ohne Monitoring?
Was ich daraus gelernt habe:
1. Monitoring ist NICHT optional
Früher dachte ich: „Monitoring kommt später, erstmal Features!“ Falsch!
Die Wahrheit: Ohne Monitoring bist du blind. Jede Production-App braucht:
- ✅ Health Checks (für Load-Balancer)
- ✅ Metriken (Memory, CPU, Requests)
- ✅ Alerts (proaktive Benachrichtigung)
2. Custom Metriken sind Gold wert
Standard-Metriken (Memory, CPU) sind wichtig. Aber Business-Metriken sind kritischer!
Meine Must-Have Business-Metriken:
- User-Registrierungen pro Minute
- Order-Success-Rate
- Payment-Fehlerrate
- Email-Versand-Erfolgsrate (von Tag 7!)
- Database-Query-Performance (unser Custom Endpoint!)
3. Dashboards vs Alerts
Dashboards sind zum Analysieren. Alerts sind zum Reagieren!
Meine Regel:
- Dashboard: „Wie ist der Zustand der App?“
- Alert: „Die App hat ein Problem – handle JETZT!“
4. Der Database-Query-Tracker war ein Game-Changer
Nach dem Memory-Leak-Disaster habe ich den Database-Query-Tracker gebaut (den wir heute implementiert haben!).
Was ich damit gefunden habe:
- ❌ Eine Query die 2.5 Sekunden dauerte (fehlender Index!)
- ❌ 50% aller Queries gingen an eine einzige Tabelle (Caching-Kandidat!)
- ❌ Error-Rate von 12% bei einer bestimmten Query (Bug im Code!)
Ohne diesen Tracker hätte ich das NIEMALS gefunden!
5. Production ist anders als Dev
In Dev läuft alles smooth. In Production:
- 1000x mehr Load
- Netzwerk-Latenz
- Database-Contention
- Memory-Leaks über Tage
- Race-Conditions bei Concurrency
Monitoring zeigt dir die Realität!
Mein Monitoring-Setup heute:
┌─────────────────┐
│ Spring Boot │
│ + Actuator │
│ + Micrometer │
└────────┬────────┘
│
↓ scrape every 15s
┌─────────────────┐
│ Prometheus │
│ (30 days data) │
└────────┬────────┘
│
┌────┴────┐
↓ ↓
┌────────┐ ┌────────────┐
│Grafana │ │Alertmanager│
└────────┘ └─────┬──────┘
↓
📧 Email
📱 Slack
🔔 PagerDuty
Das Ergebnis:
- ✅ Alerts für kritische Issues
- ✅ Dashboards für Analyse
- ✅ Historische Daten für Trends
- ✅ Proaktive Problem-Erkennung
Seit 2022: Kein einziger Freitagabend-Disaster mehr! 🎉
Meine Empfehlung für dich:
Minimum für Production:
- ✅ Health-Endpoint für Load-Balancer
- ✅ Prometheus + Grafana
- ✅ Alerts für: Memory > 80%, Error-Rate > 5%, App Down
Nice-to-Have:
- ✅ Custom Business-Metriken
- ✅ Database-Query-Tracking
- ✅ Slow-Query-Alerts
- ✅ Custom Actuator-Endpoints
Professional:
- ✅ Distributed Tracing (Zipkin/Jaeger)
- ✅ Log-Aggregation (ELK-Stack)
- ✅ APM (Application Performance Monitoring)
Start simple, add complexity as needed!
❓ FAQ (Häufige Fragen)
Q: Wie viele Actuator-Endpoints soll ich aktivieren?
A: Minimum: health, metrics, prometheus. Development: Alle! Production: Nur die nötigen + Security! Zu viele Endpoints = Angriffsfläche.
Q: Kann Actuator Performance-Impact haben?
A: Minimal! Actuator ist optimiert. Der /health Check ist < 1ms. Metriken-Collection ist negligible. Nur Heap-Dumps sind teuer – aber die machst du selten.
Q: Prometheus vs InfluxDB vs DataDog?
A: Prometheus: Open-Source, Standard für Kubernetes. InfluxDB: Bessere Query-Language, komplexer. DataDog: Commercial, All-in-One, teuer. Start mit Prometheus – gratis und gut!
Q: Wie lange Metriken speichern?
A: 30 Tage ist Standard. Für längere Trends: Prometheus -> Long-Term-Storage (Thanos, Cortex). Oder: Aggregiere zu täglichen/wöchentlichen Summaries.
Q: Custom Health Indicators – wie viele?
A: Eine pro kritische Dependency! Database, Email-Service, External-APIs, Redis, etc. Aber nicht übertreiben – zu viele Health-Checks verlangsamen /health.
Q: Kann ich Actuator-Endpoints unterschiedlich sichern?
A: Ja! Mit mehreren SecurityFilterChains. Beispiel: /health public, /metrics ROLE_MONITORING, /env ROLE_ADMIN.
Q: Docker Compose vs Kubernetes für Monitoring?
A: Docker Compose: Dev/Small-Production. Kubernetes: Large-Scale. Kubernetes hat besseres Monitoring (Prometheus Operator, Service-Discovery). Start mit Compose!
Q: Was macht ihr bei persönlichen Problemen zwischen den Projekten?
A: Das ist… kompliziert. Manche Geschichten gehören nicht in Tech-Blogs, sondern in private logs. Aber das ist ein anderes Kapitel. 🔒
📅 Nächster Kurstag: Tag 10
Morgen im Kurs / Nächster Blogbeitrag:
„Template Engines & Microservices Intro“
Was du lernen wirst:
- Thymeleaf Templates & Layouts
- Form-Handling mit Thymeleaf
- Microservices Basics
- Spring Cloud Config
- Service Discovery mit Eureka
- API Gateway Pattern
Dauer: 8 Stunden
Voraussetzung: Tag 9 abgeschlossen
👉 Zum Blogbeitrag Tag 10 (erscheint morgen)
📚 Deine Fortschritts-Übersicht
| Tag | Thema | Status |
|---|---|---|
| ✅ 1 | Auto-Configuration & Starter | ABGESCHLOSSEN! 🎉 |
| ✅ 2 | Spring Data JPA Basics | ABGESCHLOSSEN! 🎉 |
| ✅ 3 | JPA Relationships & Queries | ABGESCHLOSSEN! 🎉 |
| ✅ 4 | Spring Security Part 1 | ABGESCHLOSSEN! 🎉 |
| ✅ 5 | Spring Security Part 2 | ABGESCHLOSSEN! 🎉 |
| ✅ 6 | Spring Boot Caching & JSON | ABGESCHLOSSEN! 🎉 |
| ✅ 7 | Messaging & Email | ABGESCHLOSSEN! 🎉 |
| ✅ 8 | Testing & Dokumentation | ABGESCHLOSSEN! 🎉 |
| ✅ 9 | Spring Boot Actuator | ABGESCHLOSSEN! 🎉 |
| → 10 | Template Engines & Microservices | Der letzte Tag! |
Du hast 90% des Kurses geschafft! 💪
Alle Blogbeiträge dieser Serie:
👉 Spring Boot Aufbau – Komplette Übersicht
📥 Download & Ressourcen
Projekt zum Download:
👉 SpringBootAufbau-Tag9-Actuator-v1.0.zip (Stand: 21.10.2025)
Was ist im ZIP enthalten:
- ✅ Komplettes Maven-Projekt mit Actuator
- ✅ Custom Health Indicators (Email, External-API)
- ✅ Custom Metriken (Counter, Timer)
- ✅ Custom Actuator Endpoint (Database-Queries)
- ✅ AOP Aspect für Query-Tracking
- ✅ Docker Compose Stack (App, Prometheus, Grafana, Alertmanager)
- ✅ Prometheus Config & Alert-Rules
- ✅ Grafana Dashboard (JSON)
- ✅ Security Config für Actuator
- ✅ README mit Schnellstart
- ✅ AUFGABEN.md mit 8-Stunden-Plan
Projekt starten:
# ZIP entpacken # Docker Stack starten (Prometheus + Grafana) docker-compose up -d # App starten mvn spring-boot:run # Actuator-Endpoints testen curl http://localhost:8080/actuator/health | jq curl http://localhost:8080/actuator/metrics | jq curl http://localhost:8080/actuator/database-queries | jq # Prometheus öffnen http://localhost:9090 # Grafana öffnen (admin/admin) http://localhost:3000
AUFGABEN.md – Dein 8-Stunden-Plan:
# Tag 9: Spring Boot Actuator - 8 Stunden Lernplan ## Stunde 1-2: Actuator Basics (2h) - [ ] Actuator Dependency hinzufügen - [ ] Standard-Endpoints verstehen (/health, /metrics, /info) - [ ] Actuator konfigurieren (exposure.include) - [ ] Health-Endpoint mit Details testen ## Stunde 3-4: Custom Health & Metriken (2h) - [ ] Custom Health Indicator erstellen (Email-Service) - [ ] Custom Health Indicator erstellen (External-API) - [ ] Counter-Metrik implementieren - [ ] Timer-Metrik für Performance-Messung - [ ] Metriken über /metrics abrufen ## Stunde 5-6: Prometheus & Grafana (2h) - [ ] Prometheus mit Docker starten - [ ] prometheus.yml konfigurieren - [ ] /actuator/prometheus Endpoint testen - [ ] Grafana mit Docker starten - [ ] Data Source konfigurieren - [ ] Dashboard mit 5 Panels erstellen - [ ] Alert-Rules definieren ## Stunde 7: Custom Actuator Endpoint (1h) - [ ] DatabaseQueryTracker implementieren - [ ] AOP Aspect für Query-Tracking - [ ] Custom Endpoint erstellen - [ ] Endpoint testen (/actuator/database-queries) - [ ] Top-10 Slow-Queries analysieren ## Stunde 8: Security & Production (1h) - [ ] Actuator Security konfigurieren - [ ] ACTUATOR-Rolle erstellen - [ ] Separate Port für Actuator (optional) - [ ] Checkpoint durchgehen - [ ] Complete Stack mit docker-compose starten ## Bonus-Aufgaben: - [ ] Alertmanager konfigurieren - [ ] Email-Alerts testen - [ ] Custom Dashboard in Grafana bauen - [ ] Slow-Query-Threshold anpassen
Probleme? Issue melden oder schreib mir: elyndra@java-developer.online
Das war Tag 9 von Spring Boot Aufbau!
Du kannst jetzt:
- ✅ Actuator Endpoints konfigurieren und nutzen
- ✅ Custom Health Indicators erstellen
- ✅ Eigene Metriken mit Micrometer erfassen
- ✅ Prometheus Integration aufbauen
- ✅ Grafana Dashboards erstellen
- ✅ Actuator-Endpoints absichern
- ✅ Custom Actuator Endpoints implementieren
- ✅ Database-Query-Performance tracken mit AOP
- ✅ Production-ready Monitoring-Stack deployen
- ✅ Alerts für kritische Issues konfigurieren
Morgen ist der letzte Tag – Template Engines & Microservices! 🚀
Keep coding, keep learning! 💙
Tag 10 erscheint morgen. Bis dahin: Happy Monitoring!
P.S.: Manchmal verstecken sich die interessantesten Geschichten nicht in Code-Repositories, sondern in den… nun ja, private logs. Probier mal die Suche oben auf java-developer.online! 😉
Tags: #SpringBoot #Actuator #Monitoring #Prometheus #Grafana #Micrometer #Production #Tag9

