Logger-Design: Diagnose und Kontrolle in der Produktion priorisieren
In Produktionssystemen werden Logger nicht nach API-Geschwindigkeit bewertet, sondern danach, wie schnell sie bei der Fehlersuche helfen, ohne zusätzliche Last zu erzeugen. Logging verursacht Overhead, der im Normalbetrieb minimiert wird. Bei Problemen machen jedoch knappe Details oder chaotische Logs die Fehlersuche unmöglich. Ein guter Logger gewährleistet Diagnosierbarkeit, ohne die Performance zu opfern.
Das Problem beginnt, wenn die Log-Struktur dem Code folgt, statt Geschäftsprozessen. In Multithreading-Systemen vermischen sich Nachrichten aus verschiedenen Tasks, was das Nachverfolgen einer spezifischen Anfrage erschwert.
Kontext an Tasks binden
Die Lösung ist, Logs mit Tasks zu verknüpfen, nicht mit Aufrufstellen. Kontext über Parameter zu übergeben, verkompliziert Funktionssignaturen. Besser ist es, ihn an den Ausführungs-Thread zu binden.
In logme geschieht das über Thread-Kanäle:
auto requestCh = Logme::Instance->CreateChannel(Logme::ID("req-42"));
LogmeThreadChannel(requestCh);
HandleRequest();
Interner Code loggt automatisch in den Kanal:
void ParseHeaders() {
LogmeW("invalid header");
}
Logs gruppieren sich nun nach Operationen. In spdlog und Quill dreht sich der Kontext um den Logger, was einfacher, aber für Tasks weniger ideal ist.
Ein weiteres Problem ist Lärm durch wiederholte Nachrichten. Retry-Fehler fluten Logs mit Duplikaten. Event-Level-Limits lösen das:
LogmeW(LOGME_ONCE4THIS, "disk is full");
Die Nachricht erscheint einmal pro Klasseninstanz und hält Logs lesbar.
Flexible Nachrichten-Weiterleitung
Globale Konfiguration reicht nicht aus. Bei HTTP/2 müssen Verbindungs- und Anfrage-Logs getrennt werden. Ein Anfrage-Kanal verlinkt zum Protokoll-Kanal, aber Anfragedetails sollten den Hauptlog nicht überladen.
Logme::Override ovr;
ovr.Add.DisableLink = true;
LogmeI(ovr, "request-specific detail");
Das isoliert die Nachricht im lokalen Kanal. Allgemeine Events erscheinen überall; private bleiben lokal.
Subsysteme für präzise Details
Eine Dimension (Kanal) reicht nicht. Tasks haben Subsysteme: Cache, Netzwerk, Parsing. Selektiv loggen:
LOGME_SUBSYSTEM(cacheSid, "cache");
LOGME_SUBSYSTEM(parserSid, "parser");
LogmeD(cacheSid, "cache miss");
LogmeD(parserSid, "header parsed");
Zur Laufzeit nur das benötigte Subsystem aktivieren, z. B. Cache. Andere bleiben stumm, Logs bleiben kompakt.
Integration mit Bibliotheken
Drittanbieter-Bibliotheken haben eigenes Logging. Wir fangen es ab und integrieren:
void FfmpegLogCallback(void* ptr, int level, const char* fmt, va_list args) {
char buffer[2048];
vsnprintf(buffer, sizeof(buffer), fmt, args);
Logme::Override ovr;
ovr.Add.DisableLink = true;
LogmeW(ovr, "[ffmpeg] %s", buffer);
}
Nachrichten folgen denselben Regeln: Kanäle, Subsysteme, Deduplizierung.
Format-Unterstützung erleichtert Migration:
LogmeI("value=%i", value);
fLogmeI("value={}", value);
LogmeI() << "value=" << value;
logme kombiniert fmt- und Stream-Stile, im Gegensatz zu spdlog (nur fmt) und Quill (async fmt).
Produktionskontrolle ohne Neustarts
In dauerhaft laufenden Systemen bleiben Logs normalerweise minimal. Bei Vorfällen:
- Cache verdächtig? Cache-Subsystem aktivieren.
- Task über Kanal isolieren.
- Wiederholungen begrenzen.
Kontrolle dynamisch via logmectl: Levels, Subsysteme, ohne Neustarts.
Nach der Behebung Details zurückfahren. Sonst führt erhöhtes globales Logging zu Überlastung.
spdlog betont Geschwindigkeit, Quill Asynchronität, logme Nachrichtenfluss-Kontrolle bei hoher Performance.
Wichtige Erkenntnisse:
- Logger sollten über Kanäle nach Tasks gruppieren, nicht nach Loggers.
- Event-Level-Deduplizierung reduziert Lärm.
- Subsysteme ermöglichen gezielte Laufzeit-Details.
- Bibliotheksintegration standardisiert Ausgabe.
- Dynamische Kontrolle ohne Neustarts ist für Produktion essenziell.
— Editorial Team
Noch keine Kommentare.