2.7. Formátovače
Jak už víme z dřívějška, handlery používají formátovače, aby pro ně vhodně vyfiltrovaly a následně zformátovaly výslednou podobu logované informace. V předchozích ukázkách již byly uvedeny některé z možností formátování a v této části budou probrány důkladněji.
Důležité je uvědomit si, že je možné filtrovat (tj. vybírat jen určité položky z celého logovacího záznamu) a formátovat pouze existující obsah logovacího záznamu, tzn. pouze ty informace, které jsou v něm dostupné.
Jak již víme, každý handler má v sobě i formátovač. Zřídkakdy nám však vyhoví jeho výchozí nastavení, takže používáme jeden z dále uvedených předpřipravených vzorů. Těch existuje, stejně jako u handlerů, značné množství. Jedná se o 15 formátovačů, ale, opět stejně jako u handlerů, je mnoho z nich určeno jen pro speciální použití v konkrétních systémech třetích stran.
2.7.1. Co lze formátovat
Jak již bylo řečeno, jsou to výhradně jednotlivé položky logovacího záznamu, které se adresují jako %polozka%
. Připomeňme si jejich konkrétní jména:
%level% %level_name%
%channel% %datetime% %message% %context% %extra%
Filtrování znamená, že některé z těchto položek vůbec do formátu výstupu neuvedeme, a tak se ve výstupu nakonec neobjeví. Typicky se např. vynechává %level%
, což je málo vypovídající celé číslo.
Formátování znamená, že pořadí explicitně uvedených položek můžeme libovolně míchat a položky doplňovat / oddělovat dalšími znaky apod.
Vlastní obsah položek se vypisuje tak, jak je uveden v logovacím záznamu, s jedinou výjimkou. Jediná položka, kterou lze vnitřně dále konfigurovat, je %datetime%
Její výchozí formát je Y-m-d\TH:i:sP
, což dává např.:
2021-09-06T21:15:03.047516+02:00
Pro naše vlastní formátování se použijí vzory („formátovací řetězce“) z dokumentace DateTime::format.
např.: Y-m-d H:i:s
, což dává např. běžně užívaný formát data a času:
2021-09-06 21:15:03
Tento formátovací řetězec se pak zadává jako volitelný parametr u různých formátovačů.
2.7.2. LineFormatter
Je to nejčastěji používaný formátovač, který zajistí formátování údajů do textové řádky.
Dovoluje nastavit libovolnou kombinaci položek z logovacího záznamu a použít vlastní formát pro %datetime%
.
Jeho typické použití je:
$dateFormat = "Y-m-d H:i:s"; $zaznam = "%datetime% > %channel% > %level_name% \n"; $formatter = new LineFormatter($zaznam, $dateFormat); $handler->setFormatter($formatter);
Což vypíše např.:
2021-04-30 15:44:22 > soubor > INFO
2.7.3. HtmlFormatter
Chceme-li co s nejmenším úsilím vypisovat logovací záznamy v HTML formátu postupně do tabulky, použijeme tento formátovač.
Bohužel, jednoduchost jeho použití je často velmi omezující, protože nedovoluje filtrovat ani upravovat formátování. To znamená, že se nedá vůbec ovlivnit pořadí a množství vypisovaných položek. Jediné, co lze ovlivnit, je jen formát %datetime%.
Jeho typické použití je:
$dateFormat = "Y-m-d H:i:s"; $handler->setFormatter(new HtmlFormatter($dateFormat)); $logger->info('zpráva log-info'); $logger->error('zpráva log-error'); $logger->alert('zpráva log-alert');
Ve výsledném HTML kódu je pouze tabulka, chybí zde hlavička HTML dokumentu, proto je v příkladu kódování zobrazeno chybně. Zobrazí se:
2.7.4. JsonFormatter
Potřebujeme-li zapsat celý logovací záznam do JSON formátu, použijeme tento formátovač. Nedává ale žádné možnosti konfigurace, protože není možné změnit ani pořadí ani počet položek a dokonce ani zadat vlastní formát pro %datetime%.
Jeho typické použití je:
$jmenoSouboru = __DIR__ . '\..\..\..\logy\souborJsonFormatter.log'; $logger = new Logger('soubor'); $handler = new StreamHandler($jmenoSouboru, Logger::INFO); $handler->setFormatter(new JsonFormatter()); $logger->pushHandler($handler); $logger->info('zpráva log-info');
Obsah souboru souborJsonFormatter.log
(je provedeno odřádkování pro lepší čitelnost):
{"message":"zpráva log-info","context":{},"level":200, "level_name":"INFO","channel":"soubor", "datetime":"2021-09-09T16:54:33.824112+02:00","extra":{}}
3. Rozsáhlejší příklad
Na závěr je uveden syntetický příklad, který ukazuje různé kombinace dříve uvedených dovedností.
Poznámka:
Účelem příkladu je ukázat možnosti logování, nikoliv možnosti či způsoby psaní PHP aplikací.
V příkladu jsou použity dva loggery zapisující do jednoho výstupního souboru priklad.log
. Přičemž:
$logHlavni
zachycuje všechny „hlavní“ události běhu aplikace a jeho význam by byl zejména při provozu aplikace$logSoubor
zachycuje jen události při zpracování souboru a hodil by se ve fázi ladění. Ukazuje:- použití dvou handlerů – do konzole a do souboru
priklad.log
- využití
IntrospectionProcessor
- využití doplňkové informace v context při výpisu na konzoli
- použití dvou handlerů – do konzole a do souboru
Příklad by měl mj. demonstrovat skutečnost, že vyladění úrovní logování, volba výstupních souborů a množství zapisované informace není triviální záležitost. Vyžaduje to prvotní rozvržení a pak následné experimenty. Logovacích informací by totiž nemělo být ani moc, ani málo a měly by být relevantní – proto se např. do konzole nevypisuje časová známka.
Popis ovládání aplikace:
Zadá se jméno souboru (implicitní přípona je .txt
) následované dvěma parametry najdi
a nahrad
. V tomto souboru nalezne všechny řetězce s hodnotou najdi
a nahradí je hodnotou parametru nahrad
. Výsledek zapíše do souboru .out
. Okamžitě po provedení záměny je možné stejnou záměnu provádět v jiném souboru, protože parametry najdi
a nahrad
zůstávají vyplněné i pro další případné kolo. Pokud chceme vyhledávat a/nebo nahrazovat jiné řetězce, je třeba tyto parametry příslušně nastavit.
Soubor index.php
je zodpovědný za přípravu a konfiguraci loggerů (jsou předávány přes session) a za zpracování požadavku:
<?php use Monolog\Formatter\LineFormatter; use Monolog\Handler\StreamHandler; use Monolog\Logger; use Monolog\Handler\BrowserConsoleHandler; use Monolog\Processor\IntrospectionProcessor; use app\priklad\Funkce; require_once('Funkce.php'); require __DIR__ . '/../../../vendor/autoload.php'; session_start(); if (count($_POST) == 0) { nastavLogovani(); (new Funkce())->vypisHtml(); } else { zpracujPozadavek(); } function nastavLogovani() { $logHlavni = new Logger('hlavni'); $logSoubor = new Logger('soubor'); $dateFormat = "Y-m-d H:i:s"; $handStream = new StreamHandler("priklad.log", Logger::DEBUG); $zaznamStream = "%datetime% %channel%.%level_name% %message% %extra% \n"; $formatterStream = new LineFormatter($zaznamStream, $dateFormat); $handStream->setFormatter($formatterStream); $handKonzole = new BrowserConsoleHandler(Logger::INFO); $zaznamKonzole = "%channel%.%level_name% %message% souhrn=%context%"; $formatterKonzole = new LineFormatter($zaznamKonzole); $handKonzole->setFormatter($formatterKonzole); $logSoubor->pushProcessor(new IntrospectionProcessor()); $logHlavni->pushHandler($handStream); $logHlavni->pushHandler($handKonzole); $logSoubor->pushHandler($handStream); $_SESSION["HLAVNI"] = $logHlavni; $_SESSION["SOUBOR"] = $logSoubor; } function zpracujPozadavek() { $logHlavni = $_SESSION["HLAVNI"]; $logHlavni->notice("Zacatek zpracovani"); $soubor = $_POST["soubor"]; $najdi = $_POST["najdi"]; $nahrad = $_POST["nahrad"]; $funkce = new Funkce(); $vysledek = $funkce->overParametry($soubor, $najdi, $nahrad); $pocet = 0; if ($vysledek == true) { $pocet = $funkce->provedZamenu($soubor, $najdi, $nahrad); } $souhrn = array("soubor"=>$soubor, "najdi"=>$najdi, "nahrad"=>$nahrad, "pocet"=>$pocet); $logHlavni->notice("Konec zpracovani", $souhrn); $funkce->vypisHtml($soubor, $najdi, $nahrad, $pocet); }
Třída Funkce
je zodpovědná za kontrolu parametrů a za náhradu řetězců v daném souboru. Používá oba dva loggery. Pro frontend (metoda vypisHtml()
) je z důvodů jednoduchosti použit velmi primitivní návrh.
<?php namespace app\priklad; class Funkce { private $logHlavni; private $logSoubor; public function __construct() { $this->logHlavni = $_SESSION["HLAVNI"]; $this->logSoubor = $_SESSION["SOUBOR"]; } public function vypisHtml(string $soubor="", string $najdi="", string $nahrad="", int $pocet=-1) : void { ?> <!doctype html> <html> <head> <meta charset="utf-8"> <title>Ukázka logování</title> </head> <body> <form action="index.php" method="post" autocomplete="off"> <label for="soubor">Jméno souboru bez přípony:</label> <input id="soubor" type="text" name="soubor"> <br> <label for="najdi">Najdi:</label> <input id="najdi" type="text" name="najdi" value="<?=$najdi?>"> <br> <label for="nahrad">Nahrad:</label> <input id="nahrad" type="text" name="nahrad" value="<?=$nahrad?>"> <br> <input type="submit" value="Proveď nahrazení"> </form> <?php if ($pocet >= 0) { echo "Počet nahrazených řádek v souboru <code>".$soubor.".out</code> = ".$pocet; } ?> </body> </html> <?php } public function overParametry(string $soubor, string $najdi, string $nahrad) : bool { $vysledek = true; $vysledek = $vysledek && $this->hodnotaParametru("soubor", $soubor); $vysledek = $vysledek && $this->hodnotaParametru("najdi", $najdi); $vysledek = $vysledek && $this->hodnotaParametru("nahrad", $nahrad); return $vysledek; } private function hodnotaParametru(string $jmeno, string $hodnota) : bool { if (strlen($hodnota) > 0) { $this->logHlavni->info("Parametr: " . $jmeno ."=". $hodnota); return true; } else { $this->logHlavni->warning("Parametr: " . $jmeno ." nenastaven"); return false; } } public function provedZamenu(string $soubor, string $najdi, string $nahrad) : int { $jmenoIn = $soubor . ".txt"; $jmenoOut = $soubor . ".out"; $radky = $this->nactiSoubor($jmenoIn); if (count($radky) == 0) { $this->logHlavni->warning("Vstupni soubor:" . $jmenoIn . " neexistuje nebo je prazdny"); return 0; } $zameneno = array(); $pocet = 0; foreach ($radky as $radka) { $this->logSoubor->debug("Stara:'".$radka."'"); $nova = str_replace($najdi, $nahrad, $radka); $this->logSoubor->debug("Nova :'".$nova."'"); if ($nova != $radka) { $pocet++; $this->logSoubor->info("Aktualni pocet nahrazeni:".$pocet); } array_push($zameneno, $nova."\n"); } file_put_contents($jmenoOut, $zameneno); $this->logHlavni->notice("Vystupni soubor:".$jmenoOut); $this->logHlavni->info("Celkem zamenenych radek:".$pocet); return $pocet; } private function nactiSoubor(string $jmeno) : array { if (file_exists($jmeno) == true) { $this->logSoubor->info("Vstupni soubor:".$jmeno. " existuje"); } else { $this->logSoubor->warning("Vstupni soubor:".$jmeno. " neexistuje"); return []; } $obsah = file_get_contents($jmeno); $radky = explode("\n", $obsah); $this->logSoubor->info("Celkem radek:".count($radky)); return $radky; } }
Po spuštění index.php
se zobrazí:
Nastavení správných parametrů:
Provedení náhrad:
Obsah souboru vstup.txt
první řádka bez chyby druhá řádka ERROR s chybou třetí řádka bez chyby čtvrtá řádka ERROR s dvojnásobnou chybou ERROR pátá řádka bez chyby šestá řádka s chybou ERROR
Výpis v konzoli – poslední log ukazuje využití context pojmenovaného souhrn
Obsah souboru priklad.log
(dlouhé řádky jsou zalomené)
2021-09-09 17:24:43 hlavni.NOTICE Zacatek zpracovani [] 2021-09-09 17:24:43 hlavni.INFO Parametr: soubor=vstup [] 2021-09-09 17:24:43 hlavni.INFO Parametr: najdi=ERROR [] 2021-09-09 17:24:43 hlavni.INFO Parametr: nahrad=AbC [] 2021-09-09 17:24:43 soubor.INFO Vstupni soubor:vstup.txt existuje {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":106,"class":"app\\priklad\\Funkce","function":"nactiSoubor"} 2021-09-09 17:24:43 soubor.INFO Celkem radek:7 {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":113,"class":"app\\priklad\\Funkce","function":"nactiSoubor"} 2021-09-09 17:24:43 soubor.DEBUG Stara:'první řádka bez chyby' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":87,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Nova :'první řádka bez chyby' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":89,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Stara:'druhá řádka ERROR s chybou' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":87,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Nova :'druhá řádka AbC s chybou' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":89,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.INFO Aktualni pocet nahrazeni:1 {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":92,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Stara:'třetí řádka bez chyby' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":87,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Nova :'třetí řádka bez chyby' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":89,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Stara:'čtvrtá řádka ERROR s dvojnásobnou chybou ERROR' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":87,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Nova :'čtvrtá řádka AbC s dvojnásobnou chybou AbC' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":89,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.INFO Aktualni pocet nahrazeni:2 {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":92,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Stara:'pátá řádka bez chyby' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":87,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Nova :'pátá řádka bez chyby' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":89,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Stara:'šestá řádka s chybou ERROR' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":87,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Nova :'šestá řádka s chybou AbC' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":89,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.INFO Aktualni pocet nahrazeni:3 {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":92,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Stara:'' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":87,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 soubor.DEBUG Nova :'' {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":89,"class":"app\\priklad\\Funkce","function":"provedZamenu"} 2021-09-09 17:24:43 hlavni.NOTICE Vystupni soubor:vstup.out [] 2021-09-09 17:24:43 hlavni.INFO Celkem zamenenych radek:3 [] 2021-09-09 17:24:43 hlavni.NOTICE Konec zpracovani []
Nenastavení parametru Jméno souboru
– výsledek:
Výpis v konzoli:
Pokračující obsah souboru priklad.log
:
2021-09-09 17:27:11 hlavni.NOTICE Zacatek zpracovani [] 2021-09-09 17:27:11 hlavni.WARNING Parametr: soubor nenastaven [] 2021-09-09 17:27:11 hlavni.NOTICE Konec zpracovani []
Nastavení parametru Jméno souboru
na jméno neexistujícího souboru blbost.txt
Výpis v konzoli:
Pokračující obsah souboru priklad.log
:
2021-09-09 17:30:52 hlavni.NOTICE Zacatek zpracovani [] 2021-09-09 17:30:52 hlavni.INFO Parametr: soubor=blbost [] 2021-09-09 17:30:52 hlavni.INFO Parametr: najdi=ERROR [] 2021-09-09 17:30:52 hlavni.INFO Parametr: nahrad=AbC [] 2021-09-09 17:30:52 soubor.WARNING Vstupni soubor:blbost.txt neexistuje {"file":"C:\\xampp\\htdocs\\logovani\\src\\app\\priklad\\Funkce.php","line":108,"class":"app\\priklad\\Funkce","function":"nactiSoubor"} 2021-09-09 17:30:52 hlavni.WARNING Vstupni soubor:blbost.txt neexistuje nebo je prazdny [] 2021-09-09 17:30:52 hlavni.NOTICE Konec zpracovani []
Závěr
Toto bylo stručné seznámení s možnostmi logování. Máte-li zkušenosti s jinými knihovnami nebo s jinými postupy, prosím, napište je do diskuze.