From c29eece81cd3100e499a654cbd98f8989943083a Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 15 Feb 2023 17:45:23 -0300 Subject: [PATCH] Reverse order and multiple log files handler --- app/common/Controller/Logs.php | 17 +++ app/common/Define/Log.php | 7 + app/common/Define/Parser.php | 7 + app/common/Implement/Parser.php | 27 ++++ app/common/Service/Logs.php | 24 +++- app/resources/routes/01_logs.php | 1 + app/resources/views/logs/show.blade.php | 178 +++++++++++++++++------- app/setup/setups/01_logs.php | 5 + app/src/Log.php | 72 ++++++---- app/src/Log/File.php | 74 +++++++--- app/src/Parser/Access.php | 30 ++++ app/src/Parser/Basic.php | 8 ++ app/src/Parser/Error.php | 8 ++ app/src/Parser/Monolog.php | 81 +++++++++++ app/src/Parser/PHPDefault.php | 54 +++++++ docker-compose.yml | 2 + 16 files changed, 494 insertions(+), 101 deletions(-) create mode 100644 app/common/Define/Log.php create mode 100644 app/common/Define/Parser.php create mode 100644 app/common/Implement/Parser.php create mode 100644 app/src/Parser/Access.php create mode 100644 app/src/Parser/Basic.php create mode 100644 app/src/Parser/Error.php create mode 100644 app/src/Parser/Monolog.php create mode 100644 app/src/Parser/PHPDefault.php diff --git a/app/common/Controller/Logs.php b/app/common/Controller/Logs.php index a7b3606..988615d 100644 --- a/app/common/Controller/Logs.php +++ b/app/common/Controller/Logs.php @@ -22,4 +22,21 @@ class Logs } return $view->render($response, 'logs.show', compact('log', 'levels')); } + public function getMore(ServerRequestInterface $request, ResponseInterface $response, View $view, Service $service, string $log_file, int $start = 0, int $amount = 100): ResponseInterface + { + $log = $service->get($log_file); + + $logs = []; + foreach ($log->getLogs($start, $amount) as $l) { + $logs []= $l; + } + $logs = array_reverse($logs); + $total = $log->getTotal(); + $response->getBody()->write(\Safe\json_encode([ + 'total' => $total, + 'logs' => $logs + ])); + return $response->withStatus(200) + ->withHeader('Content-Type', 'application/json'); + } } diff --git a/app/common/Define/Log.php b/app/common/Define/Log.php new file mode 100644 index 0000000..5e59620 --- /dev/null +++ b/app/common/Define/Log.php @@ -0,0 +1,7 @@ + '/(access.log)/', + Parsers\Error::class => '/(error.log)/', + Parsers\Monolog::class => '/(php-\d{4}-\d{2}-\d{2}.log)/', + Parsers\PHPDefault::class => '/(php_errors.log)/' + ]; + foreach ($map as $class => $regex) { + if (\Safe\preg_match($regex, $filename) === 1) { + return new $class; + } + } + return new Parsers\Basic(); + } public function get(string $log_file): File { $filename = implode(DIRECTORY_SEPARATOR, [$this->getFolder(), $log_file]); $file_info = new SplFileInfo($filename); - $content = \Safe\file_get_contents($filename); + $parser = $this->getParser($log_file); return (new File()) ->setLogger($this->getLogger()) + ->setParser($parser) + ->setFullname($filename) ->setFilename($log_file) - ->setDate((new DateTimeImmutable())->setTimestamp($file_info->getCTime())) - ->setContent($content); + ->setDate((new DateTimeImmutable())->setTimestamp($file_info->getCTime())); } } diff --git a/app/resources/routes/01_logs.php b/app/resources/routes/01_logs.php index c6da3a1..068c4c0 100644 --- a/app/resources/routes/01_logs.php +++ b/app/resources/routes/01_logs.php @@ -5,5 +5,6 @@ $app->group('/logs', function($app) { $app->get('[/]', Logs::class); }); $app->group('/log/{log_file}', function($app) { + $app->get('/more/{start}[/{amount}]', [Logs::class, 'getMore']); $app->get('[/]', [Logs::class, 'get']); }); diff --git a/app/resources/views/logs/show.blade.php b/app/resources/views/logs/show.blade.php index aa8e5fb..3b4bc5e 100644 --- a/app/resources/views/logs/show.blade.php +++ b/app/resources/views/logs/show.blade.php @@ -4,53 +4,8 @@

Log File: {{$log->getFilename()}}

