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


Spring Boot Actuator

📍 Deine Position im Kurs

TagThemaStatus
1Auto-Configuration & Custom StarterVoraussetzung
2Spring Data JPA BasicsVoraussetzung
3JPA Relationships & QueriesVoraussetzung
4Spring Security Part 1 – AuthenticationVoraussetzung
5Spring Security Part 2 – AuthorizationVoraussetzung
6Spring Boot Caching & JSONVoraussetzung
7Messaging & EmailVoraussetzung
8Testing & DokumentationVoraussetzung
→ 9Spring Boot Actuator👉 DU BIST HIER!
10Template Engines & MicroservicesNoch 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:

EndpointBeschreibungBeispiel-Use-Case
/healthHealth StatusLoad-Balancer Checks
/infoApp-InformationenVersion anzeigen
/metricsMetrikenCPU, Memory überwachen
/envEnvironment ConfigConfig-Debugging
/loggersLog-LevelLog-Level zur Laufzeit ändern
/threaddumpThread-DumpDeadlock-Debugging
/heapdumpHeap-DumpMemory-Leak-Analyse
/prometheusPrometheus-MetrikenMonitoring-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:

  1. Config ändern
  2. App neu deployen
  3. 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:

  1. Data Source hinzufügen:
    • Configuration → Data Sources → Add data source
    • Prometheus auswählen
    • URL: http://prometheus:9090
    • Save & Test
  2. 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 in com.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?

  1. Zeit messen (Start)
  2. Echte Repository-Methode ausführen
  3. Zeit messen (Ende)
  4. 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.include prüfen
  • Prometheus findet keine Metriken? → /actuator/prometheus erreichbar?
  • Grafana zeigt keine Daten? → Data Source auf http://prometheus:9090 gesetzt?
  • Custom Endpoint nicht sichtbar? → @Endpoint Annotation vergessen?
  • AOP funktioniert nicht? → spring-boot-starter-aop Dependency fehlt?
  • Advanced Spring Boot Courseshttps://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

TagThemaStatus
✅ 1Auto-Configuration & StarterABGESCHLOSSEN! 🎉
✅ 2Spring Data JPA BasicsABGESCHLOSSEN! 🎉
✅ 3JPA Relationships & QueriesABGESCHLOSSEN! 🎉
✅ 4Spring Security Part 1ABGESCHLOSSEN! 🎉
✅ 5Spring Security Part 2ABGESCHLOSSEN! 🎉
✅ 6Spring Boot Caching & JSONABGESCHLOSSEN! 🎉
✅ 7Messaging & EmailABGESCHLOSSEN! 🎉
✅ 8Testing & DokumentationABGESCHLOSSEN! 🎉
✅ 9Spring Boot ActuatorABGESCHLOSSEN! 🎉
→ 10Template Engines & MicroservicesDer 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

Autor

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.