Skip to content

Commit 69ec738

Browse files
committed
[Bridge/Monolog] Enhanced the Console Handler
Basically, the formatter now uses the VarDumper & uses more significant colors and has a more compact / readable format (IMHO).
1 parent e7c12d3 commit 69ec738

File tree

4 files changed

+190
-21
lines changed

4 files changed

+190
-21
lines changed

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

+172-17
Original file line numberDiff line numberDiff line change
@@ -11,45 +11,200 @@
1111

1212
namespace Symfony\Bridge\Monolog\Formatter;
1313

14-
use Monolog\Formatter\LineFormatter;
14+
use Monolog\Formatter\FormatterInterface;
1515
use Monolog\Logger;
16+
use Symfony\Component\VarDumper\Cloner\Data;
17+
use Symfony\Component\VarDumper\Cloner\Stub;
18+
use Symfony\Component\VarDumper\Cloner\VarCloner;
19+
use Symfony\Component\VarDumper\Dumper\CliDumper;
1620

1721
/**
1822
* Formats incoming records for console output by coloring them depending on log level.
1923
*
2024
* @author Tobias Schultze <http://tobion.de>
25+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
2126
*/
22-
class ConsoleFormatter extends LineFormatter
27+
class ConsoleFormatter implements FormatterInterface
2328
{
24-
const SIMPLE_FORMAT = "%start_tag%[%datetime%] %channel%.%level_name%:%end_tag% %message% %context% %extra%\n";
29+
const SIMPLE_FORMAT = "%datetime% %start_tag%%level_name%%end_tag% <comment>[%channel%]</> %message%%context%%extra%\n";
30+
const SIMPLE_DATE = 'H:i:s';
31+
32+
private static $levelColorMap = array(
33+
Logger::DEBUG => 'fg=white',
34+
Logger::INFO => 'fg=green',
35+
Logger::NOTICE => 'fg=blue',
36+
Logger::WARNING => 'fg=cyan',
37+
Logger::ERROR => 'fg=yellow',
38+
Logger::CRITICAL => 'fg=red',
39+
Logger::ALERT => 'fg=red',
40+
Logger::EMERGENCY => 'fg=white;bg=red',
41+
);
42+
43+
private $options;
44+
private $cloner;
45+
private $outputBuffer;
46+
private $dumper;
47+
48+
/**
49+
* Constructor.
50+
*
51+
* Available options:
52+
* * format: The format of the outputted log string. The following placeholders are supported: %datetime%, %start_tag%, %level_name%, %end_tag%, %channel%, %message%, %context%, %extra%;
53+
* * date_format: The format of the outputted date string;
54+
* * colors: If true, the log string contains ANSI code to add color;
55+
* * multiline: If false, "context" and "extra" are dumped on one line.
56+
*/
57+
public function __construct($options = array())
58+
{
59+
// BC Layer
60+
if (!is_array($options)) {
61+
@trigger_error(sprintf('The constructor arguments $format, $dateFormat, $allowInlineLineBreaks, $ignoreEmptyContextAndExtra of "%s" are deprecated since 3.3 and will be removed in 4.0. Use $options instead.', self::class), E_USER_DEPRECATED);
62+
$args = func_get_args();
63+
$options = array();
64+
if (isset($args[0])) {
65+
$options['format'] = $args[0];
66+
}
67+
if (isset($args[1])) {
68+
$options['date_format'] = $args[1];
69+
}
70+
}
71+
72+
$this->options = array_replace(array(
73+
'format' => self::SIMPLE_FORMAT,
74+
'date_format' => self::SIMPLE_DATE,
75+
'colors' => true,
76+
'multiline' => false,
77+
), $options);
78+
79+
if (class_exists(VarCloner::class)) {
80+
$this->cloner = new VarCloner();
81+
$this->cloner->addCasters(array(
82+
'*' => array($this, 'castObject'),
83+
));
84+
85+
$this->outputBuffer = fopen('php://memory', 'r+b');
86+
if ($this->options['multiline']) {
87+
$output = $this->outputBuffer;
88+
} else {
89+
$output = array($this, 'echoLine');
90+
}
91+
92+
$this->dumper = new CliDumper($output, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
93+
}
94+
}
2595

2696
/**
2797
* {@inheritdoc}
2898
*/
29-
public function __construct($format = null, $dateFormat = null, $allowInlineLineBreaks = false, $ignoreEmptyContextAndExtra = true)
99+
public function formatBatch(array $records)
30100
{
31-
parent::__construct($format, $dateFormat, $allowInlineLineBreaks, $ignoreEmptyContextAndExtra);
101+
foreach ($records as $key => $record) {
102+
$records[$key] = $this->format($record);
103+
}
104+
105+
return $records;
32106
}
33107

34108
/**
35109
* {@inheritdoc}
36110
*/
37111
public function format(array $record)
38112
{
39-
if ($record['level'] >= Logger::ERROR) {
40-
$record['start_tag'] = '<error>';
41-
$record['end_tag'] = '</error>';
42-
} elseif ($record['level'] >= Logger::NOTICE) {
43-
$record['start_tag'] = '<comment>';
44-
$record['end_tag'] = '</comment>';
45-
} elseif ($record['level'] >= Logger::INFO) {
46-
$record['start_tag'] = '<info>';
47-
$record['end_tag'] = '</info>';
113+
$record = $this->replacePlaceHolder($record);
114+
115+
$levelColor = self::$levelColorMap[$record['level']];
116+
117+
if ($this->options['multiline']) {
118+
$context = $extra = "\n";
119+
} else {
120+
$context = $extra = ' ';
121+
}
122+
$context .= $this->dumpData($record['context']);
123+
$extra .= $this->dumpData($record['extra']);
124+
125+
$formatted = strtr($this->options['format'], array(
126+
'%datetime%' => $record['datetime']->format($this->options['date_format']),
127+
'%start_tag%' => sprintf('<%s>', $levelColor),
128+
'%level_name%' => sprintf('%-9s', $record['level_name']),
129+
'%end_tag%' => '</>',
130+
'%channel%' => $record['channel'],
131+
'%message%' => $this->replacePlaceHolder($record)['message'],
132+
'%context%' => $context,
133+
'%extra%' => $extra,
134+
));
135+
136+
return $formatted;
137+
}
138+
139+
/**
140+
* @internal
141+
*/
142+
public function echoLine($line, $depth, $indentPad)
143+
{
144+
if (-1 !== $depth) {
145+
fwrite($this->outputBuffer, $line);
146+
}
147+
}
148+
149+
/**
150+
* @internal
151+
*/
152+
public function castObject($v, array $a, Stub $s, $isNested)
153+
{
154+
if ($this->options['multiline']) {
155+
return $a;
156+
}
157+
158+
if ($isNested && !$v instanceof \DateTimeInterface) {
159+
$s->cut = -1;
160+
$a = array();
161+
}
162+
163+
return $a;
164+
}
165+
166+
private function replacePlaceHolder(array $record)
167+
{
168+
$message = $record['message'];
169+
170+
if (false === strpos($message, '{')) {
171+
return $record;
172+
}
173+
174+
$context = $record['context'];
175+
176+
$replacements = array();
177+
foreach ($context as $k => $v) {
178+
$replacements['{'.$k.'}'] = sprintf('<comment>%s</>', $this->dumpData($v, false));
179+
}
180+
181+
$record['message'] = strtr($message, $replacements);
182+
183+
return $record;
184+
}
185+
186+
private function dumpData($data, $colors = null)
187+
{
188+
if (null === $this->dumper) {
189+
return '';
190+
}
191+
192+
if (null === $colors) {
193+
$this->dumper->setColors($this->options['colors']);
48194
} else {
49-
$record['start_tag'] = '';
50-
$record['end_tag'] = '';
195+
$this->dumper->setColors($colors);
51196
}
52197

53-
return parent::format($record);
198+
if (!$data instanceof Data) {
199+
$data = $this->cloner->cloneVar($data);
200+
}
201+
$data = $data->withRefHandles(false);
202+
$this->dumper->dump($data);
203+
204+
$dump = stream_get_contents($this->outputBuffer, -1, 0);
205+
rewind($this->outputBuffer);
206+
ftruncate($this->outputBuffer, 0);
207+
208+
return rtrim($dump);
54209
}
55210
}

src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,14 @@ protected function write(array $record)
164164
*/
165165
protected function getDefaultFormatter()
166166
{
167-
return new ConsoleFormatter();
167+
if (!$this->output) {
168+
return new ConsoleFormatter();
169+
}
170+
171+
return new ConsoleFormatter(array(
172+
'colors' => $this->output->isDecorated(),
173+
'multiline' => OutputInterface::VERBOSITY_DEBUG <= $this->output->getVerbosity(),
174+
));
168175
}
169176

170177
/**

src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,19 @@ public function testVerbosityMapping($verbosity, $level, $isHandling, array $map
5959

6060
// check that the handler actually outputs the record if it handles it
6161
$levelName = Logger::getLevelName($level);
62+
$levelName = sprintf('%-9s', $levelName);
6263

6364
$realOutput = $this->getMockBuilder('Symfony\Component\Console\Output\Output')->setMethods(array('doWrite'))->getMock();
6465
$realOutput->setVerbosity($verbosity);
66+
if ($realOutput->isDebug()) {
67+
$log = "16:21:54 $levelName [app] My info message\n[]\n[]\n";
68+
} else {
69+
$log = "16:21:54 $levelName [app] My info message [] []\n";
70+
}
6571
$realOutput
6672
->expects($isHandling ? $this->once() : $this->never())
6773
->method('doWrite')
68-
->with("[2013-05-29 16:21:54] app.$levelName: My info message \n", false);
74+
->with($log, false);
6975
$handler = new ConsoleHandler($realOutput, true, $map);
7076

7177
$infoRecord = array(
@@ -143,7 +149,7 @@ public function testWritingAndFormatting()
143149
$output
144150
->expects($this->once())
145151
->method('write')
146-
->with('<info>[2013-05-29 16:21:54] app.INFO:</info> My info message '."\n")
152+
->with("16:21:54 <fg=green>INFO </> <comment>[app]</> My info message\n[]\n[]\n")
147153
;
148154

149155
$handler = new ConsoleHandler(null, false);

src/Symfony/Bridge/Monolog/composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
},
2323
"require-dev": {
2424
"symfony/console": "~2.8|~3.0",
25-
"symfony/event-dispatcher": "~2.8|~3.0"
25+
"symfony/event-dispatcher": "~2.8|~3.0",
26+
"symfony/var-dumper": "~3.3"
2627
},
2728
"suggest": {
2829
"symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.",

0 commit comments

Comments
 (0)