[FrameworkBundle][Monolog] Added a new way to follow logs

This commit is contained in:
Grégoire Pineau 2016-12-30 15:22:46 +01:00
parent 323529cba6
commit ac92375ddb
4 changed files with 289 additions and 1 deletions

View File

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

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

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

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