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') +
+
+ @foreach ($files as $file) + {{$file->getBasename()}} + @endforeach +
+
+@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); + } +}