Reverse order and multiple log files handler

This commit is contained in:
2023-02-15 17:45:23 -03:00
parent 110f37e4f4
commit c29eece81c
16 changed files with 494 additions and 101 deletions

View File

@ -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');
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace ProVM\Common\Define;
interface Log
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace ProVM\Common\Define;
interface Parser
{
public function parse(string $content): Log;
}

View 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);
}
}

View File

@ -5,6 +5,8 @@ use DateTimeImmutable;
use SplFileInfo;
use Psr\Log\LoggerInterface;
use ProVM\Logview\Log\File;
use ProVM\Common\Define\Parser;
use ProVM\Logview\Parser as Parsers;
class Logs
{
@ -50,15 +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
{
$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()));
}
}

View File

@ -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']);
});

View File

@ -4,53 +4,8 @@
<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">
@foreach($log->getLogs() as $line)
<div class="title">
<i class="dropdown icon"></i>
<span class="{{strtolower($line->getSeverity())}}" style="padding-left: 1ex;padding-right: 1ex;">
[{{$line->getDate()->format('Y-m-d H:i:s.u')}}] {{$line->getSeverity()}}
</span>
</div>
<div class="content">
<div class="ui fluid basic card">
<div class="content">
<div class="header">
<span class="{{strtolower($line->getSeverity())}}" style="padding: 1ex 1em;">
<i class="bug icon"></i>
{{$line->getChannel()}}.{{$line->getSeverity()}}
</span>
</div>
</div>
<div class="content">
<div class="description">
{{$line->getMessage()}}
</div>
</div>
@if ($line->hasStack())
<div class="content">
<div class="ui small feed">
@foreach ($line->getStack() as $stack)
<div class="event">
<div class="content">
{{$stack}}
</div>
</div>
@endforeach
</div>
</div>
@endif
@if ($line->hasContext())
<div class="extra content">
{{$line->getContext()}}
<div class="meta">{{$line->getExtra()}}</div>
</div>
@endif
</div>
</div>
@endforeach
</div>
<div class="ui accordion" id="logs"></div>
<hr id="watch" style="border: none;" />
</div>
@endsection
@ -65,7 +20,134 @@
@push('page_scripts')
<script type="text/javascript">
const logs = {
id: '',
start: 0,
amount: 10,
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)
})
},
unparsed: (parent, log) => {
const title = $('<div></div>').addClass('title')
.append($('<i></i>').addClass('dropdown icon'))
const content = $('<div></div>').addClass('content')
title.append(log.original)
content.html(log.original)
parent.append(title).append(content)
},
parsed: (parent, log) => {
const title = $('<div></div>').addClass('title')
.append($('<i></i>').addClass('dropdown icon'))
const content = $('<div></div>').addClass('content')
title.append(
$('<span></span>')
.addClass(log.severity.toLowerCase()).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()
$('.accordion').accordion()
const ob = new IntersectionObserver(this.watch().more)
const watch = document.querySelector('#watch')
ob.observe(watch)
}
}
$(document).ready(() => {
logs.setup('logs')
})
</script>
@endpush

View File

@ -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;
}
];

View File

@ -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<date>.*)\]\s(?<channel>\w*)\.(?<severity>\w*):\s(?<message>.*)\s[\[|\{](?<context>.*)[\]|\}]\s\[(?<extra>.*)\]/";
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',
];
}

View File

@ -1,21 +1,31 @@
<?php
namespace ProVM\Logview\Log;
use Generator;
use DateTimeInterface;
use Psr\Log\LoggerInterface;
use ProVM\Logview\Log;
use ProVM\Common\Define\Parser;
class File
{
protected LoggerInterface $logger;
protected Parser $parser;
protected string $fullname;
protected string $filename;
protected DateTimeInterface $dateTime;
protected string $content;
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;
@ -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);
}
}

30
app/src/Parser/Access.php Normal file
View 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
View 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
View File

@ -0,0 +1,8 @@
<?php
namespace ProVM\Logview\Parser;
use ProVM\Common\Implement\Parser;
class Error extends Parser
{
}

View File

@ -0,0 +1,81 @@
<?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);
//$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>.*)(?:\}|\])/";
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;
}
}

View 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;
}
}

View File

@ -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"