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 @@
-
- @foreach($log->getLogs() as $line)
-
-
-
- [{{$line->getDate()->format('Y-m-d H:i:s.u')}}] {{$line->getSeverity()}}
-
-
-
-
-
-
-
-
-
- {{$line->getMessage()}}
-
-
- @if ($line->hasStack())
-
-
-
- @foreach ($line->getStack() as $stack)
-
- @endforeach
-
-
- @endif
- @if ($line->hasContext())
-
- @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"