[FrameworkBundle][Monolog] Added a new way to follow logs
This commit is contained in:
parent
323529cba6
commit
ac92375ddb
@ -13,6 +13,7 @@ namespace Symfony\Bridge\Monolog\Formatter;
|
||||
|
||||
use Monolog\Formatter\FormatterInterface;
|
||||
use Monolog\Logger;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\VarDumper\Cloner\Data;
|
||||
use Symfony\Component\VarDumper\Cloner\Stub;
|
||||
use Symfony\Component\VarDumper\Cloner\VarCloner;
|
||||
@ -67,6 +68,9 @@ class ConsoleFormatter implements FormatterInterface
|
||||
if (isset($args[1])) {
|
||||
$options['date_format'] = $args[1];
|
||||
}
|
||||
if (isset($args[2])) {
|
||||
$options['multiline'] = $args[2];
|
||||
}
|
||||
}
|
||||
|
||||
$this->options = array_replace(array(
|
||||
@ -175,7 +179,10 @@ class ConsoleFormatter implements FormatterInterface
|
||||
|
||||
$replacements = array();
|
||||
foreach ($context as $k => $v) {
|
||||
$replacements['{'.$k.'}'] = sprintf('<comment>%s</>', $this->dumpData($v, false));
|
||||
// Remove quotes added by the dumper around string.
|
||||
$v = trim($this->dumpData($v, false), '"');
|
||||
$v = OutputFormatter::escape($v);
|
||||
$replacements['{'.$k.'}'] = sprintf('<comment>%s</>', $v);
|
||||
}
|
||||
|
||||
$record['message'] = strtr($message, $replacements);
|
||||
|
45
src/Symfony/Bridge/Monolog/Formatter/VarDumperFormatter.php
Normal file
45
src/Symfony/Bridge/Monolog/Formatter/VarDumperFormatter.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?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\Formatter;
|
||||
|
||||
use Monolog\Formatter\FormatterInterface;
|
||||
use Symfony\Component\VarDumper\Cloner\VarCloner;
|
||||
|
||||
/**
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class VarDumperFormatter implements FormatterInterface
|
||||
{
|
||||
private $cloner;
|
||||
|
||||
public function __construct(VarCloner $cloner = null)
|
||||
{
|
||||
$this->cloner = $cloner ?: new VarCloner();
|
||||
}
|
||||
|
||||
public function format(array $record)
|
||||
{
|
||||
$record['context'] = $this->cloner->cloneVar($record['context']);
|
||||
$record['extra'] = $this->cloner->cloneVar($record['extra']);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
public function formatBatch(array $records)
|
||||
{
|
||||
foreach ($records as $k => $record) {
|
||||
$record[$k] = $this->format($record);
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
}
|
114
src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php
Normal file
114
src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?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\Handler;
|
||||
|
||||
use Monolog\Handler\AbstractHandler;
|
||||
use Monolog\Logger;
|
||||
use Symfony\Bridge\Monolog\Formatter\VarDumperFormatter;
|
||||
|
||||
/**
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class ServerLogHandler extends AbstractHandler
|
||||
{
|
||||
private $host;
|
||||
private $context;
|
||||
private $socket;
|
||||
|
||||
public function __construct($host, $level = Logger::DEBUG, $bubble = true, $context = array())
|
||||
{
|
||||
parent::__construct($level, $bubble);
|
||||
|
||||
if (false === strpos($host, '://')) {
|
||||
$host = 'tcp://'.$host;
|
||||
}
|
||||
|
||||
$this->host = $host;
|
||||
$this->context = stream_context_create($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle(array $record)
|
||||
{
|
||||
if (!$this->isHandling($record)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
set_error_handler(self::class.'::nullErrorHandler');
|
||||
|
||||
try {
|
||||
if (!$this->socket = $this->socket ?: $this->createSocket()) {
|
||||
return false === $this->bubble;
|
||||
}
|
||||
|
||||
$recordFormatted = $this->formatRecord($record);
|
||||
|
||||
if (!fwrite($this->socket, $recordFormatted)) {
|
||||
fclose($this->socket);
|
||||
|
||||
// Let's retry: the persistent connection might just be stale
|
||||
if ($this->socket = $this->createSocket()) {
|
||||
fwrite($this->socket, $recordFormatted);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
return false === $this->bubble;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getDefaultFormatter()
|
||||
{
|
||||
return new VarDumperFormatter();
|
||||
}
|
||||
|
||||
private static function nullErrorHandler()
|
||||
{
|
||||
}
|
||||
|
||||
private function createSocket()
|
||||
{
|
||||
$socket = stream_socket_client($this->host, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT | STREAM_CLIENT_PERSISTENT, $this->context);
|
||||
|
||||
if ($socket) {
|
||||
stream_set_blocking($socket, false);
|
||||
}
|
||||
|
||||
return $socket;
|
||||
}
|
||||
|
||||
private function formatRecord(array $record)
|
||||
{
|
||||
if ($this->processors) {
|
||||
foreach ($this->processors as $processor) {
|
||||
$record = call_user_func($processor, $record);
|
||||
}
|
||||
}
|
||||
|
||||
$recordFormatted = $this->getFormatter()->format($record);
|
||||
|
||||
foreach (array('log_uuid', 'uuid', 'uid') as $key) {
|
||||
if (isset($record['extra'][$key])) {
|
||||
$recordFormatted['log_id'] = $record['extra'][$key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return base64_encode(serialize($recordFormatted))."\n";
|
||||
}
|
||||
}
|
122
src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php
Normal file
122
src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?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\Bundle\WebServerBundle\Command;
|
||||
|
||||
use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter;
|
||||
use Symfony\Bridge\Monolog\Handler\ConsoleHandler;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
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>
|
||||
*/
|
||||
class ServerLogCommand extends Command
|
||||
{
|
||||
private static $bgColor = array('black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow');
|
||||
|
||||
private $el;
|
||||
private $handler;
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('server:log')
|
||||
->setDescription('Start a log server that displays logs in real time')
|
||||
->addOption('host', null, InputOption::VALUE_REQUIRED, 'The server host', '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\']"')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$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.');
|
||||
}
|
||||
$this->el = new ExpressionLanguage();
|
||||
}
|
||||
|
||||
$this->handler = new ConsoleHandler($output);
|
||||
|
||||
$this->handler->setFormatter(new ConsoleFormatter(array(
|
||||
'format' => str_replace('\n', "\n", $input->getOption('format')),
|
||||
'date_format' => $input->getOption('date-format'),
|
||||
'colors' => $output->isDecorated(),
|
||||
'multiline' => OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity(),
|
||||
)));
|
||||
|
||||
if (false === strpos($host = $input->getOption('host'), '://')) {
|
||||
$host = 'tcp://'.$host;
|
||||
}
|
||||
|
||||
if (!$socket = stream_socket_server($host, $errno, $errstr)) {
|
||||
throw new \RuntimeException(sprintf('Server start failed on "%s": %s %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($input, $output, $clientId, $record);
|
||||
}
|
||||
}
|
||||
|
||||
private function getLogs($socket)
|
||||
{
|
||||
$sockets = array((int) $socket => $socket);
|
||||
$write = array();
|
||||
|
||||
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(InputInterface $input, OutputInterface $output, $clientId, array $record)
|
||||
{
|
||||
if ($this->handler->isHandling($record)) {
|
||||
if (isset($record['log_id'])) {
|
||||
$clientId = unpack('H*', $record['log_id'])[1];
|
||||
}
|
||||
$logBlock = sprintf('<bg=%s> </>', self::$bgColor[$clientId % 8]);
|
||||
$output->write($logBlock);
|
||||
}
|
||||
|
||||
$this->handler->handle($record);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user