forkbb/app/Core/LogViewer.php
2023-10-14 22:31:33 +07:00

275 lines
7.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* This file is part of the ForkBB <https://github.com/forkbb>.
*
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
* @license The MIT License (MIT)
*/
declare(strict_types=1);
namespace ForkBB\Core;
use Psr\SimpleCache\CacheInterface;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use RuntimeException;
class LogViewer
{
const CACHE_KEY = 'logs_info';
protected string $dir;
protected string $namePattern;
protected string $linePattern;
protected string $typePattern;
protected array $fileList;
protected array $hashList;
protected array $replName = [
'.' => '\\.',
'*' => '.*',
'?' => '.',
'%' => '\\%',
];
protected array $replLine = [
'[' => '\\[',
']' => '\\]',
'%datetime%' => '(?P<datetime>(?=\S)[a-z0-9,\.:/ -]+)',
'%level_name%' => '(?P<level_name>(?:emergency|alert|critical|error|warning|notice|info|debug))',
'%message%' => '(?P<message>.*?)',
'%context%' => '(?P<context>(?:\[.*?\]|{.*?}))',
];
public function __construct(array $config, protected CacheInterface $cache)
{
$this->dir = \rtrim(\realpath($config['dir'] ?? __DIR__ . '/../log'), '\\/');
$this->namePattern = $this->toNamePattern($config['pattern'] ?? '*.log');
$this->fileList = $this->getFileList();
$this->setPatterns($config['lineFormat'] ?? "%datetime% [%level_name%] %message%\t%context%\n");
}
protected function setPatterns(string $format): void
{
$pos = \strpos($format, '%level_name%');
if (false === $pos) {
throw new RuntimeException('Missing log level in log format');
}
$pos = $pos + 12;
while (\preg_match('%[^a-z0-9\%\\\]%', $format[$pos])) {
++$pos;
}
$typeFormat = \substr($format, 0, $pos);
$this->typePattern = '%^' . \strtr($typeFormat, $this->replLine) . '%i';
$this->linePattern = '%^' . \strtr($format, $this->replLine) . '%i';
}
protected function toNamePattern(string $pattern): string
{
return '%^' . \strtr($pattern, $this->replName) . '$%i';
}
protected function getFileList(): array
{
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->dir, FilesystemIterator::SKIP_DOTS)
);
$files = new RegexIterator($iterator, $this->namePattern, RegexIterator::MATCH);
$result = [];
foreach ($files as $file) {
$result[$file->getRealPath()] = $file->getMTime();
}
\arsort($result, \SORT_NUMERIC);
$this->setHashList($result);
return $result;
}
protected function setHashList(array $fileList): void
{
$this->hashList = [];
foreach ($fileList as $name => $time) {
$this->hashList[\sha1($name)] = $name;
}
}
/**
* Возвращает список логов в виде:
* 'реальный путь до файла' => 'время последнего изменения', ...
*/
public function files(): array
{
return $this->fileList;
}
/**
* Возвращает путь к логу по его хэшу
*/
public function getPath(string $hash): ?string
{
return $this->hashList[$hash] ?? null;
}
/**
* Возвращает имя лога
*/
public function getName(string $path): string
{
if (! \preg_match('%[\\\/]([^\\\/]++)$%D', $path, $matches)) {
throw new RuntimeException("Can't extract filename from path '{$path}'");
}
return $matches[1];
}
/**
* Возвращает общую информацию по логам
* и генерирует кеш
*/
public function info(array $files): array
{
$result = [];
$cache = $this->cache->get(self::CACHE_KEY, []);
foreach ($files as $key1 => $key2) {
$key = \is_string($key1) ? $key1 : $key2;
if (! isset($this->fileList[$key])) {
continue;
}
$hash = \sha1($key);
if (
isset($cache[$hash])
&& $cache[$hash]['time'] === $this->fileList[$key]
) {
$result[$hash] = $cache[$hash]['data'];
} else {
$result[$hash] = $this->generateInfo($key);
$cache[$hash] = [
'time' => $this->fileList[$key],
'data' => $result[$hash],
];
}
}
$cache = \array_intersect_key($cache, $this->hashList);
$this->cache->set(self::CACHE_KEY, $cache);
return $result;
}
protected function generateInfo(string $file): array
{
$result = [
'log_name' => $this->getName($file),
'emergency' => 0,
'alert' => 0,
'critical' => 0,
'error' => 0,
'warning' => 0,
'notice' => 0,
'info' => 0,
'debug' => 0,
];
$handle = \fopen($file, 'rb');
if ($handle) {
$contents = '';
while (! \feof($handle)) {
$contents .= \fread($handle, 8192);
$contents = \str_replace("\r\n", "\n", $contents);
$contents = \str_replace("\r", "\n", $contents);
while (false !== ($pos = \strpos($contents, "\n"))) {
$line = \substr($contents, 0, $pos + 1);
$contents = \substr($contents, $pos + 1);
if (\preg_match($this->typePattern, $line, $matches)) {
++$result[\strtolower($matches['level_name'])];
}
}
}
\fclose($handle);
}
return $result;
}
public function parse(string $file): array
{
$result = [];
$handle = \fopen($file, 'rb');
if ($handle) {
$contents = '';
$current = '';
$matches1 = [];
while (! \feof($handle)) {
$contents .= \fread($handle, 8192);
$contents = \str_replace("\r\n", "\n", $contents);
$contents = \str_replace("\r", "\n", $contents);
while (false !== ($pos = \strpos($contents, "\n"))) {
$line = \substr($contents, 0, $pos + 1);
$contents = \substr($contents, $pos + 1);
if (\preg_match($this->typePattern, $line, $matches)) {
if ('' !== $current) {
$result[] = $this->toResult($current, $matches1);
}
$current = $line;
$matches1 = $matches;
} else {
$current .= $line;
}
}
}
\fclose($handle);
}
if ('' !== $current) {
$result[] = $this->toResult($current, $matches1);
}
return $result;
}
protected function toResult(string $current, array $matches1): array
{
if (\preg_match($this->linePattern, $current, $matches2)) {
$result = $this->clearMatches($matches2);
$result['context'] = \json_decode($result['context'], true, 512, \JSON_THROW_ON_ERROR);
return $result;
} else {
return $this->clearMatches($matches1) + [
'message' => 'LOG PARSER ERROR',
'context' => null,
];
}
}
protected function clearMatches(array $matches): array
{
return \array_filter($matches, function($key) {
return \is_string($key);
}, \ARRAY_FILTER_USE_KEY);
}
}