develop (#45)
Co-authored-by: Juan Pablo Vial <jpvialb@incoviba.cl> Reviewed-on: #45
This commit is contained in:
48
app/common/Implement/Log/Formatter/PDO.php
Normal file
48
app/common/Implement/Log/Formatter/PDO.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace Incoviba\Common\Implement\Log\Formatter;
|
||||
|
||||
use Throwable;
|
||||
use Monolog\Formatter\JsonFormatter;
|
||||
use Monolog\LogRecord;
|
||||
|
||||
class PDO extends JsonFormatter
|
||||
{
|
||||
public function __construct(int $batchMode = self::BATCH_MODE_JSON, bool $appendNewline = false, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = true)
|
||||
{
|
||||
parent::__construct($batchMode, $appendNewline, $ignoreEmptyContextAndExtra, $includeStacktraces);
|
||||
}
|
||||
|
||||
public function format(LogRecord $record): string
|
||||
{
|
||||
if (is_a($record->message, Throwable::class)) {
|
||||
$exception = $record->message;
|
||||
$message = $this->normalizeException($exception);
|
||||
$context = $record->context;
|
||||
$context['exception'] = $exception;
|
||||
if ($exception->getPrevious()) {
|
||||
$context['previous'] = $this->walkException($exception);
|
||||
}
|
||||
$new_record = new LogRecord(
|
||||
$record->datetime,
|
||||
$record->channel,
|
||||
$record->level,
|
||||
json_encode($message),
|
||||
$context,
|
||||
$record->extra
|
||||
);
|
||||
$record = $new_record;
|
||||
}
|
||||
$normalized = $this->normalize($record, $this->maxNormalizeDepth);
|
||||
return $normalized['message'];
|
||||
}
|
||||
|
||||
protected function walkException(Throwable $exception, int $depth = 0): array
|
||||
{
|
||||
$output = [];
|
||||
$currentDepth = $depth;
|
||||
while ($previous = $exception->getPrevious() and $currentDepth < $this->maxNormalizeDepth) {
|
||||
$output []= $this->normalizeException($previous);
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
116
app/common/Implement/Log/Handler/MySQL.php
Normal file
116
app/common/Implement/Log/Handler/MySQL.php
Normal file
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
namespace Incoviba\Common\Implement\Log\Handler;
|
||||
|
||||
use Incoviba\Common\Define\Connection;
|
||||
use Monolog\Handler\AbstractProcessingHandler;
|
||||
use Monolog\Level;
|
||||
use Monolog\LogRecord;
|
||||
use PDOException;
|
||||
use PDOStatement;
|
||||
|
||||
class MySQL extends AbstractProcessingHandler
|
||||
{
|
||||
private bool $initialized = false;
|
||||
private array $tables = [
|
||||
'default' => 'monolog',
|
||||
'deprecated' => 'monolog_deprecated'
|
||||
];
|
||||
private array $statements = [
|
||||
'default' => null,
|
||||
'deprecated' => null
|
||||
];
|
||||
private array $baseQueries = [
|
||||
'check' => "SHOW TABLES LIKE '%s'",
|
||||
'create' => "CREATE TABLE IF NOT EXISTS %s (channel VARCHAR(255), level VARCHAR(100), message LONGTEXT, time DATETIME, context LONGTEXT, extra LONGTEXT)",
|
||||
'insert' => "INSERT INTO %s (channel, level, message, time, context, extra) VALUES (:channel, :level, :message, :time, :context, :extra)",
|
||||
'delete' => "DELETE FROM %s WHERE time < DATE_SUB(CURDATE(), INTERVAL %d DAY)"
|
||||
];
|
||||
|
||||
public function __construct(protected Connection $connection, protected int $retainDays = 90,
|
||||
int|string|Level $level = Level::Debug, bool $bubble = true)
|
||||
{
|
||||
parent::__construct($level, $bubble);
|
||||
}
|
||||
public function write(LogRecord $record): void
|
||||
{
|
||||
if (!$this->initialized) {
|
||||
if (!$this->checkTablesExist()) {
|
||||
$this->createTables();
|
||||
}
|
||||
$this->cleanup();
|
||||
$this->initialized();
|
||||
}
|
||||
if (str_contains(strtolower($record->message), 'deprecated:')) {
|
||||
$this->statements['deprecated']->execute([
|
||||
'channel' => $record->channel,
|
||||
'level' => $record->level->getName(),
|
||||
'message' => $record->formatted,
|
||||
'time' => $record->datetime->format('Y-m-d H:i:s.u'),
|
||||
'context' => (count($record->context) > 0) ? json_encode($record->context, JSON_UNESCAPED_SLASHES) : '',
|
||||
'extra' => (count($record->extra) > 0) ? json_encode($record->extra, JSON_UNESCAPED_SLASHES) : ''
|
||||
]);
|
||||
return;
|
||||
}
|
||||
$this->statements['default']->execute([
|
||||
'channel' => $record->channel,
|
||||
'level' => $record->level->getName(),
|
||||
'message' => $record->formatted,
|
||||
'time' => $record->datetime->format('Y-m-d H:i:s.u'),
|
||||
'context' => (count($record->context) > 0) ? json_encode($record->context, JSON_UNESCAPED_SLASHES) : '',
|
||||
'extra' => (count($record->extra) > 0) ? json_encode($record->extra, JSON_UNESCAPED_SLASHES) : ''
|
||||
]);
|
||||
}
|
||||
|
||||
private function initialized(): void
|
||||
{
|
||||
foreach ($this->tables as $type => $table) {
|
||||
$query = sprintf($this->baseQueries['insert'], $table);
|
||||
$this->statements[$type] = $this->connection->getPDO()->prepare($query);
|
||||
}
|
||||
$this->initialized = true;
|
||||
}
|
||||
private function checkTablesExist(): bool
|
||||
{
|
||||
return array_all($this->tables, fn($table) => $this->checkTableExists($table));
|
||||
}
|
||||
private function checkTableExists(string $table): bool
|
||||
{
|
||||
$query = sprintf($this->baseQueries['check'], $table);
|
||||
try {
|
||||
$result = $this->connection->query($query);
|
||||
} catch (PDOException) {
|
||||
return false;
|
||||
}
|
||||
return $result->rowCount() > 0;
|
||||
}
|
||||
private function createTables(): void
|
||||
{
|
||||
foreach ($this->tables as $table) {
|
||||
if (!$this->checkTableExists($table)) {
|
||||
$this->createTable($table);
|
||||
}
|
||||
}
|
||||
}
|
||||
private function createTable(string $table): void
|
||||
{
|
||||
$query = sprintf($this->baseQueries['create'], $table);
|
||||
try {
|
||||
$result = $this->connection->getPDO()->exec($query);
|
||||
if ($result === false) {
|
||||
throw new PDOException('Failed to create table: ' . $table);
|
||||
}
|
||||
} catch (PDOException) {}
|
||||
}
|
||||
private function cleanup(): void
|
||||
{
|
||||
foreach ($this->tables as $table) {
|
||||
$query = sprintf($this->baseQueries['delete'], $table, $this->retainDays);
|
||||
try {
|
||||
$result = $this->connection->getPDO()->query($query);
|
||||
if ($result === false) {
|
||||
throw new PDOException('Failed to delete from table: ' . $table);
|
||||
}
|
||||
} catch (PDOException) {}
|
||||
}
|
||||
}
|
||||
}
|
140
app/common/Implement/Log/Processor/ArrayBuilder.php
Normal file
140
app/common/Implement/Log/Processor/ArrayBuilder.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
namespace Incoviba\Common\Implement\Log\Processor;
|
||||
|
||||
use DateInvalidTimeZoneException;
|
||||
use DateMalformedStringException;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Monolog\Formatter;
|
||||
use Monolog\Handler;
|
||||
use Monolog\Level;
|
||||
use Predis;
|
||||
use Incoviba;
|
||||
use Throwable;
|
||||
|
||||
class ArrayBuilder
|
||||
{
|
||||
public function __construct(protected ContainerInterface $container) {}
|
||||
|
||||
public function build(array $data): array
|
||||
{
|
||||
$handlers = [];
|
||||
foreach ($data as $handlerData) {
|
||||
if (in_array($handlerData['handler'], [Handler\StreamHandler::class, Handler\RotatingFileHandler::class,])) {
|
||||
$params = [
|
||||
"/logs/{$handlerData['filename']}",
|
||||
];
|
||||
if ($handlerData['handler'] === Handler\RotatingFileHandler::class) {
|
||||
$params []= 10;
|
||||
}
|
||||
try {
|
||||
$formatter = Formatter\LineFormatter::class;
|
||||
if (array_key_exists('formatter', $handlerData)) {
|
||||
$formatter = $handlerData['formatter'];
|
||||
}
|
||||
$handler = new $handlerData['handler'](...$params)
|
||||
->setFormatter($this->container->get($formatter));
|
||||
} catch (NotFoundExceptionInterface | ContainerExceptionInterface $exception) {
|
||||
$this->log($exception, ['handlerData' => $handlerData]);
|
||||
continue;
|
||||
}
|
||||
} elseif ($handlerData['handler'] === Incoviba\Common\Implement\Log\Handler\MySQL::class) {
|
||||
try {
|
||||
$params = [
|
||||
$this->container->get(Incoviba\Common\Define\Connection::class)
|
||||
];
|
||||
$formatter = Incoviba\Common\Implement\Log\Formatter\PDO::class;
|
||||
if (array_key_exists('formatter', $handlerData)) {
|
||||
$formatter = $handlerData['formatter'];
|
||||
}
|
||||
$handler = new $handlerData['handler'](...$params)
|
||||
->setFormatter($this->container->get($formatter));
|
||||
} catch (NotFoundExceptionInterface | ContainerExceptionInterface $exception) {
|
||||
$this->log($exception, ['handlerData' => $handlerData]);
|
||||
continue;
|
||||
}
|
||||
} elseif ($handlerData['handler'] === Handler\RedisHandler::class) {
|
||||
try {
|
||||
$params = [
|
||||
$this->container->get(Predis\ClientInterface::class),
|
||||
"logs:{$handlerData['name']}",
|
||||
'capSize' => $handlerData['capSize'] ?? 100
|
||||
];
|
||||
} catch (NotFoundExceptionInterface | ContainerExceptionInterface $exception) {
|
||||
$this->log($exception, ['handlerData' => $handlerData]);
|
||||
continue;
|
||||
}
|
||||
$handler = new $handlerData['handler'](...$params);
|
||||
}
|
||||
if (!isset($handler)) {
|
||||
$this->log("Invalid handler", ['handlerData' => $handlerData]);
|
||||
continue;
|
||||
}
|
||||
$params = [
|
||||
$handler,
|
||||
];
|
||||
if (is_array($handlerData['levels'])) {
|
||||
foreach ($handlerData['levels'] as $level) {
|
||||
$params []= $level;
|
||||
}
|
||||
} else {
|
||||
$params []= $handlerData['levels'];
|
||||
$params []= Level::Emergency;
|
||||
}
|
||||
$params []= false;
|
||||
$handlers []= new Handler\FilterHandler(...$params);
|
||||
}
|
||||
return $handlers;
|
||||
}
|
||||
|
||||
protected function log(string|Throwable $message, array $context = []): void
|
||||
{
|
||||
try {
|
||||
$dateTime = new DateTimeImmutable('now', new DateTimeZone($_ENV['TZ'] ?? 'America/Santiago'));
|
||||
} catch (DateMalformedStringException | DateInvalidTimeZoneException $exception) {
|
||||
$dateTime = new DateTimeImmutable();
|
||||
}
|
||||
if (is_a($message, Throwable::class)) {
|
||||
$exception = $message;
|
||||
$message = $exception->getMessage();
|
||||
}
|
||||
$context = json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
if ($context === false) {
|
||||
$context = '[]';
|
||||
}
|
||||
$extra = [];
|
||||
$extra['from'] = __FILE__;
|
||||
if (isset($exception)) {
|
||||
$extra['file'] = $exception->getFile();
|
||||
$extra['line'] = $exception->getLine();
|
||||
$extra['trace'] = $exception->getTrace();
|
||||
}
|
||||
$extra = json_encode($extra, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
$code = 0;
|
||||
if (isset($exception)) {
|
||||
$code = $exception->getCode();
|
||||
}
|
||||
if ($extra === false) {
|
||||
$extra = '[]';
|
||||
}
|
||||
$output = "[{$dateTime->format('Y-m-d H:i:s P')}] [{$code}] {$message} {$context} {$extra}";
|
||||
$filename = '/logs/error.json';
|
||||
$fileContents = [];
|
||||
if (file_exists($filename)) {
|
||||
$fileContents = file_get_contents($filename);
|
||||
$fileContents = json_decode($fileContents, true);
|
||||
if ($fileContents === false) {
|
||||
$fileContents = [];
|
||||
}
|
||||
}
|
||||
$fileContents[$dateTime->getTimestamp()] = $output;
|
||||
$fileContents = json_encode($fileContents, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
if ($fileContents === false) {
|
||||
$fileContents = '[]';
|
||||
}
|
||||
file_put_contents($filename, $fileContents);
|
||||
}
|
||||
}
|
68
app/common/Implement/Log/Processor/Exception.php
Normal file
68
app/common/Implement/Log/Processor/Exception.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
namespace Incoviba\Common\Implement\Log\Processor;
|
||||
|
||||
use Throwable;
|
||||
use Monolog\LogRecord;
|
||||
use Monolog\Processor\ProcessorInterface;
|
||||
|
||||
class Exception implements ProcessorInterface
|
||||
{
|
||||
public function __invoke(LogRecord $record): LogRecord
|
||||
{
|
||||
$context = $record->context;
|
||||
$changed = false;
|
||||
array_walk_recursive($context, function (&$item) use (&$changed) {
|
||||
if (is_a($item, Throwable::class)) {
|
||||
$item = $this->processException($item);
|
||||
$changed = true;
|
||||
}
|
||||
});
|
||||
if ($changed) {
|
||||
$new_record = new LogRecord(
|
||||
$record->datetime,
|
||||
$record->channel,
|
||||
$record->level,
|
||||
$record->message,
|
||||
$context,
|
||||
$record->extra
|
||||
);
|
||||
$record = $new_record;
|
||||
}
|
||||
if (is_a($record->message, Throwable::class)) {
|
||||
$exception = $record->message;
|
||||
$output = $this->processException($exception);
|
||||
$message = $output['message'];
|
||||
if (array_key_exists('exception', $context)) {
|
||||
$context['other_exception'] = $context['exception'];
|
||||
}
|
||||
$context['exception'] = $output;
|
||||
$new_record = new LogRecord(
|
||||
$record->datetime,
|
||||
$record->channel,
|
||||
$record->level,
|
||||
$message,
|
||||
$context,
|
||||
$record->extra
|
||||
);
|
||||
$record = $new_record;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
protected function processException(Throwable $exception): array
|
||||
{
|
||||
$output = [
|
||||
'class' => get_class($exception),
|
||||
'code' => $exception->getCode(),
|
||||
'message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $exception->getTraceAsString(),
|
||||
];
|
||||
if ($exception->getPrevious() !== null) {
|
||||
$output['previous'] = $this->processException($exception);
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
18
app/common/Implement/Log/Processor/User.php
Normal file
18
app/common/Implement/Log/Processor/User.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace Incoviba\Common\Implement\Log\Processor;
|
||||
|
||||
use Incoviba\Service;
|
||||
use Monolog\LogRecord;
|
||||
use Monolog\Processor\ProcessorInterface;
|
||||
|
||||
class User implements ProcessorInterface
|
||||
{
|
||||
public function __construct(protected Service\Login $loginService) {}
|
||||
public function __invoke(LogRecord $record): LogRecord
|
||||
{
|
||||
if ($this->loginService->isIn()) {
|
||||
$record->extra['user'] = $this->loginService->getUser()->name;
|
||||
}
|
||||
return $record;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user