1. Nützliche Logs im Container
Wenn ein Service im Container zu leben beginnt, werden Logs plötzlich nicht mehr ein „schönes Extra“, sondern ein Teil des Überlebens. Im Gegensatz zum lokalen Start in der IDE, wo Sie auf den Code klicken und einen Breakpoint setzen können, sehen Sie im Container meistens nur Symptome: Containerstatus, ein paar Logzeilen und den verwirrten Blick eines Kollegen. Deshalb brauchen wir nicht einfach irgendwelche Logs, sondern Logs, die den Weg von „funktioniert nicht“ zu „aha, darum“ verkürzen.
Der häufigste Anfängerfehler sieht so aus: Die Anwendung schreibt ehrlich etwas in die Konsole, aber dieser Text beantwortet keine praktische Frage. So nach dem Motto „operation failed“ oder „start ok“. Es gibt zwar ein Log … aber es spart überhaupt keine Zeit. Und wenn es viele Logs gibt, dann schaden sie sogar, weil die wichtige Nachricht in zehntausend „na ja, ich hab mich kurz in der Schleife gedreht“ untergeht.
Damit Logs nützlich sind, ist es praktisch, ein einfaches Kriterium im Kopf zu behalten: Jede wichtige Zeile sollte mindestens drei Fragen beantworten. Was ist passiert? Womit ist es passiert (welches Objekt, ID, Datei, SKU, Profil)? Wie ist es ausgegangen (Erfolg, Übersprung, Fehler)? Das ist wie ein Kassenbon: „was gekauft“ + „wie viel“ + „Summe“. Der Bon erzählt nicht die ganze Geschichte Ihres Lebens – er hält die Tatsache fest. Ein gutes Log macht dasselbe.
Man kann sich den Weg eines Logs wie eine sehr bodenständige Kette vorstellen:
flowchart LR
A["Code im Service
log.info(...)"] --> B["Logger (SLF4J)"]
B --> C["Console-Ausgabe
stdout/stderr"]
C --> D["docker logs"]
D --> E["Entwickler liest
und zieht Schlussfolgerungen"]
Wenn der Entwickler am Ende der Kette keine Schlussfolgerung ziehen kann, liegt das Problem nicht an Docker oder der „schlechten Umgebung“, sondern an der Qualität der Nachricht.
2. Logger statt println: ohne Snobismus
Am Anfang hat man große Lust, es so zu machen wie in den Java-Lehrbüchern: System.out.println("hello"). Es funktioniert ja auch und zeigt die grundlegende Mechanik gut: Der Prozess schreibt auf stdout/stderr und Docker sammelt das. Aber in einem echten Service ist println wie „alles normal“ auf einen Zettel zu schreiben und an den Monitor zu kleben: es sieht flott aus, aber man kann es nicht steuern. Sie brauchen Ebenen (INFO/WARN/ERROR), Format, die Möglichkeit, die „Geschwätzigkeit“ ohne Umschreiben des Codes auszuschalten, und eine ordentliche Arbeit mit Exceptions.
In Spring Boot ist der Basisweg fast immer so: Wir verwenden SLF4J (org.slf4j), und innerhalb von Boot ist standardmäßig Logback enthalten. Im Ergebnis schreiben wir im Code über Logger, und die Infrastruktur entscheidet selbst, wie das auf die Konsole ausgegeben wird (und damit, wie es in die docker logs gelangt).
Ein minimales „Skelett“ für das Logging in einem Service sieht so aus:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExportService {
// Logger werden normalerweise static final gemacht: eine Instanz pro Klasse, ohne Neuerstellung bei jedem Aufruf
private static final Logger log = LoggerFactory.getLogger(ExportService.class);
}
Und hier gibt es einen wichtigen Gedanken: Der Logger wird fast immer static final gemacht, um ihn nicht bei jedem Aufruf neu zu erstellen. Das ist ein kleines Detail, aber es spiegelt den allgemeinen Ansatz wider: Logging ist ein systemischer Mechanismus, nicht „na, ein paar Zeilen drucken wir mal“.
Eine weitere Sache, die man gleich akzeptieren sollte: Logs in Spring Boot sind nicht der Ort, an dem Sie „einen String schreiben“. Sie sind der Ort, an dem Sie ein Ereignis festhalten. Deshalb werden wir im Folgenden Nachrichten mit Kontext schreiben, keine Romane.
3. Ebenen INFO, WARN, ERROR
Die Logging-Ebenen klingen pathetisch, aber im Grunde sind sie eine einfache Vereinbarung „wie wichtig ist das und wie schlimm ist es“. Das Problem ist, dass ein Entwickler am Anfang zwei Extreme kennt. Das erste – alles über INFO loggen, weil „ich will es ja sehen“. Das zweite – alles auf ERROR setzen, weil „das ist doch ein Problem, ich mache mir Sorgen“. Und in beiden Fällen werden die Logs nutzlos: entweder zu laut oder alles sieht aus wie ein Feuer, selbst wenn es eine normale Situation ist.
Für unseren Kurs reicht es, drei Ebenen sicher zu beherrschen:
| Ebene | Wann angemessen | Wie klingt es menschlich |
|---|---|---|
| INFO | wichtiges normales Ereignis | „Alles nach Plan, merk dir den Fakt“ |
| WARN | Situation seltsam/verdächtig, aber Service läuft weiter | „Gefällt mir nicht, aber ich lebe“ |
| ERROR | Operation fehlgeschlagen, Weg kaputt | „Das ist ein echter Fehler, Reaktion erforderlich“ |
Wenn Sie eine etwas mechanischere Regel wollen (als Merkhilfe, wenn das Gehirn müde ist), hier ein einfaches Schema zur Auswahl der Ebene:
flowchart TD
A[Ereignis in der Anwendung] --> B{Ist das eine erwartete normale Situation?}
B -->|Ja| C{Ist das wichtig für das Verständnis des Service-Lebens?}
C -->|Ja| I[INFO]
C -->|Nein| S[Nicht loggen]
B -->|Nein| D{Läuft der Service weiter?}
D -->|Ja| W[WARN]
D -->|Nein / Operation kaputt| E[ERROR]
Dieses Schema ist nützlich, weil es Ihnen erlaubt, einen Teil der Ereignisse ehrlich nicht zu loggen. Logs sind keine Blackbox eines Flugzeugs, in der man jeden Atemzug der CPU aufzeichnen muss. Logs sind ein Diagnosewerkzeug. Wenn ein Ereignis der Diagnose nicht hilft und keine wichtige Tatsache festhält, sollte es oft nicht in die Konsole gelangen.
4. Log-Nachricht: Kontext und Ergebnis
Wenn Leute sagen „mach das Log nützlich“, klingt das wie ein Motivationsplakat. Lassen wir uns auf den Boden holen: Die Nützlichkeit scheitert meist am fehlenden Kontext. Die Nachricht „export failed“ ist fast nutzlos. Aber „export failed: file=catalog-2026-03-21.csv“ ist schon konkret. In der Containerdiagnose denken Sie fast immer in Objekten: welche Datei, welche Item-ID, welches Profil, welcher Modus.
Ein sehr praktischer Trick ist, die Struktur der Nachricht im Kopf als Vorlage zu behalten:
Ereignis → Kontext → Ergebnis.
Zum Beispiel für den Export:
- Ereignis: Export gestartet,
- Kontext: Dateiname und Verzeichnis,
- Ergebnis: wie viele Elemente exportiert wurden und wie lange es gedauert hat (falls sinnvoll), oder was genau kaputt gegangen ist.
Um den Unterschied zu spüren, vergleichen wir „schlecht“ und „besser“ in einer kleinen Tabelle:
| Schlecht | Besser | Warum besser |
|---|---|---|
| Export started | Export started: file={} | klar, welche Datei |
| Failed | Export failed: file={} + exception | Kontext und Stack vorhanden |
| No items | Export skipped: no catalog items (WARN) | sichtbar, dass es kein Unfall ist, sondern ein Überspringen |
Und hier ist wichtig: In Spring Boot (über SLF4J) verwenden wir normalerweise parametrisierte Nachrichten mit {}. Das ist keine „Marotte“, sondern normale Praxis.
// {} ist ein SLF4J-Platzhalter: der Wert wird sicher und einheitlich eingesetzt
log.info("Export started: file={}", fileName);
Warum ist das besser als Verkettung?
// Verkettung macht den Code lauter und zwingt, den String manuell zusammenzubauen
log.info("Export started: file=" + fileName);
Parametrisierung macht die Nachricht standardisiert, lesbar und (was schön ist) spart unnötige Berechnungen, wenn die Logging-Ebene ausgeschaltet ist. Sie müssen sich diese Optimierung jetzt nicht „verkaufen“, aber ein schöner Bonus ist, dass dieser Stil für normale Projekte grundlegend ist.
5. Startup-Logs: die ersten 5 Sekunden
Der Start einer Anwendung ist der Moment, in dem Logging den größten Nutzen bringt. Gerade am Anfang zeigen sich oft Konfigurationsprobleme: das falsche Profil, der falsche Port, das falsche Exportverzeichnis, die falschen Umgebungsvariablen. Und genau beim Start schauen Sie meist zuerst in die Container-Logs. Daher ist es sinnvoll, im Voraus zu entscheiden: Welche 2–4 Tatsachen wollen wir sehen, wenn der Service „hochgefahren“ ist?
Spring Boot selbst schreibt viele Start-Logs, und das ist normal. Aber in unserem Projekt gibt es einige domänen- und infrastrukturspezifische Dinge, die gerade wir explizit sagen sollten. Zum Beispiel, welche Profile aktiv sind (damit wir nicht raten müssen „warum greift er auf die DB zu, ich wollte doch standalone“), und welcher Exportpfad tatsächlich verwendet wird (besonders nachdem wir mit Mounts gearbeitet haben).
Der einfachste Lehrweg ist, eine kleine Komponente im Paket ops zu erstellen, die auf das Ereignis „Anwendung bereit“ reagiert und eine ordentliche Zeile schreibt. Ohne Fanatismus: eine Zeile – eine Tatsache.
Beispiel für einen minimalen Start-Logger:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class StartupLogger {
private static final Logger log = LoggerFactory.getLogger(StartupLogger.class);
// Wird einmal ausgelöst, wenn Spring Boot vollständig hochgefahren ist und bereit ist, Anfragen entgegenzunehmen
@EventListener
public void onReady(ApplicationReadyEvent event) {
// Dies ist der „Bereitschaftsmarker“ in den docker logs: sofort sichtbar, dass der Service den normalen Zustand erreicht hat
log.info("Catalog service is ready to accept requests");
}
}
Ja, das ist noch ein „schwaches“ Log: Es enthält keine Profile und Parameter. Aber es ist bereits nützlich, weil Sie in den Container-Logs einen klaren Marker sehen „wir haben den bereiten Zustand erreicht“. Und dann können wir Kontext hinzufügen, aber vorsichtig: ohne Geheimnisse auszugeben und ohne „die ganze Welt“ zu drucken.
Wenn wir Profile loggen wollen, nehmen wir normalerweise Environment. Im folgenden Beispiel beachten Sie: Wir loggen nicht die gesamte Konfiguration, sondern eine einzige kurze, sichere Tatsache.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
public class ProfilesLog {
private static final Logger log = LoggerFactory.getLogger(ProfilesLog.class);
public static void logProfiles(Environment env) {
// Wir nehmen nur die aktiven Profile: das ist sicher und hilft schnell zu verstehen, „in welchem Modus“ wir gestartet sind
String profiles = String.join(",", env.getActiveProfiles());
log.info("Active profiles: {}", profiles); // Active profiles: standalone
}
}
Diese Detailtiefe beim Start reicht bereits aus, um in den Docker-Logs nicht raten zu müssen.
6. Geschäftslogik des Exports: Start, Übersprung, Erfolg
Der Katalogexport in eine Datei ist unser sehr „containerartiges“ Szenario: Er betrifft das Dateisystem, hängt vom Exportverzeichnis ab und geht leicht wegen Pfaden oder Zugriffsrechten kaputt. Daher ist dies der ideale Ort, um einen „korrekten“ Satz von Logs zu zeigen: Start der Operation, nicht standardmäßige Situation ohne Absturz, erfolgreicher Abschluss und Fehler mit Exception.
Beginnen wir mit dem Start der Operation. Es ist wichtig, den Dateinamen (Kontext) festzuhalten, weil der Student später diese Datei auf dem Host-Rechner suchen oder den Mount überprüfen wird.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExportService {
private static final Logger log = LoggerFactory.getLogger(ExportService.class);
public void export(String fileName) {
// Wichtig: Kontext festhalten (welche Datei), um später das Log mit dem tatsächlichen Ergebnis auf dem Dateisystem abgleichen zu können
log.info("Export started: file={}", fileName);
}
}
Jetzt fügen wir eine „nicht standardmäßige, aber keine Notfall“-Situation hinzu: nichts zu exportieren. Das ist kein Anwendungsfehler, der Service lebt, aber das Ereignis ist nützlich zu sehen, weil der Benutzer sonst auf die Datei wartet und denkt „sie wurde nicht erstellt, alles kaputt“.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExportService {
private static final Logger log = LoggerFactory.getLogger(ExportService.class);
public void exportIfAny(int itemsCount) {
if (itemsCount == 0) {
// WARN: Situation untypisch, aber Service lebt weiter
log.warn("Export skipped: no catalog items");
return;
}
// INFO: wichtige Tatsache des normalen Operationsverlaufs
log.info("Export will proceed: items={}", itemsCount);
}
}
Beachten Sie die Bedeutung der Ebene: WARN ist hier nicht „Aua, Feuer“, sondern „Situation verdächtig/untypisch“. Für einen Lehrservice ist das ein normales Modell: Der Benutzer hat den Export aufgerufen, aber der Katalog ist leer – wir warnen.
Ein erfolgreicher Abschluss ist ebenfalls eine wichtige Tatsache. Aber auch hier sollte man das Log nicht in eine „Dissertation über den Export“ verwandeln. Kurz: welche Datei und wie viele Elemente.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExportService {
private static final Logger log = LoggerFactory.getLogger(ExportService.class);
public void finish(String fileName, int itemsCount) {
// Bei Erfolg das Minimum festhalten: wohin geschrieben und wie viel exportiert
log.info("Export finished: file={}, items={}", fileName, itemsCount);
}
}
Und schließlich der Fehler. Hier ist entscheidend, nicht einfach „failed“ zu schreiben, sondern Kontext hinzuzufügen und die Exception zu übergeben, damit im Log ein Stack Trace erscheint. In der Containerdiagnose ist das oft die einzige Chance, die Ursache zu verstehen.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class ExportService {
private static final Logger log = LoggerFactory.getLogger(ExportService.class);
public void handleError(String fileName, IOException ex) {
// Wichtig: ex als separaten Parameter übergeben – so gelangt der Stack Trace ins Log
log.error("Export failed: file={}", fileName, ex);
}
}
Eine solche Zeile liefert Ihnen meist sowohl den verständlichen Text als auch den gesamten Fehler-Stack. Das bedeutet: Wenn der Container „irgendwie lebt“, aber der Export keine Datei erzeugt, müssen Sie nicht raten – Sie sehen, was schiefgelaufen ist.
7. Fehler und Log-Konfiguration
Fehler protokollieren: Wichtig und Überflüssig
Fehler sind der „teuerste“ Teil des Loggens, weil wir in ihnen am häufigsten nach der Wahrheit suchen. Aber mit Fehlern kann man leicht zwei typische Dummheiten begehen. Die erste – einen Fehler ohne Exception loggen und nur ex.getMessage() behalten. Die zweite – eine Exception fangen, loggen und … weitermachen als ob nichts gewesen wäre, wodurch eine halb funktionsfähige Anwendung entsteht, die sich später mit seltsamen Symptomen rächt.
Die Regel ist recht einfach: Wenn Sie eine Exception fangen und für wichtig halten, sollten Sie sie fast immer mit dem Exception-Objekt loggen, um den Stack zu bewahren. Das sieht so aus:
log.error("Export failed: file={}", fileName, ex);
Und so ist es meist schlechter, weil der Stack verloren geht (oder zu arm ist):
log.error("Export failed: file={}, message={}", fileName, ex.getMessage());
Der zweite wichtige Teil – nicht Überflüssiges und nicht Gefährliches loggen. Zum Beispiel hat man große Lust, beim Start die datasource-URL auszugeben „um zu überprüfen“. Aber wenn Sie die URL zusammen mit dem Passwort (oder Token) loggen, schaffen Sie selbst ein Sicherheitsloch. In unserem Lehrprojekt halten wir bewusst Disziplin: Logs sollen der Diagnose helfen, aber nicht zum „Geheimnisverlust“ werden.
Wenn Sie trotzdem Kontext brauchen, suchen Sie nach einer sicheren Variante. Zum Beispiel kann man die Tatsache loggen „wir arbeiten im postgres-Modus“, aber nicht das Passwort loggen, nicht den vollständigen Connection-String mit Geheimnissen, nicht den Inhalt der Dateien, die wir exportieren.
Logging-Ebene über Konfiguration
Die angenehmste Seite des Loggers (im Vergleich zu println) ist, dass Sie die Detailtiefe der Nachrichten über die Konfiguration steuern können. Das heißt, Sie schreiben den Code nicht um („ich füge mal noch 20 Zeilen zum Debuggen hinzu“), sondern ändern die Logging-Ebene für die Dauer der Diagnose.
In Spring Boot wird das normalerweise über logging.level.* in der application.yml gemacht. Für unser Projekt ist eine vernünftige Baseline, die normale Ebene auf INFO für den eigenen Code zu lassen.
spring:
application:
# Der Anwendungsname landet oft in Logs/Metriken und hilft, Services in der Infrastruktur zu unterscheiden
name: catalog-service
logging:
level:
# Basis-Logebene für unseren Code: Wir sehen wichtige Fakten, ertrinken aber nicht im Rauschen
com.example.catalog: INFO
Wenn irgendwo „zu laut“ ist, können Sie die Ebene punktuell für ein bestimmtes Paket oder eine Klasse senken. Umgekehrt, wenn Sie vorübergehend den Export debuggen und mehr Details wünschen, erhöhen Sie die Ebene für com.example.catalog.export (z. B. auf DEBUG) – und setzen sie später zurück, ohne ein „lebenslanges Debug“ im Code zu hinterlassen.
Hier ist es wichtig, Disziplin zu wahren: Die Logging-Ebene ist ein Hebel, keine Krücke. Wenn Sie eine wichtige Tatsache im Normalmodus loggen müssen, sollte sie auf INFO/WARN/ERROR sein. Und wenn es „vorübergehendes Debuggen zum Verstehen“ ist, dann ist das ein Kandidat für eine niedrigere Ebene und das Einschalten über die Konfiguration. Wir machen den Service nicht dauerhaft geschwätzig, sonst geht jedes echte Signal verloren.
8. Typische Fehler
Fehler Nr. 1: „Logs um der Logs willen“ – Nachrichten ohne Kontext.
Anfänger schreiben oft so etwas wie export started und halten die Aufgabe für erledigt. Aber ohne Dateiname und Ergebnis können Sie später die Nachricht nicht mit dem tatsächlichen Effekt auf dem Dateisystem verbinden. Machen Sie lieber Nachrichten, die das Objekt der Operation festhalten: file, items, id, sku.
Fehler Nr. 2: Alles zu ERROR machen, weil „ich mache mir Sorgen“.
Wenn ein leerer Katalog als ERROR geloggt wird, erzeugen Sie ein falsches Gefühl eines Notfalls. Nach ein paar Wochen werden solche ERROR nicht mehr gelesen, wie „Fehlalarme einer Alarmanlage“. Die normale Skala ist einfach: INFO – wichtige Fakten, WARN – seltsam, aber wir leben, ERROR – der Operationsweg ist kaputt.
Fehler Nr. 3: Einen Fehler ohne Exception loggen und den Stack verlieren.
Der Satz „Export failed“ ohne Stack Trace ist fast wie „mir geht’s schlecht“ ohne Symptombeschreibung. Im Container können Sie nicht immer schnell in den Debug einsteigen, daher ist der Exception-Stack oft die Hauptquelle der Wahrheit. Verwenden Sie log.error("...", ex), nicht nur ex.getMessage().
Fehler Nr. 4: Geheimnisse und sensible Parameter „der Einfachheit halber“ drucken.
Es ist sehr verlockend, Passwörter, Token oder den vollständigen Connection-String mal kurz ins Log zu schreiben. Aber Logs leben leicht länger als Ihr „mal kurz“ und gelangen schneller in fremde Hände, als Sie „hoppla“ sagen können. Halten Sie in Logs nur sichere Fakten fest: Profile, Modus, Dateiname, Entitäts-IDs – ohne Geheimnisse.
Fehler Nr. 5: Laute Logs als dauerhaften Standard.
Wenn der Service bei jeder HTTP-Anfrage zehn Zeilen schreibt und bei jedem Katalogelement noch eine, werden die Logs zu einem Hintergrundrauschen, das niemand liest. Ein nützliches Log ist nicht das, was „häufig“ ist, sondern das, was „einen Sinn ergibt“. Lassen Sie die wichtigen Ereignisse sichtbar sein, anstatt im ständigen Geplapper unterzugehen.
GO TO FULL VERSION