diff --git a/app/common/Controller/Logs.php b/app/common/Controller/Logs.php index 988615d..f6494bd 100644 --- a/app/common/Controller/Logs.php +++ b/app/common/Controller/Logs.php @@ -5,7 +5,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\Views\Blade as View; use ProVM\Common\Service\Logs as Service; -use ProVM\Logview\Log; +use function Safe\json_encode; class Logs { @@ -13,16 +13,9 @@ class Logs { $log = $service->get($log_file); - $levels = []; - foreach (Log::LEVELS as $level) { - $levels[strtolower($level)] = (object) [ - 'text' => Log::COLORS[$level], - 'background' => Log::BACKGROUNDS[$level], - ]; - } - return $view->render($response, 'logs.show', compact('log', 'levels')); + return $view->render($response, 'logs.show', compact('log')); } - public function getMore(ServerRequestInterface $request, ResponseInterface $response, View $view, Service $service, string $log_file, int $start = 0, int $amount = 100): ResponseInterface + public function getMore(ServerRequestInterface $request, ResponseInterface $response, Service $service, string $log_file, int $start = 0, int $amount = 100): ResponseInterface { $log = $service->get($log_file); @@ -32,7 +25,7 @@ class Logs } $logs = array_reverse($logs); $total = $log->getTotal(); - $response->getBody()->write(\Safe\json_encode([ + $response->getBody()->write(json_encode([ 'total' => $total, 'logs' => $logs ])); diff --git a/app/common/Define/Log.php b/app/common/Define/Log.php index 5e59620..3bc1a65 100644 --- a/app/common/Define/Log.php +++ b/app/common/Define/Log.php @@ -1,7 +1,30 @@ = $offset) { + break; + } + } } } diff --git a/app/common/Service/Logs.php b/app/common/Service/Logs.php index 8e7c715..4a4b43c 100644 --- a/app/common/Service/Logs.php +++ b/app/common/Service/Logs.php @@ -3,11 +3,14 @@ namespace ProVM\Common\Service; use DateTimeImmutable; use SplFileInfo; +use FilesystemIterator; use Psr\Log\LoggerInterface; use ProVM\Logview\Log\File; use ProVM\Common\Define\Parser; use ProVM\Logview\Parser as Parsers; +use function Safe\{preg_match}; + class Logs { public function __construct(LoggerInterface $logger, string $folder) @@ -42,7 +45,7 @@ class Logs public function getFiles(): array { - $files = new \FilesystemIterator($this->getFolder()); + $files = new FilesystemIterator($this->getFolder()); $output = []; foreach ($files as $file) { if ($file->isDir()) { @@ -57,11 +60,12 @@ class Logs $map = [ Parsers\Access::class => '/(access.log)/', Parsers\Error::class => '/(error.log)/', + Parsers\Debug::class => '/(debug.log)/', Parsers\Monolog::class => '/(-\d{4}-\d{2}-\d{2}.log)/', - Parsers\PHPDefault::class => '/(php_errors.log)/' + Parsers\PHPDefault::class => '/(php_errors.log)/', ]; foreach ($map as $class => $regex) { - if (\Safe\preg_match($regex, $filename) === 1) { + if (preg_match($regex, $filename) === 1) { return new $class; } } diff --git a/app/resources/views/logs/show.blade.php b/app/resources/views/logs/show.blade.php index aedf07d..52ce08e 100644 --- a/app/resources/views/logs/show.blade.php +++ b/app/resources/views/logs/show.blade.php @@ -12,14 +12,60 @@ @push('page_styles') @endpush @push('page_scripts') diff --git a/app/src/Exception/Parse/EmptyException.php b/app/src/Exception/Parse/EmptyException.php new file mode 100644 index 0000000..dd6ddcb --- /dev/null +++ b/app/src/Exception/Parse/EmptyException.php @@ -0,0 +1,14 @@ +context) and $this->context !== ''; } - public function getColor(): string - { - return self::COLORS[strtoupper($this->getSeverity())]; - } - public function getBackgroundColor(): string - { - return self::BACKGROUNDS[strtoupper($this->getSeverity())]; - } - public function jsonSerialize(): mixed { return ($this->parsed()) ? [ @@ -147,26 +140,4 @@ class Log implements \ProVM\Common\Define\Log, \JsonSerializable 'EMERGENCY', 'DEPRECATED', ]; - const COLORS = [ - 'DEBUG' => '#000', - 'INFO' => '#fff', - 'NOTICE' => '#fff', - 'WARNING' => '#000', - 'ERROR' => '#fff', - 'CRITICAL' => '#fff', - 'ALERT' => '#fff', - 'EMERGENCY' => '#fff', - 'DEPRECATED' => '#fff', - ]; - const BACKGROUNDS = [ - 'DEBUG' => '#fff', - 'INFO' => '#00f', - 'NOTICE' => '#55f', - 'WARNING' => '#dd5', - 'ERROR' => '#555', - 'CRITICAL' => '#f00', - 'ALERT' => '#f55', - 'EMERGENCY' => '#f55', - 'DEPRECATED' => '#f50', - ]; } diff --git a/app/src/Log/File.php b/app/src/Log/File.php index 7e4e8d8..449b890 100644 --- a/app/src/Log/File.php +++ b/app/src/Log/File.php @@ -3,9 +3,13 @@ namespace ProVM\Logview\Log; use Generator; use DateTimeInterface; +use ProVM\Logview\Exception\Parse\EmptyException; +use ProVM\Logview\Exception\Parse\EmptyLineException; use Psr\Log\LoggerInterface; use ProVM\Common\Define\Parser; +use function Safe\{fopen, fclose}; + class File { protected LoggerInterface $logger; @@ -77,26 +81,24 @@ class File $i = 0; } - $cnt = 1; - $fh = \Safe\fopen($this->getFullname(), 'r'); + $debug = compact('i', 'f'); + $cnt = $i; + $fh = fopen($this->getFullname(), 'r'); + $debug []= ftell($fh); + $this->getParser()->advance($fh, $i); + $debug []= ftell($fh); + \Safe\error_log(var_export($debug,true).PHP_EOL,3,'/logs/debug'); while (!feof($fh)) { - $line = fgets($fh); - if ($cnt < $i) { - $cnt ++; + try { + yield $this->getParser()->parse($fh); + } catch (EmptyException | EmptyLineException $e) { continue; } - if (!$line) { - continue; - } - if (trim($line) === '') { - continue; - } - yield $this->getParser()->parse(trim($line)); $cnt ++; if ($cnt > $f) { break; } } - \Safe\fclose($fh); + fclose($fh); } } diff --git a/app/src/Parser/Access.php b/app/src/Parser/Access.php index 6c8c057..9914c34 100644 --- a/app/src/Parser/Access.php +++ b/app/src/Parser/Access.php @@ -1,18 +1,25 @@ getOriginal(); $regex = "/(?\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[(?\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2} [\+|\-]\d{4})\] (?.*)/"; - preg_match($regex, $content, $matches); + try { + preg_match($regex, $content, $matches); + } catch (Exception $e) { + return $log; + } try { $log->setDate(DateTimeImmutable::createFromFormat('d/M/Y:H:i:s P', $matches['date'])); } catch (DatetimeException $e) { diff --git a/app/src/Parser/Debug.php b/app/src/Parser/Debug.php new file mode 100644 index 0000000..8330b50 --- /dev/null +++ b/app/src/Parser/Debug.php @@ -0,0 +1,104 @@ +getOriginal()]; + $this->parseError($log, $content[0]); + while(!feof($file_handler)) { + $line_number = ftell($file_handler); + $line = fgets($file_handler); + if (!$line or trim($line) === '') { + continue; + } + if (str_contains($line, 'Error')) { + fseek($file_handler, $line_number); + break; + } + $content []= $line; + if (str_starts_with($line, '#')) { + $this->parseStack($log, $line); + continue; + } + $this->parseLine($log, $line); + } + $log->setOriginal(implode(PHP_EOL, $content)); + return $log; + } + + public function advance(mixed &$file_handler, int $offset): void + { + if ($offset === 0) { + return; + } + $cnt = 0; + while(!feof($file_handler)) { + $line = fgets($file_handler); + if (str_starts_with($line, 'Error')) { + $cnt ++; + } + if ($cnt >= $offset) { + break; + } + } + } + + protected function parseError(Log &$log, string $line): void + { + $regex = '/(?Error): (?\w|\s|\"|\\*)(?\/\w\.*):(?\d*)/'; + try { + preg_match($regex, $line, $matches); + } catch (Exception $e) { + return; + } + $log->setSeverity($matches['level']); + $log->setChannel($matches['level']); + $log->setMessage("{$matches['message']} {$matches['filename']}:{$matches['line']}"); + $log->setContext("{$matches['filename']} ({$matches['line']})"); + } + protected function parseStack(Log &$log, string $line): void + { + $stack = $log->getStack(); + $stack []= $line; + $log->setStack($stack); + } + protected function parseLine(Log &$log, string $line): void + { + $extra = explode(PHP_EOL, $log->getExtra()) ?? []; + $extra []= $line; + $log->setExtra(implode(PHP_EOL, $extra)); + } +} diff --git a/app/src/Parser/Monolog.php b/app/src/Parser/Monolog.php index c35d2ca..9f0a9ab 100644 --- a/app/src/Parser/Monolog.php +++ b/app/src/Parser/Monolog.php @@ -1,32 +1,57 @@ total($filename) !== $this->getLines($filename)) { + return true; + } + return false; + } + public function total(string $filename): int { + if ($this->isMultiline($filename)) { + return (new Monolog\Multiline())->total($filename); + } try { $regex = "/\[(?P\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}(?:\+|-)\d{2}:\d{2})\]/"; - $fh = \Safe\fopen($filename, 'r'); + $fh = fopen($filename, 'r'); $sum = 0; while(!feof($fh)) { $line = fgets($fh); - $sum += \Safe\preg_match_all($regex, $line); + $sum += preg_match_all($regex, $line); } fclose($fh); return $sum; - } catch (\Exception $e) { - \Safe\error_log($e . PHP_EOL, 3, '/logs/total.log'); + } catch (Exception $e) { return 0; } } - public function parse(string $content): Log + public function parse(mixed &$file_handler): Log { - $log = parent::parse($content); + $reset_line = ftell($file_handler); + $log = parent::parse($file_handler); + $content = $log->getOriginal(); + + $line_number = ftell($file_handler); + $line = fgets($file_handler); + fseek($file_handler, $line_number); + if (str_starts_with($line, 'Stack trace')) { + fseek($file_handler, $reset_line); + return (new Monolog\Multiline())->parse($file_handler); + } $regex = [ "\[(?P\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}(?:\+|-)\d{2}:\d{2})\]", @@ -38,18 +63,16 @@ class Monolog extends Parser ]; $regex = implode('', $regex); try { - \Safe\preg_match("/{$regex}/", $content, $matches); - } catch (\Exception $e) { - \Safe\error_log($content . PHP_EOL, 3, '/logs/debug.log'); - \Safe\error_log($e . PHP_EOL, 3, '/logs/debug.log'); - return $log; + preg_match("/{$regex}/", $content, $matches); + } catch (Exception $e) { + return (new Monolog\Multiline())->parse($file_handler); } try { $extra = []; try { $log->setDate(DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.uP', $matches['date'])); - } catch (\Exception $e) { + } catch (Exception $e) { $log->setDate(new DateTimeImmutable()); $extra['date'] = $matches['date']; } @@ -72,13 +95,34 @@ class Monolog extends Parser $extra['extra'] = "{{$matches['extra']}}"; } if (count($extra) > 0) { - $log->setExtra(\Safe\json_encode($extra, JSON_UNESCAPED_SLASHES)); + $log->setExtra(json_encode($extra, JSON_UNESCAPED_SLASHES)); } - } catch (\Error $e) { - \Safe\error_log($e . PHP_EOL, 3, '/logs/debug.log'); - \Safe\error_log(var_export($matches, true) . PHP_EOL, 3, '/logs/debug.log'); + } catch (Error $e) { + error_log($e . PHP_EOL, 3, '/logs/debug.log'); + error_log(var_export($matches, true) . PHP_EOL, 3, '/logs/debug.log'); } return $log; } + + public function advance(mixed &$file_handler, int $offset): void + { + (new Monolog\Multiline())->advance($file_handler, $offset); + } + + protected int $lines; + protected function getLines(string $filename): int + { + if (!isset($this->lines)) { + $cnt = 0; + $fh = fopen($filename, 'r'); + while(!feof($fh)) { + fgets($fh); + $cnt ++; + } + fclose($fh); + $this->lines = $cnt; + } + return $this->lines; + } } diff --git a/app/src/Parser/Monolog/Multiline.php b/app/src/Parser/Monolog/Multiline.php new file mode 100644 index 0000000..2026f36 --- /dev/null +++ b/app/src/Parser/Monolog/Multiline.php @@ -0,0 +1,145 @@ +getOriginal()]; + $this->parseError($log, $content[0]); + while(!feof($file_handler)) { + $line_number = ftell($file_handler); + $line = fgets($file_handler); + if (!$line or trim($line) === '') { + continue; + } + if (str_starts_with($line, '[')) { + fseek($file_handler, $line_number); + break; + } + $content []= $line; + if (str_starts_with($line, 'Stack trace')) { + $log->setStack([$line]); + continue; + } + if (str_starts_with($line, '#')) { + $this->parseStack($log, $line); + continue; + } + $this->parseLine($log, $line); + } + $log->setOriginal(implode(PHP_EOL, $content)); + return $log; + } + + public function advance(mixed &$file_handler, int $offset): void + { + if ($offset === 0) { + return; + } + $cnt = 0; + while(!feof($file_handler)) { + if ($cnt >= $offset) { + break; + } + $line = fgets($file_handler); + if (str_starts_with($line, '[')) { + $cnt ++; + } + } + } + + protected function parseError(Log &$log, string $line): void + { + $regex = [ + "\[(?P\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}(?:\+|-)\d{2}:\d{2})\]", + "\s(?\w*)", + "\.(?\w*)", + ":\s(?.*)", + ]; + $regex = implode('', $regex); + try { + preg_match("/{$regex}/", $line, $matches); + } catch (Exception $e) { + return; + } + try { + $extra = []; + try { + $log->setDate(DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.uP', $matches['date'])); + } catch (Exception $e) { + $log->setDate(new DateTimeImmutable()); + $extra['date'] = $matches['date']; + } + $log->setChannel($matches['channel']); + $log->setSeverity($matches['severity']); + $message = $matches['message']; + if (str_contains($message, 'Stack trace')) { + list($msg, $data) = explode('Stack trace:', $message); + $message = trim($msg); + $regex = '/\s#\d+\s/'; + $lines = preg_split($regex, $data); + array_shift($lines); + $log->setStack($lines); + } + $log->setMessage($message); + if ($matches['context'] !== '') { + $log->setContext("{{$matches['context']}}"); + } + if (isset($matches['extra']) and $matches['extra'] !== '') { + $extra['extra'] = "{{$matches['extra']}}"; + } + if (count($extra) > 0) { + $log->setExtra(json_encode($extra, JSON_UNESCAPED_SLASHES)); + } + } catch (Error $e) { + error_log($e . PHP_EOL, 3, '/logs/debug.log'); + error_log(var_export($matches, true) . PHP_EOL, 3, '/logs/debug.log'); + } + } + protected function parseStack(Log &$log, string $line): void + { + $stack = $log->getStack(); + $stack []= $line; + $log->setStack($stack); + } + protected function parseLine(Log &$log, string $line): void + { + $extra = explode(PHP_EOL, $log->getExtra()) ?? []; + $extra []= $line; + $log->setExtra(implode(PHP_EOL, $extra)); + } +} diff --git a/app/src/Parser/PHPDefault.php b/app/src/Parser/PHPDefault.php index 5a763f5..d4d1bf5 100644 --- a/app/src/Parser/PHPDefault.php +++ b/app/src/Parser/PHPDefault.php @@ -1,7 +1,10 @@ \d{2}-\w{3}-\d{4}\s\d{2}:\d{2}:\d{2}\s\w{3})\]/"; - $fh = \Safe\fopen($filename, 'r'); + $fh = fopen($filename, 'r'); $sum = 0; while(!feof($fh)) { $line = fgets($fh); - $sum += \Safe\preg_match_all($regex, $line); + $sum += preg_match_all($regex, $line); } fclose($fh); return $sum; - } catch (\Exception $e) { + } catch (Exception $e) { return 0; } } - public function parse(string $content): Log + public function parse(mixed &$file_handler): Log { - $log = parent::parse($content); + $log = parent::parse($file_handler); + $content = $log->getOriginal(); $regex = "/\[(?\d{2}-\w{3}-\d{4}\s\d{2}:\d{2}:\d{2}\s\w{3})\]\s(?PHP|User)\s(?\w+):\s(?.*)/"; try { - \Safe\preg_match($regex, $content, $matches); - } catch (\Error $e) { - \Safe\error_log($e . PHP_EOL, 3, '/logs/debug.log'); + preg_match($regex, $content, $matches); + } catch (Error $e) { + error_log($e . PHP_EOL, 3, '/logs/debug.log'); return $log; } $extra = []; try { $log->setDate(DateTimeImmutable::createFromFormat('d-M-Y H:i:s e', $matches['date'])); - } catch (\Exception $e) { + } catch (Exception $e) { $log->setDate(new DateTimeImmutable()); $extra['date'] = $matches['date']; } $log->setChannel(''); $log->setSeverity($matches['severity']); $log->setMessage($matches['message']); - $log->setContext(\Safe\json_encode(['level' => $matches['level']], JSON_UNESCAPED_SLASHES)); + $log->setContext(json_encode(['level' => $matches['level']], JSON_UNESCAPED_SLASHES)); if (count($extra) > 0) { - $log->setExtra(\Safe\json_encode($extra, JSON_UNESCAPED_SLASHES)); + $log->setExtra(json_encode($extra, JSON_UNESCAPED_SLASHES)); } return $log;