8000 [FrameworkBundle][Monolog] Added a new way to follow logs · symfony/symfony@ac92375 · GitHub
[go: up one dir, main page]

Skip to content

Commit ac92375

Browse files
committed
[FrameworkBundle][Monolog] Added a new way to follow logs
1 parent 323529c commit ac92375

File tree

4 files changed

+289
-1
lines changed

4 files changed

+289
-1
lines changed

src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Monolog\Formatter\FormatterInterface;
1515
use Monolog\Logger;
16+
use Symfony\Component\Console\Formatter\OutputFormatter;
1617
use Symfony\Component\VarDumper\Cloner\Data;
1718
use Symfony\Component\VarDumper\Cloner\Stub;
1819
use Symfony\Component\VarDumper\Cloner\VarCloner;
@@ -67,6 +68,9 @@ public function __construct($options = array())
6768
if (isset($args[1])) {
6869
$options['date_format'] = $args[1];
6970
}
71+
if (isset($args[2])) {
72+
$options['multiline'] = $args[2];
73+
}
7074
}
7175

7276
$this->options = array_replace(array(
@@ -175,7 +179,10 @@ private function replacePlaceHolder(array $record)
175179

176180
$replacements = array();
177181
foreach ($context as $k => $v) {
178-
$replacements['{'.$k.'}'] = sprintf('<comment>%s</>', $this->dumpData($v, false));
182+
// Remove quotes added by the dumper around string.
183+
$v = trim($this->dumpData($v, false), '"');
184+
$v = OutputFormatter::escape($v);
185+
$replacements['{'.$k.'}'] = sprintf('<comment>%s</>', $v);
179186
}
180187

181188
$record['message'] = strtr($message, $replacements);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Monolog\Formatter;
13+
14+
use Monolog\Formatter\FormatterInterface;
15+
use Symfony\Component\VarDumper\Cloner\VarCloner;
16+
17+
/**
18+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
19+
*/
20+
class VarDumperFormatter implements FormatterInterface
21+
{
22+
private $cloner;
23+
24+
public function __construct(VarCloner $cloner = null)
25+
{
26+
$this->cloner = $cloner ?: new VarCloner();
27+
}
28+
29+
public function format(array $record)
30+
{
31+
$record['context'] = $this->cloner->cloneVar($record['context']);
32+
$record['extra'] = $this->cloner->cloneVar($record['extra']);
33+
34+
return $record;
35+
}
36+
37+
public function formatBatch(array $records)
38+
{
39+
foreach ($records as $k => $record) {
40+
$record[$k] = $this->format($record);
41+
}
42+
43+
return $records;
44+
}
45+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Monolog\Handler;
13+
14+
use Monolog\Handler\AbstractHandler;
15+
use Monolog\Logger;
16+
use Symfony\Bridge\Monolog\Formatter\VarDumperFormatter;
17+
18+
/**
19+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
20+
*/
21+
class ServerLogHandler extends AbstractHandler
22+
{
23+
private $host;
24+
private $context;
25+
private $socket;
26+
27+
public function __construct($host, $level = Logger::DEBUG, $bubble = true, $context = array())
28+
{
29+
parent::__construct($level, $bubble);
30+
31+
if (false === strpos($host, '://')) {
32+
$host = 'tcp://'.$host;
33+
}
34+
35+
$this->host = $host;
36+
$this->context = stream_context_create($context);
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public function handle(array $record)
43+
{
44+
if (!$this->isHandling($record)) {
45+
return false;
46+
}
47+
48+
set_error_handler(self::class.'::nullErrorHandler');
49+
50+
try {
51+
if (!$this->socket = $this->socket ?: $this->createSocket()) {
52+
return false === $this->bubble;
53+
}
54+
55+
$recordFormatted = $this->formatRecord($record);
56+
57+
if (!fwrite($this->socket, $recordFormatted)) {
58+
fclose($this->socket);
59+
60+
// Let's retry: the persistent connection might just be stale
61+
if ($this->socket = $this->createSocket()) {
62+
fwrite($this->socket, $recordFormatted);
63+
}
64+
}
65+
} finally {
66+
restore_error_handler();
67+
}
68+
69+
return false === $this->bubble;
70+
}
71+
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
protected function getDefaultFormatter()
76+
{
77+
return new VarDumperFormatter();
78+
}
79+
80+
private static function nullErrorHandler()
81+
{
82+
}
83+
84+
private function createSocket()
85+
{
86+
$socket = stream_socket_client($this->host, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT | STREAM_CLIENT_PERSISTENT, $this->context);
87+
88+
if ($socket) {
89+
stream_set_blocking($socket, false);
90+
}
91+
92+
return $socket;
93+
}
94+
95+
private function formatRecord(array $record)
96+
{
97+
if ($this->processors) {
98+
foreach ($this->processors as $processor) {
99+
$record = call_user_func($processor, $record);
100+
}
101+
}
102+
103+
$recordFormatted = $this->getFormatter()->format($record);
104+
105+
foreach (array('log_uuid', 'uuid', 'uid') as $key) {
106+
if (isset($record['extra'][$key])) {
107+
$recordFormatted['log_id'] = $record['extra'][$key];
108+
break;
109+
}
110+
}
111+
112+
return base64_encode(serialize($recordFormatted))."\n";
113+
}
114+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\WebServerBundle\Command;
13+
14+
use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter;
15+
use Symfony\Bridge\Monolog\Handler\ConsoleHandler;
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
21+
22+
/**
23+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
24+
*/
25+
class ServerLogCommand extends Command
26+
{
27+
private static $bgColor = array('black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow');
28+
29+
private $el;
30+
private $handler;
31+
32+
protected function configure()
33+
{
34+
$this
35+
->setName('server:log')
36+
->setDescription('Start a log server that displays logs in real time')
37+
->addOption('host', null, InputOption::VALUE_REQUIRED, 'The server host', '0:9911')
38+
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The line format', ConsoleFormatter::SIMPLE_FORMAT)
39+
->addOption('date-format', null, InputOption::VALUE_REQUIRED, 'The date format', ConsoleFormatter::SIMPLE_DATE)
40+
->addOption('filter', null, InputOption::VALUE_REQUIRED, 'An expression to filter log. Example: "level > 200 or channel in [\'app\', \'doctrine\']"')
41+
;
42+
}
43+
44+
protected function execute(InputInterface $input, OutputInterface $output)
45+
{
46+
$filter = $input->getOption('filter');
47+
if ($filter) {
48+
if (!class_exists(ExpressionLanguage::class)) {
49+
throw new \LogicException('Package "symfony/expression-language" is required to use the "filter" option.');
50+
}
51+
$this->el = new ExpressionLanguage();
52+
}
53+
54+
$this->handler = new ConsoleHandler($output);
55+
56+
$this->handler->setFormatter(new ConsoleFormatter(array(
57+
'format' => str_replace('\n', "\n", $input->getOption('format')),
58+
'date_format' => $input->getOption('date-format'),
59+
'colors' => $output->isDecorated(),
60+
'multiline' => OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity(),
61+
)));
62+
63+
if (false === strpos($host = $input->getOption('host'), '://')) {
64+
$host = 'tcp://'.$host;
65+
}
66+
67+
if (!$socket = stream_socket_server($host, $errno, $errstr)) {
68+
throw new \RuntimeException(sprintf('Server start failed on "%s": %s %s.', $host, $errstr, $errno));
69+
}
70+
71+
foreach ($this->getLogs($socket) as $clientId => $message) {
72+
$record = unserialize(base64_decode($message));
73+
74+
// Impossible to decode the message, give up.
75+
if (false === $record) {
76+
continue;
77+
}
78+
79+
if ($filter && !$this->el->evaluate($filter, $record)) {
80+
continue;
81+
}
82+
83+
$this->displayLog($input, $output, $clientId, $record);
84+
}
85+
}
86+
87+
private function getLogs($socket)
88+
{
89+
$sockets = array((int) $socket => $socket);
90+
$write = array();
91+
92+
while (true) {
93+
$read = $sockets;
94+
stream_select($read, $write, $write, null);
95+
96+
foreach ($read as $stream) {
97+
if ($socket === $stream) {
98+
$stream = stream_socket_accept($socket);
99+
$sockets[(int) $stream] = $stream;
100+
} elseif (feof($stream)) {
101+
unset($sockets[(int) $stream]);
102+
fclose($stream);
103+
} else {
104+
yield (int) $stream => fgets($stream);
105+
}
106+
}
107+
}
108+
}
109+
110+
private function displayLog(InputInterface $input, OutputInterface $output, $clientId, array $record)
111+
{
112+
if ($this->handler->isHandling($record)) {
113+
if (isset($record['log_id'])) {
114+
$clientId = unpack('H*', $record['log_id'])[1];
115+
}
116+
$logBlock = sprintf('<bg=%s> </>', self::$bgColor[$clientId % 8]);
117+
$output->write($logBlock);
118+
}
119+
120+
$this->handler->handle($record);
121+
}
122+
}

0 commit comments

Comments
 (0)
0