Design loggeru: zaměření na diagnostiku a kontrolu v produkci
V produkčních systémech se logger nehodnotí podle rychlosti API, ale podle schopnosti rychle diagnostikovat incidenty bez dodatečné zátěže. Logování nese overhead, proto se v normálním režimu minimalizuje. Avšak při problémech nedostatek detailů nebo jejich chaotičnost činí analýzu nemožnou. Úkolem loggeru je zajistit diagnostikovatelnost bez poškození výkonu.
Problém nastává, když struktura logu odráží kód, nikoli obchodní procesy. V vícevláknových systémech se zprávy z různých úkolů míchají, což ztěžuje sledování konkrétní požadavky.
Připojení kontextu k úkolu
Řešením je přidružit logy k úkolu, nikoli k místu volání. Přenos kontextu prostřednictvím parametrů znečišťuje signatury. Lepší je jej přidružit k toku provádění.
V logme je to implementováno přes thread channel:
auto requestCh = Logme::Instance->CreateChannel(Logme::ID("req-42"));
LogmeThreadChannel(requestCh);
HandleRequest();
Vnitřní kód zapisuje do kanálu automaticky:
void ParseHeaders() {
LogmeW("invalid header");
}
Log je nyní seskupen podle operací. V spdlog a Quill je kontext organizován kolem loggeru, což je jednodušší, ale méně vhodné pro úkoly.
Dalším problémem je šum z opakujících se zpráv. Chyby retry naplňují log stejnými řádky. Omezení na úrovni události to řeší:
LogmeW(LOGME_ONCE4THIS, "disk is full");
Zpráva se vypíše jednou pro instanci třídy, čímž se zachová čitelnost.
Flexibilní směrování zpráv
Globální konfigurace nestačí. V HTTP/2 je třeba rozlišovat logy spojení a požadavků. Kanál požadavku je propojen s kanálem protokolu, ale detaily požadavku by neměly znečišťovat obecný log.
Logme::Override ovr;
ovr.Add.DisableLink = true;
LogmeI(ovr, "request-specific detail");
Tím se zpráva izoluje v lokálním kanálu. Obecné události jsou viditelné všude, soukromé pouze lokálně.
Podoblasti pro přesnou detailizaci
Jedno měření (kanál) je málo. V úkolu jsou podoblasti: cache, síť, parsování. Logujeme výběrově:
LOGME_SUBSYSTEM(cacheSid, "cache");
LOGME_SUBSYSTEM(parserSid, "parser");
LogmeD(cacheSid, "cache miss");
LogmeD(parserSid, "header parsed");
V runtime zapínají pouze potřebnou podoblast, například cache. Zbytek mlčí, log zůstává kompaktní.
Integrace s knihovnami
Strany knihovny mají své vlastní logování. Zachytáváme a integrujeme:
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);
}
Zprávy podléhají stejným pravidlům: kanály, podoblasti, deduplikace.
Podpora formátů zjednodušuje migraci:
LogmeI("value=%i", value);
fLogmeI("value={}", value);
LogmeI() << "value=" << value;
logme kombinuje fmt a streamový přístup, na rozdíl od spdlog (fmt) a Quill (asynchronní fmt).
Správa v produkci bez restartu
V nepřetržitých systémech se logy minimalizují v normálním režimu. Při incidentu:
- Podezření na cache – zapínají podoblast cache.
- Izolují úkol prostřednictvím channel.
- Omezují opakování.
Správa je dynamická prostřednictvím logmectl: úrovně, podoblasti, bez restartu.
Po opravě vypínají detailizaci. Bez toho – globální zapnutí logů vede k přetížení.
spdlog se zaměřuje na rychlost, Quill na asynchronicitu, logme na kontrolu toku zpráv při zachování výkonu.
Co je důležité:
- Logger by měl seskupovat podle úkolů přes kanály, nikoli podle loggerů.
- Deduplikace na úrovni událostí zabraňuje šumu.
- Podoblasti umožňují bodovou detailizaci v runtime.
- Integrace knihoven sjednocuje výstup.
- Dynamická správa bez restartu je kritická pro produkci.
— Editorial Team
Zatím žádné komentáře.