diff --git a/.gitignore b/.gitignore
index a8a0611..57618a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
**/*.env
**/.idea/
-**/logs/
+/logs/
**/cache/
**/vendor/
**/*.lock
diff --git a/Prod.Dockerfile b/Prod.Dockerfile
new file mode 100644
index 0000000..92c0fd1
--- /dev/null
+++ b/Prod.Dockerfile
@@ -0,0 +1,23 @@
+FROM php:8-fpm
+
+ENV LOGVIEW_INSTALLATION_PATH=/app
+ENV COMPOSER_ALLOW_SUPERUSER=1
+ENV APACHE_DOCUMENT_ROOT="${LOGVIEW_INSTALLATION_PATH}/app"
+ENV APACHE_PUBLIC_ROOT="${APACHE_DOCUMENT_ROOT}/public"
+
+COPY --from=composer /usr/bin/composer /usr/bin/composer
+WORKDIR "${LOGVIEW_INSTALLATION_PATH}"
+
+RUN apt-get update \
+ && apt-get install -yq --no-install-recommends git zip unzip libzip-dev \
+ && rm -r /var/lib/apt/lists/* \
+ && git clone http://git.provm.cl/ProVM/logview.git "${LOGVIEW_INSTALLATION_PATH}" \
+ && docker-php-ext-install zip \
+ && composer -d "${LOGVIEW_INSTALLATION_PATH}/app" install \
+ && mkdir "${LOGVIEW_INSTALLATION_PATH}/app/cache" \
+ && chmod -R 777 "${LOGVIEW_INSTALLATION_PATH}/app/cache" \
+ && sed -ri -e "s!/var/www/html!${APACHE_PUBLIC_ROOT}!g" /etc/apache2/sites-available/*.conf \
+ && sed -ri -e "s!/var/www/!${APACHE_DOCUMENT_ROOT}!g" /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
+ && a2enmod rewrite \
+ && a2enmod actions \
+ && service apache2 restart
diff --git a/app/.htaccess b/app/.htaccess
new file mode 100644
index 0000000..659993e
--- /dev/null
+++ b/app/.htaccess
@@ -0,0 +1,3 @@
+RewriteEngine on
+RewriteRule ^$ public/ [L]
+RewriteRule (.*) public/$1 [L]
diff --git a/app/common/Controller/Base.php b/app/common/Controller/Base.php
index 1f9e35c..a5b3693 100644
--- a/app/common/Controller/Base.php
+++ b/app/common/Controller/Base.php
@@ -1,16 +1,20 @@
getFiles();
+ usort($files, function(SplFileInfo $a, SplFileInfo $b) {
+ return $b->getCTime() - $a->getCTime();
+ });
return $view->render($response, 'home', compact('files'));
}
}
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 @@
+setLogger($logger)
->setFolder($folder);
}
+ protected LoggerInterface $logger;
protected string $folder;
+ public function getLogger(): LoggerInterface
+ {
+ return $this->logger;
+ }
public function getFolder(): string
{
return $this->folder;
}
+ public function setLogger(LoggerInterface $logger): Logs
+ {
+ $this->logger = $logger;
+ return $this;
+ }
public function setFolder(string $folder): Logs
{
$this->folder = $folder;
@@ -36,9 +52,31 @@ class Logs
}
return $output;
}
+ public function getParser(string $filename): Parser
+ {
+ $map = [
+ Parsers\Access::class => '/(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
{
- $content = \Safe\file_get_contents(implode(DIRECTORY_SEPARATOR, [$this->getFolder(), $log_file]));
- return (new File())->setFilename($log_file)->setContent($content);
+ $filename = implode(DIRECTORY_SEPARATOR, [$this->getFolder(), $log_file]);
+ $file_info = new SplFileInfo($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()));
}
}
diff --git a/app/public/.htaccess b/app/public/.htaccess
new file mode 100644
index 0000000..66ef8f6
--- /dev/null
+++ b/app/public/.htaccess
@@ -0,0 +1,4 @@
+RewriteEngine On
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^ index.php [QSA,L]
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/home.blade.php b/app/resources/views/home.blade.php
index fe56fd2..fde4321 100644
--- a/app/resources/views/home.blade.php
+++ b/app/resources/views/home.blade.php
@@ -4,7 +4,7 @@
diff --git a/app/resources/views/logs/base.blade.php b/app/resources/views/logs/base.blade.php
new file mode 100644
index 0000000..6993900
--- /dev/null
+++ b/app/resources/views/logs/base.blade.php
@@ -0,0 +1,5 @@
+@extends('layout.base')
+
+@section('page_title')
+ Log File
+@endsection
diff --git a/app/resources/views/logs/show.blade.php b/app/resources/views/logs/show.blade.php
new file mode 100644
index 0000000..aedf07d
--- /dev/null
+++ b/app/resources/views/logs/show.blade.php
@@ -0,0 +1,155 @@
+@extends('logs.base')
+
+@section('page_content')
+
+@endsection
+
+@push('page_styles')
+
+@endpush
+
+@push('page_scripts')
+
+@endpush
diff --git a/app/setup/settings/99_general.php b/app/setup/settings/99_general.php
new file mode 100644
index 0000000..e825caf
--- /dev/null
+++ b/app/setup/settings/99_general.php
@@ -0,0 +1,4 @@
+ $_ENV['MAX_LOG_AMOUNT'] ?? 500
+];
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/setup/setups/02_view.php b/app/setup/setups/02_view.php
index 3bb8056..060829f 100644
--- a/app/setup/setups/02_view.php
+++ b/app/setup/setups/02_view.php
@@ -8,7 +8,8 @@ return [
$container->get('folders')->get('cache'),
null,
[
- 'urls' => $container->get('urls')
+ 'urls' => $container->get('urls'),
+ 'max_log_amount' => $container->get('max_log_amount')
]
);
}
diff --git a/app/setup/setups/03_services.php b/app/setup/setups/03_services.php
index fc06cb9..ac7fa9d 100644
--- a/app/setup/setups/03_services.php
+++ b/app/setup/setups/03_services.php
@@ -4,6 +4,7 @@ use Psr\Container\ContainerInterface;
return [
ProVM\Common\Service\Logs::class => function(ContainerInterface $container) {
return new ProVM\Common\Service\Logs(
+ $container->get(Psr\Log\LoggerInterface::class),
$container->get('logs_folder')
);
}
diff --git a/app/src/Log.php b/app/src/Log.php
index c2f129f..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,9 +22,13 @@ class Log
protected string $context;
protected string $extra;
+ public function getOriginal(): string
+ {
+ return $this->original;
+ }
public function getDate(): DateTimeInterface
{
- return $this->dateTime;
+ return $this->dateTime ?? new DateTimeImmutable();
}
public function getChannel(): string
{
@@ -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,30 +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);
- $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 = [
@@ -132,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',
@@ -152,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 07d0778..7e4e8d8 100644
--- a/app/src/Log/File.php
+++ b/app/src/Log/File.php
@@ -2,43 +2,101 @@
namespace ProVM\Logview\Log;
use Generator;
-use ProVM\Logview\Log;
+use DateTimeInterface;
+use Psr\Log\LoggerInterface;
+use ProVM\Common\Define\Parser;
class File
{
+ protected LoggerInterface $logger;
+ protected Parser $parser;
+ protected string $fullname;
protected string $filename;
- protected string $content;
+ protected DateTimeInterface $dateTime;
+ public function getLogger(): LoggerInterface
+ {
+ return $this->logger;
+ }
+ public function getParser(): Parser
+ {
+ return $this->parser;
+ }
+ public function getFullname(): string
+ {
+ return $this->fullname;
+ }
public function getFilename(): string
{
return $this->filename;
}
- public function getContent(): string
+ public function getDate(): DateTimeInterface
{
- return $this->content;
+ return $this->dateTime;
}
+ 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;
return $this;
}
- public function setContent(string $content): File
+ public function setDate(DateTimeInterface $dateTime): File
{
- $this->content = $content;
+ $this->dateTime = $dateTime;
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;
}
- $logs []= Log::parse($line);
+ 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);
+ 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);
+ if ($matches['context'] !== '') {
+ $log->setContext("{{$matches['context']}}");
+ }
+ if (isset($matches['extra']) and $matches['extra'] !== '') {
+ $extra['extra'] = "{{$matches['extra']}}";
+ }
+ if (count($extra) > 0) {
+ $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 4d4cd33..bed1a7f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,11 +6,12 @@ services:
profiles:
- app
image: nginx
+ restart: unless-stopped
ports:
- "${WEB_PORT:-8030}:80"
volumes:
- "./nginx.conf:/etc/nginx/conf.d/default.conf"
- - "./src:/app"
+ - "./app:/app"
- "./logs:/logs"
php:
@@ -18,6 +19,7 @@ services:
profiles:
- app
build: .
+ restart: unless-stopped
volumes:
- - "./src:/app"
+ - "./app:/app"
- "./logs:/logs"