<?php /** * ---------------------------------------------------------------------- * * Debug logging during a client request. * So you can measure any action find bottlenecks in your code. * * Licence: GNU GPL 3.0 * Source: https://github.com/axelhahn/ahlogger * Docs: https://www.axel-hahn.de/docs/ahlogger/ * * USAGE:<br> * (1) Trigger a message with add() to add a marker<br> * (2) The render() method lists all items in a table with time since start * and the delta to the last message. <br> * * @author www.axel-hahn.de * * ---------------------------------------------------------------------- * 2016-02-26 init * 2016-11-19 add memory usage * (...) * 2022-09-25 add memory tracking, add cli renderer * 2022-09-27 css updates * 2022-10-02 add emoji chars * 2022-10-16 mark longest action with an icon * 2022-12-15 make it compatible to PHP 8.2; add doc + comments * 2023-05-15 fix _getBar() - division by zero * 2024-07-12 php8 only: use variable types; update phpdocs * 2024-09-04 fix short array syntax * ---------------------------------------------------------------------- */ class logger { /** * @var {array} array of added messages */ protected $aMessages = []; /** * @var {bool} flag: show debug infos? default: false */ protected $bShowDebug = false; /** * @var {int} memory usage on start */ protected $_iMemStart = false; /** * @var {string} dynamic prefix for used css - it is set in the cronstructor */ protected $sCssPrefix = ''; protected $sSourceUrl = 'https://github.com/axelhahn/ahlogger'; // ---------------------------------------------------------------------- // CONSTRUCTOR // ---------------------------------------------------------------------- /** * Constuctor * @param string $sInitMessage init message */ public function __construct(string $sInitMessage = "Logger was initialized.") { $this->_iMemStart = memory_get_usage(); $this->enableDebug(true); $this->add($sInitMessage); $this->sCssPrefix = 'debug-' . md5(microtime(true)); } // ---------------------------------------------------------------------- // PUBLIC METHODS // ---------------------------------------------------------------------- /** * Add a logging message * @param string $sMessage * @param string $sLevel * @return boolean */ public function add(string $sMessage, string $sLevel = "info"): bool { if (!$this->bShowDebug) { return false; } $this->aMessages[] = [ 'time' => microtime(true), 'message' => $sMessage, 'level' => preg_replace('/[^a-z0-9\-\_]/', '', $sLevel), 'memory' => memory_get_usage() ]; return true; } /** * Enable / disable debugging * @param bool $bEnable * @return bool */ public function enableDebug(bool $bEnable = true): bool { return $this->bShowDebug = !!$bEnable; } /** * Enable client debugging by a given array of allowed ip addresses * @param array $aIpArray list of ip addresses in a flat array * @return boolean */ public function enableDebugByIp(array $aIpArray): bool { $this->enableDebug(false); if (!$_SERVER || !is_array($_SERVER) || !array_key_exists("REMOTE_ADDR", $_SERVER)) { return false; } if (array_search($_SERVER['REMOTE_ADDR'], $aIpArray) !== false) { $this->enableDebug(true); } return true; } /** * Helper function: prepare array of added massages before output * - detect warnings and errors * - detect needed time for each action * - detect longest action * - detect maximum of memory usage * - calculate total time * * @return array */ protected function _prepareRendering(): array { $iMem = memory_get_usage(); $this->add('<hr>'); $this->add('Memory on start: ' . number_format($this->_iMemStart, 0, '.', ',') . " bytes"); $this->add('Memory on end: ' . number_format($iMem, 0, '.', ',') . " bytes"); $this->add('Memory peak: ' . number_format(memory_get_peak_usage(), 0, '.', ',') . " bytes"); $aReturn = [ 'totaltime' => false, 'level' => false, 'warnings' => '', 'errors' => '', 'maxrowid' => false, 'maxtime' => false, 'result' => [] ]; $sStarttime = $this->aMessages[0]["time"]; $iLasttime = $sStarttime; $iCounter = 0; $sMaxRowId = false; $iMaxtime = -1; $iMaxmem = -1; $bHasWarning = false; $bHasError = false; foreach ($this->aMessages as $aLogentry) { $iCounter++; if ($aLogentry["level"] == "warning") { $bHasWarning = true; } if ($aLogentry["level"] == "error") { $bHasError = true; } $sTrId = $this->sCssPrefix . 'debugTableRow' . $iCounter; $iDelta = $aLogentry["time"] - $iLasttime; if ($iDelta > $iMaxtime) { $iMaxtime = $iDelta; $sMaxRowId = $sTrId; } $iMaxmem = max($aLogentry["memory"], $iMaxmem); if (($iDelta > 1) || $aLogentry["level"] == "warning") { $aReturn['warnings'] .= '<a href="#' . $sTrId . '" title="' . sprintf("%01.4f", $iDelta) . ' s">' . $iCounter . '</a> '; } if ($aLogentry["level"] == "error") { $aReturn['errors'] .= '<a href="#' . $sTrId . '" title="' . sprintf("%01.4f", $iDelta) . ' s">' . $iCounter . '</a> '; } $aReturn['entries'][] = [ 'time' => $aLogentry["time"], 'level' => $aLogentry["level"], 'message' => $aLogentry["message"], 'memory' => sprintf("%01.2f", $aLogentry["memory"] / 1024 / 1024), // MB 'trid' => $sTrId, 'trclass' => $aLogentry["level"], 'counter' => $iCounter, 'timer' => sprintf("%01.3f", $aLogentry["time"] - $sStarttime), 'delta' => sprintf("%01.0f", $iDelta * 1000), ]; $iLasttime = $aLogentry["time"]; } $aReturn['level'] = ($bHasWarning ? ($bHasError ? 'error' : 'warning') : '' ); $aReturn['maxrowid'] = $sMaxRowId; $aReturn['maxtime'] = sprintf("%01.3f", $iMaxtime); $aReturn['maxmem'] = sprintf("%01.2f", $iMaxmem / 1024 / 1024); $aReturn['totaltime'] = sprintf("%01.3f", $aLogentry['time'] - $aReturn['entries'][0]['time']); return $aReturn; } /** * Get html code for a progressbar with divs * @param int|float $iVal value between 0..max value * @param int|float $iMax max value * @return string */ protected function _getBar(int|float $iVal, int|float $iMax): string { return $iMax > 0 ? '<div class="bar"><div class="progress" style="width: ' . ($iVal / $iMax * 100) . '%;"> </div></div>' : '' ; } /** * Render output of all logging messages * @return string */ public function render(): string { if (!$this->bShowDebug) { return false; } $aData = $this->_prepareRendering(); /* Array ( [totaltime] => 0.006 [errors] => [warnings] => 3 [maxrowid] => debugTableRow3 [maxtime] => 0.005 [result] => Array ( ) [entries] => Array mit Elementen Array ( [time] => 1663959608.2566 [level] => info [message] => Logger was initialized. [memory] => 538056 [trid] => debugTableRow1 [trclass] => info [trstyle] => [counter] => 1 [timer] => 0.000 [delta] => 0.000 ) */ $sOut = ''; // echo '<pre>'; print_r($aData); die(); foreach ($aData['entries'] as $aLogentry) { $sOut .= '<tr class="' . $this->sCssPrefix . '-level-' . $aLogentry["level"] . '' . ($aLogentry["trid"] == $aData["maxrowid"] ? ' ' . $this->sCssPrefix . '-maxrow' : '') . '" ' . 'id="' . $aLogentry["trid"] . '">' . '<td align="right">' . $aLogentry["counter"] . '</td>' . '<td>' . $aLogentry["level"] . '</td>' . '<td align="right">' . $aLogentry["timer"] . '</td>' . '<td align="right">' . $this->_getBar($aLogentry["delta"], $aData["maxtime"] * 1000) . ($aLogentry["delta"] == $aData['maxtime'] * 1000 ? '⏱️ ' : '') . $aLogentry["delta"] . ' ms</td>' . '<td align="right">' . $this->_getBar($aLogentry["memory"], $aData["maxmem"]) . $aLogentry["memory"] . ' MB' . '</td>' . '<td>' . $aLogentry["message"] . '</td>' . '</tr>'; } if ($sOut) { $sOut = ' <style> .' . $this->sCssPrefix . '-info {position: fixed; top: 6em; right: 1em; background: rgba(230,240,255, 0.8); border: 2px solid rgba(0,0,0,0.2); border-radius: 0.3em; z-index: 99999;} .' . $this->sCssPrefix . '-info .loggerhead {background: rgba(0,0,0,0.4); color: #fff;padding: 0em 0.5em 0.2em; border-radius: 0.3em 0.3em 0 0; } .' . $this->sCssPrefix . '-info .loggercontent {padding: 0.5em; } .' . $this->sCssPrefix . '-info .loggercontent .total {font-size: 160%; color: rgba(0,0,0,0.5); margin: 0.3em 0; display: inline-block;} .' . $this->sCssPrefix . '-messages {margin: 5em 2em 2em;} .' . $this->sCssPrefix . '-messages>h3 {font-size: 150%; margin: 0 0 0.5em 0;} .' . $this->sCssPrefix . '-messages .bar {background: rgba(0,0,0,0.03); height: 1.4em; position: absolute; width: 6em; border-right: 1px solid rgba(0,0,0,0.2);} .' . $this->sCssPrefix . '-messages .progress {background: rgba(100,140,180,0.2); height: 1.4em; padding: 0; float: left;} .' . $this->sCssPrefix . '-messages table{background: #fff; color: #222;table-layout:fixed; border: 2px solid rgba(0,0,0,0.2); border-radius: 0.5em;} .' . $this->sCssPrefix . '-messages table th{background: none; color: #222; border-bottom: 2px solid rgba(0,0,0,0.4);} .' . $this->sCssPrefix . '-messages table th.barcol{min-width: 7em; position: relative;} .' . $this->sCssPrefix . '-messages table td{padding: 3px; vertical-align: top;} .' . $this->sCssPrefix . '-messages table th:hover{background:#aaa !important;} .' . $this->sCssPrefix . '-level-info{background: #f0f4f4; color:#124} .' . $this->sCssPrefix . '-level-warning{background: #fcf8e3; color: #980;} .' . $this->sCssPrefix . '-level-error{background: #fce0e0; color: #944;} .' . $this->sCssPrefix . '-maxrow{color:#f33; font-weight: bold;} </style> <div class="' . $this->sCssPrefix . ' ' . $this->sCssPrefix . '-info ' . $this->sCssPrefix . '-level-' . $aData['level'] . '" onclick="location.href=\'#' . $this->sCssPrefix . '-messages\';"> <div class="loggerhead">ahLogger</div> <div class="loggercontent"> <span class="total">⏱️ ' . $aData['totaltime'] . ' s</span><br> 🪲 <a href="#' . $this->sCssPrefix . '-messages">Debug infos</a> | 🔺 <a href="#">top</a><br> <span>longest action: ⏱️ <a href="#' . $aData['maxrowid'] . '">' . ($aData['maxtime'] * 1000) . ' ms</a></span> ' . ($aData['errors'] ? '<br><span>‼️ Errors: ' . $aData['errors'] . '</span>' : '') . ' ' . ($aData['warnings'] ? '<br><span>⚠️ Warnings: ' . $aData['warnings'] . '</span>' : '') . ' </div> </div> <div id="' . $this->sCssPrefix . '-messages" class="' . $this->sCssPrefix . ' ' . $this->sCssPrefix . '-messages"> <h3>ahLogger 🪳 Debug messages</h3>' . ($aData['errors'] ? '<span>Errors: ' . $aData['errors'] . '</span><br>' : '') . ($aData['warnings'] ? '<span>Warnings: ' . $aData['warnings'] . '</span><br>' : '') . '<br> <table > <thead> <tr> <th>#</th> <th>level</th> <th>time [s]</th> <th class="barcol">delta</th> <th class="barcol">memory</th> <th>message</th> </tr></thead><tbody> ' . $sOut . '</tbody></table>' . '🌐 <a href="'.$this->sSourceUrl.'" target="_blank">'.$this->sSourceUrl.'</a>' ; } return $sOut; } /** * Render output of all logging messages for cli output * @return string */ public function renderCli(): string { if (!$this->bShowDebug) { return false; } $aData = $this->_prepareRendering(); $sOut = ''; foreach ($aData['entries'] as $aLogentry) { $sOut .= $aLogentry["timer"] . ' | ' . $aLogentry["delta"] . ' ms | ' . $aLogentry["level"] . ' | ' . (sprintf("%01.3f", $aLogentry["memory"] / 1024 / 1024)) . ' MB | ' . $aLogentry["message"] . ' ' . "\n" ; } $sOut .= "\nTotal time: " . $aData['totaltime'] . "\n"; return $sOut; } }