Skip to content

[Bridge/Monolog] Enhance the Console Handler #21705

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Symfony/Bridge/Monolog/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

3.3.0
-----

* Improved the console handler output formatting by adding var-dumper support

3.0.0
-----

Expand Down
189 changes: 172 additions & 17 deletions src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,45 +11,200 @@

namespace Symfony\Bridge\Monolog\Formatter;

use Monolog\Formatter\LineFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\Stub;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;

/**
* Formats incoming records for console output by coloring them depending on log level.
*
* @author Tobias Schultze <http://tobion.de>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class ConsoleFormatter extends LineFormatter
class ConsoleFormatter implements FormatterInterface
{
const SIMPLE_FORMAT = "%start_tag%[%datetime%] %channel%.%level_name%:%end_tag% %message% %context% %extra%\n";
const SIMPLE_FORMAT = "%datetime% %start_tag%%level_name%%end_tag% <comment>[%channel%]</> %message%%context%%extra%\n";
const SIMPLE_DATE = 'H:i:s';

private static $levelColorMap = array(
Logger::DEBUG => 'fg=white',
Logger::INFO => 'fg=green',
Logger::NOTICE => 'fg=blue',
Logger::WARNING => 'fg=cyan',
Logger::ERROR => 'fg=yellow',
Logger::CRITICAL => 'fg=red',
Logger::ALERT => 'fg=red',
Logger::EMERGENCY => 'fg=white;bg=red',
);

private $options;
private $cloner;
private $outputBuffer;
private $dumper;

/**
* Constructor.
*
* Available options:
* * format: The format of the outputted log string. The following placeholders are supported: %datetime%, %start_tag%, %level_name%, %end_tag%, %channel%, %message%, %context%, %extra%;
* * date_format: The format of the outputted date string;
* * colors: If true, the log string contains ANSI code to add color;
* * multiline: If false, "context" and "extra" are dumped on one line.
*/
public function __construct($options = array())
{
// BC Layer
if (!is_array($options)) {
@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);
$args = func_get_args();
$options = array();
if (isset($args[0])) {
$options['format'] = $args[0];
}
if (isset($args[1])) {
$options['date_format'] = $args[1];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the two last arguments?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are not relevant anymore. So I don't need to "map" them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't $allowInlineLineBreaks map to multiline ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. I will fix it in the next pr. Thanks.

Copy link
Contributor

@mpdude mpdude Sep 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you intentionally removed the option to suppress emtpy context and extra data?

Update: Just saw that this was mentioned in #21705 (comment) as well.

}

$this->options = array_replace(array(
'format' => self::SIMPLE_FORMAT,
'date_format' => self::SIMPLE_DATE,
'colors' => true,
'multiline' => false,
), $options);

if (class_exists(VarCloner::class)) {
$this->cloner = new VarCloner();
$this->cloner->addCasters(array(
'*' => array($this, 'castObject'),
));

$this->outputBuffer = fopen('php://memory', 'r+b');
if ($this->options['multiline']) {
$output = $this->outputBuffer;
} else {
$output = array($this, 'echoLine');
}

$this->dumper = new CliDumper($output, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
}
}

/**
* {@inheritdoc}
*/
public function __construct($format = null, $dateFormat = null, $allowInlineLineBreaks = false, $ignoreEmptyContextAndExtra = true)
public function formatBatch(array $records)
{
parent::__construct($format, $dateFormat, $allowInlineLineBreaks, $ignoreEmptyContextAndExtra);
foreach ($records as $key => $record) {
$records[$key] = $this->format($record);
}

return $records;
}

/**
* {@inheritdoc}
*/
public function format(array $record)
{
if ($record['level'] >= Logger::ERROR) {
$record['start_tag'] = '<error>';
$record['end_tag'] = '</error>';
} elseif ($record['level'] >= Logger::NOTICE) {
$record['start_tag'] = '<comment>';
$record['end_tag'] = '</comment>';
} elseif ($record['level'] >= Logger::INFO) {
$record['start_tag'] = '<info>';
$record['end_tag'] = '</info>';
$record = $this->replacePlaceHolder($record);

$levelColor = self::$levelColorMap[$record['level']];

if ($this->options['multiline']) {
$context = $extra = "\n";
} else {
$context = $extra = ' ';
}
$context .= $this->dumpData($record['context']);
$extra .= $this->dumpData($record['extra']);

$formatted = strtr($this->options['format'], array(
'%datetime%' => $record['datetime']->format($this->options['date_format']),
'%start_tag%' => sprintf('<%s>', $levelColor),
'%level_name%' => sprintf('%-9s', $record['level_name']),
'%end_tag%' => '</>',
'%channel%' => $record['channel'],
'%message%' => $this->replacePlaceHolder($record)['message'],
'%context%' => $context,
'%extra%' => $extra,
));

return $formatted;
}

/**
* @internal
*/
public function echoLine($line, $depth, $indentPad)
{
if (-1 !== $depth) {
fwrite($this->outputBuffer, $line);
}
}

/**
* @internal
*/
public function castObject($v, array $a, Stub $s, $isNested)
{
if ($this->options['multiline']) {
return $a;
}

if ($isNested && !$v instanceof \DateTimeInterface) {
$s->cut = -1;
$a = array();
}

return $a;
}

private function replacePlaceHolder(array $record)
{
$message = $record['message'];

if (false === strpos($message, '{')) {
return $record;
}

$context = $record['context'];

$replacements = array();
foreach ($context as $k => $v) {
$replacements['{'.$k.'}'] = sprintf('<comment>%s</>', $this->dumpData($v, false));
}

$record['message'] = strtr($message, $replacements);

return $record;
}

private function dumpData($data, $colors = null)
{
if (null === $this->dumper) {
return '';
}

if (null === $colors) {
$this->dumper->setColors($this->options['colors']);
} else {
$record['start_tag'] = '';
$record['end_tag'] = '';
$this->dumper->setColors($colors);
}

return parent::format($record);
if (!$data instanceof Data) {
$data = $this->cloner->cloneVar($data);
}
$data = $data->withRefHandles(false);
$this->dumper->dump($data);

$dump = stream_get_contents($this->outputBuffer, -1, 0);
rewind($this->outputBuffer);
ftruncate($this->outputBuffer, 0);

return rtrim($dump);
}
}
9 changes: 8 additions & 1 deletion src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,14 @@ protected function write(array $record)
*/
protected function getDefaultFormatter()
{
return new ConsoleFormatter();
if (!$this->output) {
return new ConsoleFormatter();
}

return new ConsoleFormatter(array(
'colors' => $this->output->isDecorated(),
'multiline' => OutputInterface::VERBOSITY_DEBUG <= $this->output->getVerbosity(),
));
}

/**
Expand Down
10 changes: 8 additions & 2 deletions src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,19 @@ public function testVerbosityMapping($verbosity, $level, $isHandling, array $map

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

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

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

$handler = new ConsoleHandler(null, false);
Expand Down
3 changes: 2 additions & 1 deletion src/Symfony/Bridge/Monolog/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
},
"require-dev": {
"symfony/console": "~2.8|~3.0",
"symfony/event-dispatcher": "~2.8|~3.0"
"symfony/event-dispatcher": "~2.8|~3.0",
"symfony/var-dumper": "~3.3"
},
"suggest": {
"symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.",
Expand Down