Projektowanie loggera: skupienie na diagnostyce i kontroli w produkcji
W systemach produkcyjnych loggera ocenia się nie po szybkości API, ale po zdolności do szybkiego diagnozowania incydentów bez dodatkowego obciążenia. Logowanie niesie ze sobą overhead, dlatego w normalnym trybie je minimalizujemy. Jednak w przypadku problemów brak szczegółów lub ich chaotyczność uniemożliwia analizę. Zadaniem loggera jest zapewnienie diagnostyki bez uszczerbku dla wydajności.
Problem pojawia się, gdy struktura loga odzwierciedla kod, a nie procesy biznesowe. W systemach wielowątkowych komunikaty z różnych zadań mieszają się, utrudniając śledzenie konkretnego żądania.
Przywiązanie kontekstu do zadania
Rozwiązanie — kojarzyć logi z zadaniem, a nie z miejscem wywołania. Przekazywanie kontekstu przez parametry zanieczyszcza sygnatury. Lepiej przywiązać go do strumienia wykonania.
W logme to jest realizowane przez thread channel:
auto requestCh = Logme::Instance->CreateChannel(Logme::ID("req-42"));
LogmeThreadChannel(requestCh);
HandleRequest();
Wewnętrzny kod pisze w kanał automatycznie:
void ParseHeaders() {
LogmeW("invalid header");
}
Log jest teraz grupowany według operacji. W spdlog i Quill kontekst jest organizowany wokół loggera, co jest prostsze, ale mniej odpowiednie dla zadań.
Inny problem to szum z powtarzających się wiadomości. Błędy retry wypełniają log identycznymi liniami. Ograniczenie na poziomie zdarzenia rozwiązuje to:
LogmeW(LOGME_ONCE4THIS, "disk is full");
Wiadomość jest wyświetlana raz dla instancji klasy, zachowując czytelność.
Elastyczne rutowanie wiadomości
Globalna konfiguracja jest niewystarczająca. W HTTP/2 trzeba rozróżniać logi połączeń i żądań. Kanał żądania jest powiązany z kanałem protokołu, ale szczegóły żądania nie powinny zaśmiecać ogólnego loga.
Logme::Override ovr;
ovr.Add.DisableLink = true;
LogmeI(ovr, "request-specific detail");
To izoluje wiadomość w lokalnym kanale. Ogólne wydarzenia są widoczne wszędzie, prywatne — tylko lokalnie.
Podsystemy dla precyzyjnej szczegółowości
Jeden wymiar (kanał) to za mało. W zadaniu są podsystemy: cache, sieć, parsowanie. Logujemy wybiórczo:
LOGME_SUBSYSTEM(cacheSid, "cache");
LOGME_SUBSYSTEM(parserSid, "parser");
LogmeD(cacheSid, "cache miss");
LogmeD(parserSid, "header parsed");
W runtime włącza się tylko potrzebny podsystem, na przykład cache. Pozostałe milczą, log pozostaje kompaktowy.
Integracja z bibliotekami
Biblioteki stron trzecich mają własne logowanie. Przechwytujemy i integrujemy:
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);
}
Wiadomości podlegają tym samym regułom: kanały, podsystemy, de-duplikacja.
Wsparcie formatów ułatwia migrację:
LogmeI("value=%i", value);
fLogmeI("value={}", value);
LogmeI() << "value=" << value;
logme łączy fmt i stream-podejście, w przeciwieństwie do spdlog (fmt) i Quill (asynchroniczny fmt).
Zarządzanie w produkcji bez restartu
W ciągłych systemach logi minimalizuje się w normalnym trybie. Przy incydencie:
- Podejrzenie cache — włącza się podsystem cache.
- Izoluje zadanie przez channel.
- Ogranicza powtórzenia.
Zarządzanie dynamiczne przez logmectl: poziomy, podsystemy, bez restartu.
Po naprawie wyłącza się szczegółowość. Bez tego — globalne włączenie logów prowadzi do przeciążenia.
spdlog skupia się na szybkości, Quill — na asynchroniczności, logme — na kontroli strumienia wiadomości przy zachowaniu wydajności.
Co jest ważne:
- Logger powinien grupować według zadań przez kanały, a nie przez loggera.
- De-duplikacja na poziomie zdarzeń zapobiega szumowi.
- Podsystemy pozwalają na punktową szczegółowość w runtime.
- Integracja bibliotek unifikuje wyjście.
- Dynamiczne zarządzanie bez restartu jest kluczowe dla produkcji.
— Editorial Team
Brak komentarzy.