FestinHegre/vendor/symfony/monolog-bridge/Command/ServerLogCommand.php
2024-09-26 17:26:04 +02:00

172 lines
5.9 KiB
PHP

<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Monolog\Command;
use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\HandlerInterface;
use Monolog\Level;
use Monolog\LogRecord;
use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter;
use Symfony\Bridge\Monolog\Handler\ConsoleHandler;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
#[AsCommand(name: 'server:log', description: 'Start a log server that displays logs in real time')]
class ServerLogCommand extends Command
{
private const BG_COLOR = ['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow'];
private ExpressionLanguage $el;
private HandlerInterface $handler;
public function isEnabled(): bool
{
if (!class_exists(ConsoleFormatter::class)) {
return false;
}
// based on a symfony/symfony package, it crashes due a missing FormatterInterface from monolog/monolog
if (!interface_exists(FormatterInterface::class)) {
return false;
}
return parent::isEnabled();
}
protected function configure(): void
{
if (!class_exists(ConsoleFormatter::class)) {
return;
}
$this
->addOption('host', null, InputOption::VALUE_REQUIRED, 'The server host', '0.0.0.0:9911')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The line format', ConsoleFormatter::SIMPLE_FORMAT)
->addOption('date-format', null, InputOption::VALUE_REQUIRED, 'The date format', ConsoleFormatter::SIMPLE_DATE)
->addOption('filter', null, InputOption::VALUE_REQUIRED, 'An expression to filter log. Example: "level > 200 or channel in [\'app\', \'doctrine\']"')
->setHelp(<<<'EOF'
<info>%command.name%</info> starts a log server to display in real time the log
messages generated by your application:
<info>php %command.full_name%</info>
To filter the log messages using any ExpressionLanguage compatible expression, use the <comment>--filter</> option:
<info>php %command.full_name% --filter="level > 200 or channel in ['app', 'doctrine']"</info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$filter = $input->getOption('filter');
if ($filter) {
if (!class_exists(ExpressionLanguage::class)) {
throw new LogicException('Package "symfony/expression-language" is required to use the "filter" option. Try running "composer require symfony/expression-language".');
}
$this->el = new ExpressionLanguage();
}
$this->handler = new ConsoleHandler($output, true, [
OutputInterface::VERBOSITY_NORMAL => Level::Debug,
]);
$this->handler->setFormatter(new ConsoleFormatter([
'format' => str_replace('\n', "\n", $input->getOption('format')),
'date_format' => $input->getOption('date-format'),
'colors' => $output->isDecorated(),
'multiline' => OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity(),
]));
if (!str_contains($host = $input->getOption('host'), '://')) {
$host = 'tcp://'.$host;
}
if (!$socket = stream_socket_server($host, $errno, $errstr)) {
throw new RuntimeException(sprintf('Server start failed on "%s": ', $host).$errstr.' '.$errno);
}
foreach ($this->getLogs($socket) as $clientId => $message) {
$record = unserialize(base64_decode($message));
// Impossible to decode the message, give up.
if (false === $record) {
continue;
}
if ($filter && !$this->el->evaluate($filter, $record)) {
continue;
}
$this->displayLog($output, $clientId, $record);
}
return 0;
}
private function getLogs($socket): iterable
{
$sockets = [(int) $socket => $socket];
$write = [];
while (true) {
$read = $sockets;
stream_select($read, $write, $write, null);
foreach ($read as $stream) {
if ($socket === $stream) {
$stream = stream_socket_accept($socket);
$sockets[(int) $stream] = $stream;
} elseif (feof($stream)) {
unset($sockets[(int) $stream]);
fclose($stream);
} else {
yield (int) $stream => fgets($stream);
}
}
}
}
private function displayLog(OutputInterface $output, int $clientId, array $record): void
{
if (isset($record['log_id'])) {
$clientId = unpack('H*', $record['log_id'])[1];
}
$logBlock = sprintf('<bg=%s> </>', self::BG_COLOR[$clientId % 8]);
$output->write($logBlock);
$record = new LogRecord(
$record['datetime'],
$record['channel'],
Level::fromValue($record['level']),
$record['message'],
// We wrap context and extra, because they have been already dumped.
// So they are instance of Symfony\Component\VarDumper\Cloner\Data
// But LogRecord expects array
['data' => $record['context']],
['data' => $record['extra']],
);
$this->handler->handle($record);
}
}