Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c3a4275

Browse files
committed
[FrameworkBundle][Monolog] Added a new way to follow logs
1 parent a0f1e85 commit c3a4275

File tree

3 files changed

+346
-0
lines changed

3 files changed

+346
-0
lines changed
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 <[email protected]>
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 <[email protected]>
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: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
namespace Symfony\Bridge\Monolog\Handler;
4+
5+
use Monolog\Handler\AbstractHandler;
6+
use Monolog\Logger;
7+
use Symfony\Bridge\Monolog\Formatter\VarDumperFormatter;
8+
9+
class ServerLogHandler extends AbstractHandler
10+
{
11+
private $host;
12+
private $context;
13+
private $socket;
14+
15+
public function __construct($host, $level = Logger::DEBUG, $bubble = true, $context = array())
16+
{
17+
parent::__construct($level, $bubble);
18+
19+
if (false === strpos($host, '://')) {
20+
$host = 'tcp://'.$host;
21+
}
22+
23+
$this->host = $host;
24+
$this->context = stream_context_create($context);
25+
}
26+
27+
/**
28+
* {@inheritdoc}
29+
*/
30+
public function handle(array $record)
31+
{
32+
if (!$this->isHandling($record)) {
33+
return false;
34+
}
35+
36+
set_error_handler(self::class.'::nullErrorHandler');
37+
38+
try {
39+
if (!$this->socket = $this->socket ?: $this->createSocket()) {
40+
return false === $this->bubble;
41+
}
42+
43+
$recordFormatted = $this->formatRecord($record);
44+
45+
if (!fwrite($this->socket, $recordFormatted)) {
46+
fclose($this->socket);
47+
48+
// Let's retry: the persistent connection might just be stale
49+
if ($this->socket = $this->createSocket()) {
50+
fwrite($this->socket, $recordFormatted);
51+
}
52+
}
53+
} finally {
54+
restore_error_handler();
55+
}
56+
57+
return false === $this->bubble;
58+
}
59+
60+
/**
61+
* {@inheritdoc}
62+
*/
63+
protected function getDefaultFormatter()
64+
{
65+
return new VarDumperFormatter();
66+
}
67+
68+
private static function nullErrorHandler()
69+
{
70+
}
71+
72+
private function createSocket()
73+
{
74+
$socket = stream_socket_client($this->host, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT | STREAM_CLIENT_PERSISTENT, $this->context);
75+
76+
if ($socket) {
77+
stream_set_blocking($socket, false);
78+
}
79+
80+
return $socket;
81+
}
82+
83+
private function formatRecord(array $record)
84+
{
85+
if ($this->processors) {
86+
foreach ($this->processors as $processor) {
87+
$record = call_user_func($processor, $record);
88+
}
89+
}
90+
91+
$recordFormatted = $this->getFormatter()->format($record);
92+
93+
foreach (array('log_uuid', 'uuid', 'uid') as $key) {
94+
if (isset($record['extra'][$key])) {
95+
$recordFormatted['log_id'] = $record['extra'][$key];
96+
break;
97+
}
98+
}
99+
100+
return base64_encode(serialize($recordFormatted))."\n";
101+
}
102+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Input\InputOption;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
19+
use Symfony\Component\VarDumper\Cloner\Data;
20+
use Symfony\Component\VarDumper\Dumper\CliDumper;
21+
22+
/**
23+
* @author Grégoire Pineau <[email protected]>
24+
*/
25+
class ServerLogCommand extends Command
26+
{
27+
private $dumper;
28+
private $output;
29+
private $el;
30+
31+
private static $levelColorMap = array(
32+
100 => 'fg=white',
33+
200 => 'fg=green',
34+
250 => 'fg=blue',
35+
300 => 'fg=cyan',
36+
400 => 'fg=yellow',
37+
500 => 'fg=red',
38+
550 => 'fg=red',
39+
600 => 'fg=white;bg=red',
40+
);
41+
42+
private static $bgColor = array(
43+
'black',
44+
'blue',
45+
'cyan',
46+
'green',
47+
'magenta',
48+
'red',
49+
'white',
50+
'yellow',
51+
);
52+
53+
protected function configure()
54+
{
55+
$this
56+
->setName('server:log')
57+
->setDescription('Start a log server that displays logs in real time')
58+
->addOption('host', null, InputOption::VALUE_REQUIRED, 'The server host', '0:9911')
59+
->addOption('date-format', null, InputOption::VALUE_REQUIRED, 'The date format', 'H:i:s')
60+
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The line format', '%s <%s>%-9s</> <options=bold>%-8.8s</> %s')
61+
->addOption('show-context', null, InputOption::VALUE_NONE, 'Display the context')
62+
->addOption('show-extra', null, InputOption::VALUE_NONE, 'Display the extra')
63+
->addOption('filter', null, InputOption::VALUE_REQUIRED, 'An expression to filter log. Example: record[\'level\'] > 200 or record[\'channel\'] in [\'app\', \'doctrine\']"')
64+
;
65+
}
66+
67+
protected function initialize(InputInterface $input, OutputInterface $output)
68+
{
69+
$this->dumper = new CliDumper();
70+
$this->dumper->setOutput($this->output = fopen('php://memory', 'r+b'));
71+
72+
if ($input->getOption('filter')) {
73+
if (!class_exists(ExpressionLanguage::class)) {
74+
throw new \LogicException('Package "symfony/expression-language" is required to use the "filter" option.');
75+
}
76+
$this->el = new ExpressionLanguage();
77+
}
78+
}
79+
80+
protected function execute(InputInterface $input, OutputInterface $output)
81+
{
82+
if (false === strpos($host = $input->getOption('host'), '://')) {
83+
$host = 'tcp://'.$host;
84+
}
85+
86+
if (!$socket = stream_socket_server($host, $errno, $errstr)) {
87+
throw new \RuntimeException(sprintf('Server start failed on "%s": %s %s.', $host, $errstr, $errno));
88+
}
89+
90+
$this->displayLog($input, $output, 0, array(
91+
'datetime' => new \DateTime(),
92+
'level' => 100,
93+
'level_name' => 'info',
94+
'channel' => 'app',
95+
'message' => sprintf('Listening on <comment>%s</comment>', $host).PHP_EOL,
96+
'context' => array(),
97+
'extra' => array(),
98+
));
99+
100+
$logs = $this->getLogs($socket);
101+
$filter = $input->getOption('filter');
102+
103+
foreach ($logs as $clientId => $message) {
104+
$record = unserialize(base64_decode($message));
105+
106+
// Impossible to decode the message, give up.
107+
if (false === $record) {
108+
continue;
109+
}
110+
111+
if ($filter && !$this->el->evaluate($filter, $record)) {
112+
continue;
113+
}
114+
115+
$this->displayLog($input, $output, $clientId, $record);
116+
}
117+
}
118+
119+
private function getLogs($socket)
120+
{
121+
$sockets = array((int) $socket => $socket);
122+
$write = array();
123+
124+
while (true) {
125+
$read = $sockets;
126+
stream_select($read, $write, $write, null);
127+
128+
foreach ($read as $stream) {
129+
if ($socket === $stream) {
130+
$stream = stream_socket_accept($socket);
131+
$sockets[(int) $stream] = $stream;
132+
} elseif (feof($stream)) {
133+
unset($sockets[(int) $stream]);
134+
fclose($stream);
135+
} else {
136+
yield (int) $stream => fgets($stream);
137+
}
138+
}
139+
}
140+
}
141+
142+
private function displayLog(InputInterface $input, OutputInterface $output, $clientId, array $record)
143+
{
144+
$record = $this->replacePlaceHolder($output, $record);
145+
146+
$format = $input->getOption('format');
147+
$date = $record['datetime']->format($input->getOption('date-format'));
148+
if (isset($record['log_id'])) {
149+
$clientId = unpack('H*', $record['log_id'])[1];
150+
}
151+
$logBlock = sprintf('<bg=%s> </>', self::$bgColor[$clientId % 8]);
152+
$levelColor = self::$levelColorMap[$record['level']];
153+
$message = $logBlock.sprintf($format, $date, $levelColor, $record['level_name'], $record['channel'], $record['message']);
154+
155+
$output->writeln($message);
156+
157+
if ($record['context'] && $input->getOption('show-context') && $record['context']->getRawData()[0][0]) {
158+
$output->writeln(sprintf('%sContext %s', $logBlock, $this->dumpData($output, $record['context'])));
159+
}
160+
if ($record['extra'] && $input->getOption('show-extra') && $record['extra']->getRawData()[0][0]) {
161+
$output->writeln(sprintf('%sExtra %s', $logBlock, $this->dumpData($output, $record['extra'])));
162+
}
163+
}
164+
165+
private function replacePlaceHolder(OutputInterface $output, array $record)
166+
{
167+
$message = $record['message'];
168+
169+
if (false === strpos($message, '{')) {
170+
return $record;
171+
}
172+
173+
$context = $record['context'];
174+
175+
$replacements = array();
176+
foreach ($context->getRawData()[1] as $k => $v) {
177+
$replacements['"{'.$k.'}"'] = sprintf('<comment>%s</>', $this->dumpData($output, $context->seek($k), false));
178+
}
179+
180+
$record['message'] = strtr($message, $replacements);
181+
182+
return $record;
183+
}
184+
185+
private function dumpData(OutputInterface $output, Data $data, $colors = true, $maxDepth = 0)
186+
{
187+
if ($output->isDecorated()) {
188+
$this->dumper->setColors($colors);
189+
}
190+
191+
$this->dumper->dump($data);
192+
193+
$dump = stream_get_contents($this->output, -1, 0);
194+
rewind($this->output);
195+
ftruncate($this->output, 0);
196+
197+
return rtrim($dump);
198+
}
199+
}

0 commit comments

Comments
 (0)