Compare commits
19 Commits
66bf12c353
...
1dbcaf0a26
Author | SHA1 | Date | |
---|---|---|---|
1dbcaf0a26 | |||
894732e806 | |||
c29eece81c | |||
110f37e4f4 | |||
e59e91805b | |||
44f94d4101 | |||
3718c1e0aa | |||
f8dbb1390e | |||
b9d5c6c3f5 | |||
a576f3b0d0 | |||
376b72bacb | |||
024a6de924 | |||
b9ba960c79 | |||
b5065ecea0 | |||
05de20cb4b | |||
6ea469d5a9 | |||
85be48b794 | |||
40598221a1 | |||
45069dfc00 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,6 @@
|
|||||||
**/*.env
|
**/*.env
|
||||||
**/.idea/
|
**/.idea/
|
||||||
**/logs/
|
/logs/
|
||||||
**/cache/
|
**/cache/
|
||||||
**/vendor/
|
**/vendor/
|
||||||
**/*.lock
|
**/*.lock
|
||||||
|
23
Prod.Dockerfile
Normal file
23
Prod.Dockerfile
Normal file
@ -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
|
3
app/.htaccess
Normal file
3
app/.htaccess
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
RewriteEngine on
|
||||||
|
RewriteRule ^$ public/ [L]
|
||||||
|
RewriteRule (.*) public/$1 [L]
|
@ -1,16 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace ProVM\Common\Controller;
|
namespace ProVM\Common\Controller;
|
||||||
|
|
||||||
use ProVM\Common\Service\Logs;
|
use SplFileInfo;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Slim\Views\Blade as View;
|
use Slim\Views\Blade as View;
|
||||||
|
use ProVM\Common\Service\Logs;
|
||||||
|
|
||||||
class Base
|
class Base
|
||||||
{
|
{
|
||||||
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, View $view, Logs $service): ResponseInterface
|
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, View $view, Logs $service): ResponseInterface
|
||||||
{
|
{
|
||||||
$files = $service->getFiles();
|
$files = $service->getFiles();
|
||||||
|
usort($files, function(SplFileInfo $a, SplFileInfo $b) {
|
||||||
|
return $b->getCTime() - $a->getCTime();
|
||||||
|
});
|
||||||
return $view->render($response, 'home', compact('files'));
|
return $view->render($response, 'home', compact('files'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,4 +22,21 @@ class Logs
|
|||||||
}
|
}
|
||||||
return $view->render($response, 'logs.show', compact('log', 'levels'));
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
7
app/common/Define/Log.php
Normal file
7
app/common/Define/Log.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
namespace ProVM\Common\Define;
|
||||||
|
|
||||||
|
interface Log
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
7
app/common/Define/Parser.php
Normal file
7
app/common/Define/Parser.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
namespace ProVM\Common\Define;
|
||||||
|
|
||||||
|
interface Parser
|
||||||
|
{
|
||||||
|
public function parse(string $content): Log;
|
||||||
|
}
|
27
app/common/Implement/Parser.php
Normal file
27
app/common/Implement/Parser.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
namespace ProVM\Common\Implement;
|
||||||
|
|
||||||
|
use ProVM\Common\Define\Log;
|
||||||
|
|
||||||
|
abstract class Parser implements \ProVM\Common\Define\Parser
|
||||||
|
{
|
||||||
|
public function total(string $filename): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$fh = \Safe\fopen($filename, 'r');
|
||||||
|
$cnt = 0;
|
||||||
|
while(!feof($fh)) {
|
||||||
|
$line = fgets($fh);
|
||||||
|
$cnt ++;
|
||||||
|
}
|
||||||
|
fclose($fh);
|
||||||
|
return $cnt;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public function parse(string $content): Log
|
||||||
|
{
|
||||||
|
return new \ProVM\Logview\Log($content);
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,39 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace ProVM\Common\Service;
|
namespace ProVM\Common\Service;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use SplFileInfo;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use ProVM\Logview\Log\File;
|
use ProVM\Logview\Log\File;
|
||||||
|
use ProVM\Common\Define\Parser;
|
||||||
|
use ProVM\Logview\Parser as Parsers;
|
||||||
|
|
||||||
class Logs
|
class Logs
|
||||||
{
|
{
|
||||||
public function __construct(string $folder)
|
public function __construct(LoggerInterface $logger, string $folder)
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
|
->setLogger($logger)
|
||||||
->setFolder($folder);
|
->setFolder($folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected LoggerInterface $logger;
|
||||||
protected string $folder;
|
protected string $folder;
|
||||||
|
|
||||||
|
public function getLogger(): LoggerInterface
|
||||||
|
{
|
||||||
|
return $this->logger;
|
||||||
|
}
|
||||||
public function getFolder(): string
|
public function getFolder(): string
|
||||||
{
|
{
|
||||||
return $this->folder;
|
return $this->folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setLogger(LoggerInterface $logger): Logs
|
||||||
|
{
|
||||||
|
$this->logger = $logger;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
public function setFolder(string $folder): Logs
|
public function setFolder(string $folder): Logs
|
||||||
{
|
{
|
||||||
$this->folder = $folder;
|
$this->folder = $folder;
|
||||||
@ -36,9 +52,31 @@ class Logs
|
|||||||
}
|
}
|
||||||
return $output;
|
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
|
public function get(string $log_file): File
|
||||||
{
|
{
|
||||||
$content = \Safe\file_get_contents(implode(DIRECTORY_SEPARATOR, [$this->getFolder(), $log_file]));
|
$filename = implode(DIRECTORY_SEPARATOR, [$this->getFolder(), $log_file]);
|
||||||
return (new File())->setFilename($log_file)->setContent($content);
|
$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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
app/public/.htaccess
Normal file
4
app/public/.htaccess
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^ index.php [QSA,L]
|
@ -5,5 +5,6 @@ $app->group('/logs', function($app) {
|
|||||||
$app->get('[/]', Logs::class);
|
$app->get('[/]', Logs::class);
|
||||||
});
|
});
|
||||||
$app->group('/log/{log_file}', function($app) {
|
$app->group('/log/{log_file}', function($app) {
|
||||||
|
$app->get('/more/{start}[/{amount}]', [Logs::class, 'getMore']);
|
||||||
$app->get('[/]', [Logs::class, 'get']);
|
$app->get('[/]', [Logs::class, 'get']);
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="ui list">
|
<div class="ui list">
|
||||||
@foreach ($files as $file)
|
@foreach ($files as $file)
|
||||||
<a class="item" href="{{$urls->base}}/log/{{urlencode($file->getBasename())}}">{{$file->getBasename()}}</a>
|
<a class="item" href="{{$urls->base}}/log/{{urlencode($file->getBasename())}}">[{{(new DateTimeImmutable)->setTimestamp($file->getCTime())->format('Y-m-d H:i:s')}}] {{$file->getBasename()}}</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
5
app/resources/views/logs/base.blade.php
Normal file
5
app/resources/views/logs/base.blade.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@extends('layout.base')
|
||||||
|
|
||||||
|
@section('page_title')
|
||||||
|
Log File
|
||||||
|
@endsection
|
155
app/resources/views/logs/show.blade.php
Normal file
155
app/resources/views/logs/show.blade.php
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
@extends('logs.base')
|
||||||
|
|
||||||
|
@section('page_content')
|
||||||
|
<div class="ui container">
|
||||||
|
<h3 class="ui header">Log File: {{$log->getFilename()}}</h3>
|
||||||
|
<h5 class="ui header">{{$log->getDate()->format('Y-m-d H:i:s')}}</h5>
|
||||||
|
<div class="ui accordion" id="logs"></div>
|
||||||
|
<hr id="watch" style="border: none;" />
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('page_styles')
|
||||||
|
<style>
|
||||||
|
body {overflow-y:scroll;}
|
||||||
|
@foreach ($levels as $level => $colors)
|
||||||
|
.{{$level}} {background-color: {{$colors->background}}; color: {{$colors->text}};}
|
||||||
|
@endforeach
|
||||||
|
</style>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@push('page_scripts')
|
||||||
|
<script type="text/javascript">
|
||||||
|
const logs = {
|
||||||
|
id: '',
|
||||||
|
start: 0,
|
||||||
|
amount: {{$max_log_amount}},
|
||||||
|
total: 0,
|
||||||
|
remaining: 0,
|
||||||
|
watch_pos: 0,
|
||||||
|
get: function() {
|
||||||
|
return {
|
||||||
|
id: () => {
|
||||||
|
if (this.id.indexOf('#') !== -1) {
|
||||||
|
return this.id
|
||||||
|
}
|
||||||
|
return '#' + this.id
|
||||||
|
},
|
||||||
|
more: (start, amount) => {
|
||||||
|
if (this.total > 0 && this.remaining <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: '{{$urls->base}}/log/{{urlencode($log->getFilename())}}/more/' + start + '/' + amount
|
||||||
|
}).then(response => {
|
||||||
|
if (response.logs) {
|
||||||
|
if (this.total === 0) {
|
||||||
|
this.remaining = this.total = response.total
|
||||||
|
}
|
||||||
|
this.remaining -= response.logs.length
|
||||||
|
this.start += response.logs.length
|
||||||
|
console.debug(this.total, this.remaining)
|
||||||
|
this.draw().more(response.logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
draw: function() {
|
||||||
|
return {
|
||||||
|
more: logs => {
|
||||||
|
const parent = $(this.get().id())
|
||||||
|
logs.forEach(log => {
|
||||||
|
if (log.parsed) {
|
||||||
|
this.draw().parsed(parent, log)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.draw().unparsed(parent, log)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
title: () => {
|
||||||
|
return $('<div></div>').addClass('title')
|
||||||
|
.append($('<i></i>').addClass('dropdown icon'))
|
||||||
|
},
|
||||||
|
unparsed: (parent, log) => {
|
||||||
|
const title = this.draw().title()
|
||||||
|
const content = $('<div></div>').addClass('content')
|
||||||
|
title.append(log.original)
|
||||||
|
content.html(log.original)
|
||||||
|
parent.append(title).append(content)
|
||||||
|
},
|
||||||
|
parsed: (parent, log) => {
|
||||||
|
const title = this.draw().title()
|
||||||
|
const content = $('<div></div>').addClass('content')
|
||||||
|
title.append(
|
||||||
|
$('<span></span>')
|
||||||
|
.addClass(log.severity.toLowerCase()).css('padding', '.5ex 1ex')//.css('padding-left', '1ex').css('padding-right', '1ex')
|
||||||
|
.html('[' + log.date + '] ' + log.severity)
|
||||||
|
)
|
||||||
|
const card = $('<div></div>').addClass('ui fluid basic card').append(
|
||||||
|
$('<div></div>').addClass('content').append(
|
||||||
|
$('<div></div>').addClass('header').append(
|
||||||
|
$('<span></span>').addClass(log.severity.toLowerCase()).css('padding', '1ex 1em').append(
|
||||||
|
$('<i></i>').addClass('bug icon')
|
||||||
|
).append(
|
||||||
|
((log.channel === '') ? '' : log.channel + '.') + log.severity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).append(
|
||||||
|
$('<div></div>').addClass('content').append(
|
||||||
|
$('<div></div>').addClass('description').html(log.message)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (log.stack.length > 0) {
|
||||||
|
const feed = $('<div></div>').addClass('ui small feed')
|
||||||
|
log.stack.forEach(stack => {
|
||||||
|
feed.append(
|
||||||
|
$('<div></div>').addClass('event').append(
|
||||||
|
$('<div></div>').addClass('content').html(stack)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
card.append(
|
||||||
|
$('<div></div>').addClass('content').append(feed)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (log.context !== '') {
|
||||||
|
card.append(
|
||||||
|
$('<div></div>').addClass('extra content').append(
|
||||||
|
log.context
|
||||||
|
).append(
|
||||||
|
$('<div></div>').addClass('meta').html(log.extra)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
content.append(card)
|
||||||
|
parent.append(title).append(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: function() {
|
||||||
|
return {
|
||||||
|
more: payload => {
|
||||||
|
if (payload[0].isIntersecting) {
|
||||||
|
if (payload[0].rootBounds.bottom !== this.watch_pos) {
|
||||||
|
this.get().more(this.start, this.amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup: function(id) {
|
||||||
|
this.id = id
|
||||||
|
$(this.get().id()).accordion()
|
||||||
|
|
||||||
|
const ob = new IntersectionObserver(this.watch().more)
|
||||||
|
const watch = document.querySelector('#watch')
|
||||||
|
ob.observe(watch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$(document).ready(() => {
|
||||||
|
logs.setup('logs')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
@endpush
|
4
app/setup/settings/99_general.php
Normal file
4
app/setup/settings/99_general.php
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
'max_log_amount' => $_ENV['MAX_LOG_AMOUNT'] ?? 500
|
||||||
|
];
|
@ -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;
|
return $logger;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -8,7 +8,8 @@ return [
|
|||||||
$container->get('folders')->get('cache'),
|
$container->get('folders')->get('cache'),
|
||||||
null,
|
null,
|
||||||
[
|
[
|
||||||
'urls' => $container->get('urls')
|
'urls' => $container->get('urls'),
|
||||||
|
'max_log_amount' => $container->get('max_log_amount')
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ use Psr\Container\ContainerInterface;
|
|||||||
return [
|
return [
|
||||||
ProVM\Common\Service\Logs::class => function(ContainerInterface $container) {
|
ProVM\Common\Service\Logs::class => function(ContainerInterface $container) {
|
||||||
return new ProVM\Common\Service\Logs(
|
return new ProVM\Common\Service\Logs(
|
||||||
|
$container->get(Psr\Log\LoggerInterface::class),
|
||||||
$container->get('logs_folder')
|
$container->get('logs_folder')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,16 @@ namespace ProVM\Logview;
|
|||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use DateTimeImmutable;
|
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 DateTimeInterface $dateTime;
|
||||||
protected string $channel;
|
protected string $channel;
|
||||||
protected string $severity;
|
protected string $severity;
|
||||||
@ -14,9 +22,13 @@ class Log
|
|||||||
protected string $context;
|
protected string $context;
|
||||||
protected string $extra;
|
protected string $extra;
|
||||||
|
|
||||||
|
public function getOriginal(): string
|
||||||
|
{
|
||||||
|
return $this->original;
|
||||||
|
}
|
||||||
public function getDate(): DateTimeInterface
|
public function getDate(): DateTimeInterface
|
||||||
{
|
{
|
||||||
return $this->dateTime;
|
return $this->dateTime ?? new DateTimeImmutable();
|
||||||
}
|
}
|
||||||
public function getChannel(): string
|
public function getChannel(): string
|
||||||
{
|
{
|
||||||
@ -36,13 +48,18 @@ class Log
|
|||||||
}
|
}
|
||||||
public function getContext(): string
|
public function getContext(): string
|
||||||
{
|
{
|
||||||
return $this->context;
|
return $this->context ?? '';
|
||||||
}
|
}
|
||||||
public function getExtra(): string
|
public function getExtra(): string
|
||||||
{
|
{
|
||||||
return $this->extra ?? '';
|
return $this->extra ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setOriginal(string $original): Log
|
||||||
|
{
|
||||||
|
$this->original = $original;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
public function setDate(DateTimeInterface $dateTime): Log
|
public function setDate(DateTimeInterface $dateTime): Log
|
||||||
{
|
{
|
||||||
$this->dateTime = $dateTime;
|
$this->dateTime = $dateTime;
|
||||||
@ -79,13 +96,18 @@ class Log
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function parsed(): bool
|
||||||
|
{
|
||||||
|
return isset($this->severity);
|
||||||
|
}
|
||||||
|
|
||||||
public function hasStack(): bool
|
public function hasStack(): bool
|
||||||
{
|
{
|
||||||
return isset($this->stack);
|
return isset($this->stack);
|
||||||
}
|
}
|
||||||
public function hasContext(): bool
|
public function hasContext(): bool
|
||||||
{
|
{
|
||||||
return $this->context !== '';
|
return isset($this->context) and $this->context !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getColor(): string
|
public function getColor(): string
|
||||||
@ -97,30 +119,21 @@ class Log
|
|||||||
return self::BACKGROUNDS[strtoupper($this->getSeverity())];
|
return self::BACKGROUNDS[strtoupper($this->getSeverity())];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function parse(string $content): Log
|
public function jsonSerialize(): mixed
|
||||||
{
|
{
|
||||||
$log = new Log();
|
return ($this->parsed()) ? [
|
||||||
|
'date' => $this->getDate()->format('Y-m-d H:i:s.u'),
|
||||||
$regex = "/\[(?P<date>.*)\]\s(?<channel>\w*)\.(?<severity>\w*):\s(?<message>.*)\s[\[|\{](?<context>.*)[\]|\}]\s\[(?<extra>.*)\]/";
|
'channel' => $this->getChannel(),
|
||||||
preg_match($regex, $content, $matches);
|
'severity' => $this->getSeverity(),
|
||||||
$log->setDate(DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.uP', $matches['date']));
|
'message' => $this->getMessage(),
|
||||||
$log->setChannel($matches['channel']);
|
'stack' => $this->hasStack() ? $this->getStack() : [],
|
||||||
$log->setSeverity($matches['severity']);
|
'context' => $this->hasContext() ? $this->getContext() : '',
|
||||||
$message = $matches['message'];
|
'extra' => $this->getExtra(),
|
||||||
if (str_contains($message, 'Stack trace')) {
|
'parsed' => $this->parsed(),
|
||||||
list($msg, $data) = explode('Stack trace:', $message);
|
] : [
|
||||||
$message = trim($msg);
|
'parsed' => $this->parsed(),
|
||||||
$regex = '/\s#\d+\s/';
|
'original' => $this->getOriginal(),
|
||||||
$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 = [
|
const LEVELS = [
|
||||||
@ -132,16 +145,18 @@ class Log
|
|||||||
'CRITICAL',
|
'CRITICAL',
|
||||||
'ALERT',
|
'ALERT',
|
||||||
'EMERGENCY',
|
'EMERGENCY',
|
||||||
|
'DEPRECATED',
|
||||||
];
|
];
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
'DEBUG' => '#000',
|
'DEBUG' => '#000',
|
||||||
'INFO' => '#000',
|
'INFO' => '#fff',
|
||||||
'NOTICE' => '#fff',
|
'NOTICE' => '#fff',
|
||||||
'WARNING' => '#000',
|
'WARNING' => '#000',
|
||||||
'ERROR' => '#fff',
|
'ERROR' => '#fff',
|
||||||
'CRITICAL' => '#fff',
|
'CRITICAL' => '#fff',
|
||||||
'ALERT' => '#fff',
|
'ALERT' => '#fff',
|
||||||
'EMERGENCY' => '#fff',
|
'EMERGENCY' => '#fff',
|
||||||
|
'DEPRECATED' => '#fff',
|
||||||
];
|
];
|
||||||
const BACKGROUNDS = [
|
const BACKGROUNDS = [
|
||||||
'DEBUG' => '#fff',
|
'DEBUG' => '#fff',
|
||||||
@ -152,5 +167,6 @@ class Log
|
|||||||
'CRITICAL' => '#f00',
|
'CRITICAL' => '#f00',
|
||||||
'ALERT' => '#f55',
|
'ALERT' => '#f55',
|
||||||
'EMERGENCY' => '#f55',
|
'EMERGENCY' => '#f55',
|
||||||
|
'DEPRECATED' => '#f50',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -2,43 +2,101 @@
|
|||||||
namespace ProVM\Logview\Log;
|
namespace ProVM\Logview\Log;
|
||||||
|
|
||||||
use Generator;
|
use Generator;
|
||||||
use ProVM\Logview\Log;
|
use DateTimeInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use ProVM\Common\Define\Parser;
|
||||||
|
|
||||||
class File
|
class File
|
||||||
{
|
{
|
||||||
|
protected LoggerInterface $logger;
|
||||||
|
protected Parser $parser;
|
||||||
|
protected string $fullname;
|
||||||
protected string $filename;
|
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
|
public function getFilename(): string
|
||||||
{
|
{
|
||||||
return $this->filename;
|
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
|
public function setFilename(string $filename): File
|
||||||
{
|
{
|
||||||
$this->filename = $filename;
|
$this->filename = $filename;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
public function setContent(string $content): File
|
public function setDate(DateTimeInterface $dateTime): File
|
||||||
{
|
{
|
||||||
$this->content = $content;
|
$this->dateTime = $dateTime;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLogs(): array
|
public function getTotal(): int
|
||||||
{
|
{
|
||||||
$lines = explode(PHP_EOL, $this->getContent());
|
return $this->getParser()->total($this->getFullname());
|
||||||
$logs = [];
|
}
|
||||||
foreach ($lines as $line) {
|
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) === '') {
|
if (trim($line) === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$logs []= Log::parse($line);
|
yield $this->getParser()->parse(trim($line));
|
||||||
|
$cnt ++;
|
||||||
|
if ($cnt > $f) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return array_reverse($logs);
|
\Safe\fclose($fh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
30
app/src/Parser/Access.php
Normal file
30
app/src/Parser/Access.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
namespace ProVM\Logview\Parser;
|
||||||
|
|
||||||
|
use Safe\DateTimeImmutable;
|
||||||
|
use ProVM\Common\Define\Log;
|
||||||
|
use ProVM\Common\Implement\Parser;
|
||||||
|
use Safe\Exceptions\DatetimeException;
|
||||||
|
|
||||||
|
class Access extends Parser
|
||||||
|
{
|
||||||
|
public function parse(string $content): Log
|
||||||
|
{
|
||||||
|
$log = parent::parse($content);
|
||||||
|
$regex = "/(?<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[(?<date>\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2} [\+|\-]\d{4})\] (?<message>.*)/";
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
8
app/src/Parser/Basic.php
Normal file
8
app/src/Parser/Basic.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
namespace ProVM\Logview\Parser;
|
||||||
|
|
||||||
|
use ProVM\Common\Implement\Parser;
|
||||||
|
|
||||||
|
class Basic extends Parser
|
||||||
|
{
|
||||||
|
}
|
8
app/src/Parser/Error.php
Normal file
8
app/src/Parser/Error.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
namespace ProVM\Logview\Parser;
|
||||||
|
|
||||||
|
use ProVM\Common\Implement\Parser;
|
||||||
|
|
||||||
|
class Error extends Parser
|
||||||
|
{
|
||||||
|
}
|
84
app/src/Parser/Monolog.php
Normal file
84
app/src/Parser/Monolog.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
namespace ProVM\Logview\Parser;
|
||||||
|
|
||||||
|
use Safe\DateTimeImmutable;
|
||||||
|
use ProVM\Common\Define\Log;
|
||||||
|
use ProVM\Common\Implement\Parser;
|
||||||
|
|
||||||
|
class Monolog extends Parser
|
||||||
|
{
|
||||||
|
public function total(string $filename): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$regex = "/\[(?P<date>\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<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}(?:\+|-)\d{2}:\d{2})\]",
|
||||||
|
"\s(?<channel>\w*)",
|
||||||
|
"\.(?<severity>\w*)",
|
||||||
|
":\s(?<message>.*)",
|
||||||
|
"\s(?:\[|\{)(?<context>.*)(?:\]|\})",
|
||||||
|
"\s(?:\{|\[)(?<extra>.*)(?:\}|\])"
|
||||||
|
];
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
54
app/src/Parser/PHPDefault.php
Normal file
54
app/src/Parser/PHPDefault.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
namespace ProVM\Logview\Parser;
|
||||||
|
|
||||||
|
use Safe\DateTimeImmutable;
|
||||||
|
use ProVM\Common\Define\Log;
|
||||||
|
use ProVM\Common\Implement\Parser;
|
||||||
|
|
||||||
|
class PHPDefault extends Parser
|
||||||
|
{
|
||||||
|
public function total(string $filename): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$regex = "/\[(?<date>\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 = "/\[(?<date>\d{2}-\w{3}-\d{4}\s\d{2}:\d{2}:\d{2}\s\w{3})\]\s(?<level>PHP|User)\s(?<severity>\w+):\s(?<message>.*)/";
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -6,11 +6,12 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- app
|
- app
|
||||||
image: nginx
|
image: nginx
|
||||||
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${WEB_PORT:-8030}:80"
|
- "${WEB_PORT:-8030}:80"
|
||||||
volumes:
|
volumes:
|
||||||
- "./nginx.conf:/etc/nginx/conf.d/default.conf"
|
- "./nginx.conf:/etc/nginx/conf.d/default.conf"
|
||||||
- "./src:/app"
|
- "./app:/app"
|
||||||
- "./logs:/logs"
|
- "./logs:/logs"
|
||||||
|
|
||||||
php:
|
php:
|
||||||
@ -18,6 +19,7 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- app
|
- app
|
||||||
build: .
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- "./src:/app"
|
- "./app:/app"
|
||||||
- "./logs:/logs"
|
- "./logs:/logs"
|
||||||
|
Reference in New Issue
Block a user