diff --git a/app/common/Controller/Base.php b/app/common/Controller/Base.php
new file mode 100644
index 0000000..1f9e35c
--- /dev/null
+++ b/app/common/Controller/Base.php
@@ -0,0 +1,16 @@
+getFiles();
+ return $view->render($response, 'home', compact('files'));
+ }
+}
diff --git a/app/common/Controller/Logs.php b/app/common/Controller/Logs.php
new file mode 100644
index 0000000..a7b3606
--- /dev/null
+++ b/app/common/Controller/Logs.php
@@ -0,0 +1,25 @@
+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'));
+ }
+}
diff --git a/app/common/Service/Logs.php b/app/common/Service/Logs.php
new file mode 100644
index 0000000..24d65f4
--- /dev/null
+++ b/app/common/Service/Logs.php
@@ -0,0 +1,44 @@
+setFolder($folder);
+ }
+
+ protected string $folder;
+
+ public function getFolder(): string
+ {
+ return $this->folder;
+ }
+
+ public function setFolder(string $folder): Logs
+ {
+ $this->folder = $folder;
+ return $this;
+ }
+
+ public function getFiles(): array
+ {
+ $files = new \FilesystemIterator($this->getFolder());
+ $output = [];
+ foreach ($files as $file) {
+ if ($file->isDir()) {
+ continue;
+ }
+ $output []= $file;
+ }
+ return $output;
+ }
+ 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);
+ }
+}
diff --git a/app/composer.json b/app/composer.json
new file mode 100644
index 0000000..52f493c
--- /dev/null
+++ b/app/composer.json
@@ -0,0 +1,34 @@
+{
+ "name": "provm/logview",
+ "description": "Monolog log file viewer",
+ "type": "project",
+ "require": {
+ "berrnd/slim-blade-view": "^1.0",
+ "monolog/monolog": "^3.3",
+ "nyholm/psr7": "^1.5",
+ "nyholm/psr7-server": "^1.0",
+ "php-di/php-di": "^7.0",
+ "php-di/slim-bridge": "^3.3",
+ "slim/slim": "^4.11",
+ "thecodingmachine/safe": "^2.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0",
+ "kint-php/kint": "^5.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "ProVM\\Logview\\": "src/",
+ "ProVM\\Common\\": "common/"
+ }
+ },
+ "authors": [
+ {
+ "name": "Aldarien",
+ "email": "aldarien85@gmail.com"
+ }
+ ],
+ "config": {
+ "sort-packages": true
+ }
+}
diff --git a/app/public/index.php b/app/public/index.php
new file mode 100644
index 0000000..c7aad6a
--- /dev/null
+++ b/app/public/index.php
@@ -0,0 +1,16 @@
+getContainer()->get(LoggerInterface::class));
+try {
+ $app->run();
+} catch (Exception $e) {
+ $app->getContainer()->get(LoggerInterface::class)->alert($e);
+} catch (Error $e) {
+ $app->getContainer()->get(LoggerInterface::class)->error($e);
+}
diff --git a/app/resources/routes/01_logs.php b/app/resources/routes/01_logs.php
new file mode 100644
index 0000000..c6da3a1
--- /dev/null
+++ b/app/resources/routes/01_logs.php
@@ -0,0 +1,9 @@
+group('/logs', function($app) {
+ $app->get('[/]', Logs::class);
+});
+$app->group('/log/{log_file}', function($app) {
+ $app->get('[/]', [Logs::class, 'get']);
+});
diff --git a/app/resources/routes/99_base.php b/app/resources/routes/99_base.php
new file mode 100644
index 0000000..67385ae
--- /dev/null
+++ b/app/resources/routes/99_base.php
@@ -0,0 +1,4 @@
+get('[/]', Base::class);
diff --git a/app/resources/views/home.blade.php b/app/resources/views/home.blade.php
new file mode 100644
index 0000000..fe56fd2
--- /dev/null
+++ b/app/resources/views/home.blade.php
@@ -0,0 +1,11 @@
+@extends('layout.base')
+
+@section('page_content')
+
+@endsection
diff --git a/app/resources/views/layout/base.blade.php b/app/resources/views/layout/base.blade.php
new file mode 100644
index 0000000..9deb8c1
--- /dev/null
+++ b/app/resources/views/layout/base.blade.php
@@ -0,0 +1,5 @@
+
+
+@include('layout.head')
+@include('layout.body')
+
diff --git a/app/resources/views/layout/body.blade.php b/app/resources/views/layout/body.blade.php
new file mode 100644
index 0000000..2df0fdb
--- /dev/null
+++ b/app/resources/views/layout/body.blade.php
@@ -0,0 +1,6 @@
+
+@include('layout.body.header')
+@yield('page_content')
+@include('layout.body.footer')
+@include('layout.body.scripts')
+
diff --git a/app/resources/views/layout/body/footer.blade.php b/app/resources/views/layout/body/footer.blade.php
new file mode 100644
index 0000000..e69de29
diff --git a/app/resources/views/layout/body/header.blade.php b/app/resources/views/layout/body/header.blade.php
new file mode 100644
index 0000000..461b7a7
--- /dev/null
+++ b/app/resources/views/layout/body/header.blade.php
@@ -0,0 +1,5 @@
+
diff --git a/app/resources/views/layout/body/scripts.blade.php b/app/resources/views/layout/body/scripts.blade.php
new file mode 100644
index 0000000..7dc11f8
--- /dev/null
+++ b/app/resources/views/layout/body/scripts.blade.php
@@ -0,0 +1,4 @@
+
+
+
+@stack('page_scripts')
diff --git a/app/resources/views/layout/head.blade.php b/app/resources/views/layout/head.blade.php
new file mode 100644
index 0000000..6d119ec
--- /dev/null
+++ b/app/resources/views/layout/head.blade.php
@@ -0,0 +1,10 @@
+
+
+ @hasSection('page_title')
+ Logs - @yield('page_title')
+ @else
+ Logs
+ @endif
+
+ @include('layout.head.styles')
+
diff --git a/app/resources/views/layout/head/styles.blade.php b/app/resources/views/layout/head/styles.blade.php
new file mode 100644
index 0000000..60ed4f9
--- /dev/null
+++ b/app/resources/views/layout/head/styles.blade.php
@@ -0,0 +1,3 @@
+
+
+@stack('page_styles')
diff --git a/app/setup/app.php b/app/setup/app.php
new file mode 100644
index 0000000..f97f6e1
--- /dev/null
+++ b/app/setup/app.php
@@ -0,0 +1,40 @@
+isDir()) {
+ continue;
+ }
+ $builder->addDefinitions($file->getRealPath());
+ }
+}
+
+$app = Bridge::create($builder->build());
+
+$folder = implode(DIRECTORY_SEPARATOR, [__DIR__, 'middlewares']);
+if (file_exists($folder)) {
+ $files = new FilesystemIterator($folder);
+ foreach ($files as $file) {
+ if ($file->isDir()) {
+ continue;
+ }
+ include_once $file->getRealPath();
+ }
+}
+
+return $app;
diff --git a/app/setup/composer.php b/app/setup/composer.php
new file mode 100644
index 0000000..b451f96
--- /dev/null
+++ b/app/setup/composer.php
@@ -0,0 +1,6 @@
+getContainer()->get('folders')->get('routes');
+$files = new FilesystemIterator($folder);
+foreach ($files as $file) {
+ if ($file->isDir()) {
+ continue;
+ }
+ include_once $file->getRealPath();
+}
diff --git a/app/setup/settings/01_env.php b/app/setup/settings/01_env.php
new file mode 100644
index 0000000..6e29aa3
--- /dev/null
+++ b/app/setup/settings/01_env.php
@@ -0,0 +1,4 @@
+ $_ENV['LOGS_PATH'] ?? '/logs'
+];
diff --git a/app/setup/settings/02_folders.php b/app/setup/settings/02_folders.php
new file mode 100644
index 0000000..9bcd19f
--- /dev/null
+++ b/app/setup/settings/02_folders.php
@@ -0,0 +1,22 @@
+ function() {
+ return new DI\Container([
+ 'base' => dirname(__FILE__, 3),
+ 'resources' => DI\String('{base}/resources'),
+ 'routes' => DI\String('{resources}/routes'),
+ 'cache' => DI\String('{base}/cache'),
+ 'templates' => DI\String('{resources}/views')
+ ]);
+ /*$arr = ['base' => dirname(__FILE__, 3)];
+ $arr['resources'] = implode(DIRECTORY_SEPARATOR, [
+ $arr['base'],
+ 'resources'
+ ]);
+ $arr['routes'] = implode(DIRECTORY_SEPARATOR, [
+ $arr['resources'],
+ 'routes'
+ ]);
+ return (object) $arr;*/
+ }
+];
diff --git a/app/setup/settings/03_urls.php b/app/setup/settings/03_urls.php
new file mode 100644
index 0000000..cd39a6a
--- /dev/null
+++ b/app/setup/settings/03_urls.php
@@ -0,0 +1,11 @@
+ function() {
+ $arr = ['base' => $_ENV['WEB_URL'] ?? 'http://localhost:' . ($_ENV['WEB_PORT'] ?? 8030)];
+ $arr['assets'] = implode('/', [
+ $arr['base'],
+ 'assets'
+ ]);
+ return (object) $arr;
+ }
+];
diff --git a/app/setup/setups/01_logs.php b/app/setup/setups/01_logs.php
new file mode 100644
index 0000000..1b4efa5
--- /dev/null
+++ b/app/setup/setups/01_logs.php
@@ -0,0 +1,16 @@
+ function(ContainerInterface $container) {
+ $logger = new Monolog\Logger('logger');
+ $logger->pushHandler(
+ new Monolog\Handler\RotatingFileHandler(
+ implode(DIRECTORY_SEPARATOR, [
+ $container->get('logs_folder'), 'php.log'
+ ])
+ )
+ );
+ return $logger;
+ }
+];
diff --git a/app/setup/setups/02_view.php b/app/setup/setups/02_view.php
new file mode 100644
index 0000000..3bb8056
--- /dev/null
+++ b/app/setup/setups/02_view.php
@@ -0,0 +1,15 @@
+ function(ContainerInterface $container) {
+ return new Slim\Views\Blade(
+ $container->get('folders')->get('templates'),
+ $container->get('folders')->get('cache'),
+ null,
+ [
+ 'urls' => $container->get('urls')
+ ]
+ );
+ }
+];
diff --git a/app/setup/setups/03_services.php b/app/setup/setups/03_services.php
new file mode 100644
index 0000000..fc06cb9
--- /dev/null
+++ b/app/setup/setups/03_services.php
@@ -0,0 +1,10 @@
+ function(ContainerInterface $container) {
+ return new ProVM\Common\Service\Logs(
+ $container->get('logs_folder')
+ );
+ }
+];
diff --git a/app/src/Log.php b/app/src/Log.php
new file mode 100644
index 0000000..c2f129f
--- /dev/null
+++ b/app/src/Log.php
@@ -0,0 +1,156 @@
+dateTime;
+ }
+ public function getChannel(): string
+ {
+ return $this->channel;
+ }
+ public function getSeverity(): string
+ {
+ return $this->severity;
+ }
+ public function getMessage(): string
+ {
+ return $this->message;
+ }
+ public function getStack(): array
+ {
+ return $this->stack ?? [];
+ }
+ public function getContext(): string
+ {
+ return $this->context;
+ }
+ public function getExtra(): string
+ {
+ return $this->extra ?? '';
+ }
+
+ public function setDate(DateTimeInterface $dateTime): Log
+ {
+ $this->dateTime = $dateTime;
+ return $this;
+ }
+ public function setChannel(string $channel): Log
+ {
+ $this->channel = $channel;
+ return $this;
+ }
+ public function setSeverity(string $severity): Log
+ {
+ $this->severity = $severity;
+ return $this;
+ }
+ public function setMessage(string $message): Log
+ {
+ $this->message = $message;
+ return $this;
+ }
+ public function setStack(array $stack): Log
+ {
+ $this->stack = $stack;
+ return $this;
+ }
+ public function setContext(string $context): Log
+ {
+ $this->context = $context;
+ return $this;
+ }
+ public function setExtra(string $extra): Log
+ {
+ $this->extra = $extra;
+ return $this;
+ }
+
+ public function hasStack(): bool
+ {
+ return isset($this->stack);
+ }
+ public function hasContext(): bool
+ {
+ return $this->context !== '';
+ }
+
+ public function getColor(): string
+ {
+ return self::COLORS[strtoupper($this->getSeverity())];
+ }
+ public function getBackgroundColor(): string
+ {
+ return self::BACKGROUNDS[strtoupper($this->getSeverity())];
+ }
+
+ public static function parse(string $content): Log
+ {
+ $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;
+ }
+
+ const LEVELS = [
+ 'DEBUG',
+ 'INFO',
+ 'NOTICE',
+ 'WARNING',
+ 'ERROR',
+ 'CRITICAL',
+ 'ALERT',
+ 'EMERGENCY',
+ ];
+ const COLORS = [
+ 'DEBUG' => '#000',
+ 'INFO' => '#000',
+ 'NOTICE' => '#fff',
+ 'WARNING' => '#000',
+ 'ERROR' => '#fff',
+ 'CRITICAL' => '#fff',
+ 'ALERT' => '#fff',
+ 'EMERGENCY' => '#fff',
+ ];
+ const BACKGROUNDS = [
+ 'DEBUG' => '#fff',
+ 'INFO' => '#00f',
+ 'NOTICE' => '#55f',
+ 'WARNING' => '#dd5',
+ 'ERROR' => '#555',
+ 'CRITICAL' => '#f00',
+ 'ALERT' => '#f55',
+ 'EMERGENCY' => '#f55',
+ ];
+}
diff --git a/app/src/Log/File.php b/app/src/Log/File.php
new file mode 100644
index 0000000..07d0778
--- /dev/null
+++ b/app/src/Log/File.php
@@ -0,0 +1,44 @@
+filename;
+ }
+ public function getContent(): string
+ {
+ return $this->content;
+ }
+
+ public function setFilename(string $filename): File
+ {
+ $this->filename = $filename;
+ return $this;
+ }
+ public function setContent(string $content): File
+ {
+ $this->content = $content;
+ return $this;
+ }
+
+ public function getLogs(): array
+ {
+ $lines = explode(PHP_EOL, $this->getContent());
+ $logs = [];
+ foreach ($lines as $line) {
+ if (trim($line) === '') {
+ continue;
+ }
+ $logs []= Log::parse($line);
+ }
+ return array_reverse($logs);
+ }
+}