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;