{{$log->getDate()->format('Y-m-d H:i:s')}}
-
- @foreach($log->getLogs() as $line) -
- - - [{{$line->getDate()->format('Y-m-d H:i:s.u')}}] {{$line->getSeverity()}} - -
-
-
-
-
- - - {{$line->getChannel()}}.{{$line->getSeverity()}} - -
-
-
-
- {{$line->getMessage()}} -
-
- @if ($line->hasStack()) -
- -
- @foreach ($line->getStack() as $stack) -
-
- {{$stack}} -
-
- @endforeach -
-
- @endif - @if ($line->hasContext()) -
- {{$line->getContext()}} -
{{$line->getExtra()}}
-
- @endif -
-
- @endforeach -
+
+
@endsection @@ -65,7 +20,134 @@ @push('page_scripts') @endpush diff --git a/app/setup/setups/01_logs.php b/app/setup/setups/01_logs.php index 1b4efa5..5431d74 100644 --- a/app/setup/setups/01_logs.php +++ b/app/setup/setups/01_logs.php @@ -11,6 +11,11 @@ return [ ]) ) ); + $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); + $logger->pushProcessor(new Monolog\Processor\WebProcessor()); + $logger->pushProcessor(new Monolog\Processor\HostnameProcessor()); + $logger->pushProcessor(new Monolog\Processor\IntrospectionProcessor()); + $logger->pushProcessor(new Monolog\Processor\MemoryPeakUsageProcessor()); return $logger; } ]; diff --git a/app/src/Log.php b/app/src/Log.php index 13b31ff..5ea50f2 100644 --- a/app/src/Log.php +++ b/app/src/Log.php @@ -4,8 +4,16 @@ namespace ProVM\Logview; use DateTimeInterface; use DateTimeImmutable; -class Log +class Log implements \ProVM\Common\Define\Log, \JsonSerializable { + public function __construct(?string $original = null) + { + if ($original !== null) { + $this->setOriginal($original); + } + } + + protected string $original; protected DateTimeInterface $dateTime; protected string $channel; protected string $severity; @@ -14,6 +22,10 @@ class Log protected string $context; protected string $extra; + public function getOriginal(): string + { + return $this->original; + } public function getDate(): DateTimeInterface { return $this->dateTime ?? new DateTimeImmutable(); @@ -36,13 +48,18 @@ class Log } public function getContext(): string { - return $this->context; + return $this->context ?? ''; } public function getExtra(): string { return $this->extra ?? ''; } + public function setOriginal(string $original): Log + { + $this->original = $original; + return $this; + } public function setDate(DateTimeInterface $dateTime): Log { $this->dateTime = $dateTime; @@ -79,13 +96,18 @@ class Log return $this; } + public function parsed(): bool + { + return isset($this->severity); + } + public function hasStack(): bool { return isset($this->stack); } public function hasContext(): bool { - return $this->context !== ''; + return isset($this->context) and $this->context !== ''; } public function getColor(): string @@ -97,32 +119,21 @@ class Log return self::BACKGROUNDS[strtoupper($this->getSeverity())]; } - public static function parse(string $content): Log + public function jsonSerialize(): mixed { - $log = new Log(); - - $regex = "/\[(?P.*)\]\s(?\w*)\.(?\w*):\s(?.*)\s[\[|\{](?.*)[\]|\}]\s\[(?.*)\]/"; - preg_match($regex, $content, $matches); - if (isset($matches['date'])) { - $log->setDate(DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.uP', $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); - $log->setContext($matches['context']); - if (isset($matches['extra'])) { - $log->setExtra($matches['extra']); - } - return $log; + return ($this->parsed()) ? [ + 'date' => $this->getDate()->format('Y-m-d H:i:s.u'), + 'channel' => $this->getChannel(), + 'severity' => $this->getSeverity(), + 'message' => $this->getMessage(), + 'stack' => $this->hasStack() ? $this->getStack() : [], + 'context' => $this->hasContext() ? $this->getContext() : '', + 'extra' => $this->getExtra(), + 'parsed' => $this->parsed(), + ] : [ + 'parsed' => $this->parsed(), + 'original' => $this->getOriginal(), + ]; } const LEVELS = [ @@ -134,16 +145,18 @@ class Log 'CRITICAL', 'ALERT', 'EMERGENCY', + 'DEPRECATED', ]; const COLORS = [ 'DEBUG' => '#000', - 'INFO' => '#000', + 'INFO' => '#fff', 'NOTICE' => '#fff', 'WARNING' => '#000', 'ERROR' => '#fff', 'CRITICAL' => '#fff', 'ALERT' => '#fff', 'EMERGENCY' => '#fff', + 'DEPRECATED' => '#fff', ]; const BACKGROUNDS = [ 'DEBUG' => '#fff', @@ -154,5 +167,6 @@ class Log 'CRITICAL' => '#f00', 'ALERT' => '#f55', 'EMERGENCY' => '#f55', + 'DEPRECATED' => '#f50', ]; } diff --git a/app/src/Log/File.php b/app/src/Log/File.php index 4c5fd28..7e4e8d8 100644 --- a/app/src/Log/File.php +++ b/app/src/Log/File.php @@ -1,21 +1,31 @@ logger; } + public function getParser(): Parser + { + return $this->parser; + } + public function getFullname(): string + { + return $this->fullname; + } public function getFilename(): string { return $this->filename; @@ -24,16 +34,22 @@ class File { return $this->dateTime; } - public function getContent(): string - { - return $this->content; - } public function setLogger(LoggerInterface $logger): File { $this->logger = $logger; return $this; } + public function setParser(Parser $parser): File + { + $this->parser = $parser; + return $this; + } + public function setFullname(string $fullname): File + { + $this->fullname = $fullname; + return $this; + } public function setFilename(string $filename): File { $this->filename = $filename; @@ -44,27 +60,43 @@ class File $this->dateTime = $dateTime; return $this; } - public function setContent(string $content): File - { - $this->content = $content; - return $this; - } - public function getLogs(): array + public function getTotal(): int { - $lines = explode(PHP_EOL, $this->getContent()); - $logs = []; - foreach ($lines as $line) { + return $this->getParser()->total($this->getFullname()); + } + public function getLogs(int $start = 0, int $amount = 100): Generator + { + $total = $this->getParser()->total($this->getFullname()); + if ($start >= $total) { + return; + } + $f = $total - $start; + $i = $f - $amount + 1; + if ($i <= 0) { + $i = 0; + } + + $cnt = 1; + $fh = \Safe\fopen($this->getFullname(), 'r'); + while (!feof($fh)) { + $line = fgets($fh); + if ($cnt < $i) { + $cnt ++; + continue; + } + if (!$line) { + continue; + } if (trim($line) === '') { continue; } - try { - $logs []= Log::parse($line); - } catch (\Error | \Exception $e) { - $this->getLogger()->debug($line); - $this->getLogger()->error($e); + yield $this->getParser()->parse(trim($line)); + $cnt ++; + if ($cnt > $f) { + break; } } - return array_reverse($logs); + \Safe\fclose($fh); } } diff --git a/app/src/Parser/Access.php b/app/src/Parser/Access.php new file mode 100644 index 0000000..6c8c057 --- /dev/null +++ b/app/src/Parser/Access.php @@ -0,0 +1,30 @@ +\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 { + $log->setDate(DateTimeImmutable::createFromFormat('d/M/Y:H:i:s P', $matches['date'])); + } catch (DatetimeException $e) { + $log->setDate(new DateTimeImmutable()); + $log->setExtra(json_encode([ + 'date' => $matches['date'] + ], JSON_UNESCAPED_SLASHES)); + } + $log->setSeverity('Info'); + $log->setChannel(''); + $log->setMessage($matches['message']); + $log->setContext($matches['ip']); + return $log; + } +} diff --git a/app/src/Parser/Basic.php b/app/src/Parser/Basic.php new file mode 100644 index 0000000..3fcc3de --- /dev/null +++ b/app/src/Parser/Basic.php @@ -0,0 +1,8 @@ +\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}(?:\+|-)\d{2}:\d{2})\]/"; + $fh = \Safe\fopen($filename, 'r'); + $sum = 0; + while(!feof($fh)) { + $line = fgets($fh); + $sum += \Safe\preg_match_all($regex, $line); + } + fclose($fh); + return $sum; + } catch (\Exception $e) { + \Safe\error_log($e . PHP_EOL, 3, '/logs/total.log'); + return 0; + } + } + public function parse(string $content): Log + { + $log = parent::parse($content); + + $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(?.*)", + "\s(?:\[|\{)(?.*)(?:\]|\})", + "\s(?:\{|\[)(?.*)(?:\}|\])" + ]; + $regex = implode('', $regex); + //$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(?.*)\s(?:\[|\{)(?.*)(?:\]|\})\s(?:\{|\[)(?.*)(?:\}|\])/"; + 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; + } + + 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); + $log->setContext($matches['context']); + if (isset($matches['extra'])) { + $extra['extra'] = "{{$matches['extra']}}"; + } + $log->setExtra(\Safe\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'); + } + + return $log; + } +} diff --git a/app/src/Parser/PHPDefault.php b/app/src/Parser/PHPDefault.php new file mode 100644 index 0000000..5a763f5 --- /dev/null +++ b/app/src/Parser/PHPDefault.php @@ -0,0 +1,54 @@ +\d{2}-\w{3}-\d{4}\s\d{2}:\d{2}:\d{2}\s\w{3})\]/"; + $fh = \Safe\fopen($filename, 'r'); + $sum = 0; + while(!feof($fh)) { + $line = fgets($fh); + $sum += \Safe\preg_match_all($regex, $line); + } + fclose($fh); + return $sum; + } catch (\Exception $e) { + return 0; + } + } + public function parse(string $content): Log + { + $log = parent::parse($content); + $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'); + return $log; + } + + $extra = []; + try { + $log->setDate(DateTimeImmutable::createFromFormat('d-M-Y H:i:s e', $matches['date'])); + } 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)); + if (count($extra) > 0) { + $log->setExtra(\Safe\json_encode($extra, JSON_UNESCAPED_SLASHES)); + } + + return $log; + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 250ad73..bed1a7f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: profiles: - app image: nginx + restart: unless-stopped ports: - "${WEB_PORT:-8030}:80" volumes: @@ -18,6 +19,7 @@ services: profiles: - app build: . + restart: unless-stopped volumes: - "./app:/app" - "./logs:/logs"