From b663ab5246f604c17de7a30f7582e9e91fdebdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 17 Feb 2017 15:51:08 +0100 Subject: [PATCH] [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). --- src/Symfony/Bridge/Monolog/CHANGELOG.md | 5 + .../Monolog/Formatter/ConsoleFormatter.php | 189 ++++++++++++++++-- .../Bridge/Monolog/Handler/ConsoleHandler.php | 9 +- .../Tests/Handler/ConsoleHandlerTest.php | 10 +- src/Symfony/Bridge/Monolog/composer.json | 3 +- 5 files changed, 195 insertions(+), 21 deletions(-) diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index cb7deea2e9052..f91d4c5d9a224 100644 --- a/src/Symfony/Bridge/Monolog/CHANGELOG.md +++ b/src/Symfony/Bridge/Monolog/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.3.0 +----- + + * Improved the console handler output formatting by adding var-dumper support + 3.0.0 ----- diff --git a/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php b/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php index 1af93bc97aebe..80e15d5752c81 100644 --- a/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php +++ b/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php @@ -11,24 +11,98 @@ 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 + * @author Grégoire Pineau */ -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% [%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]; + } + } + + $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; } /** @@ -36,20 +110,101 @@ public function __construct($format = null, $dateFormat = null, $allowInlineLine */ public function format(array $record) { - if ($record['level'] >= Logger::ERROR) { - $record['start_tag'] = ''; - $record['end_tag'] = ''; - } elseif ($record['level'] >= Logger::NOTICE) { - $record['start_tag'] = ''; - $record['end_tag'] = ''; - } elseif ($record['level'] >= Logger::INFO) { - $record['start_tag'] = ''; - $record['end_tag'] = ''; + $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('%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); } } diff --git a/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php b/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php index 24c79397cf9a2..01f055f20a1aa 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php @@ -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(), + )); } /** diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php index 60f57c39d085c..63b5a8f07b6d7 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php @@ -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( @@ -143,7 +149,7 @@ public function testWritingAndFormatting() $output ->expects($this->once()) ->method('write') - ->with('[2013-05-29 16:21:54] app.INFO: My info message '."\n") + ->with("16:21:54 INFO [app] My info message\n[]\n[]\n") ; $handler = new ConsoleHandler(null, false); diff --git a/src/Symfony/Bridge/Monolog/composer.json b/src/Symfony/Bridge/Monolog/composer.json index 81bc6995ee552..acb09396a51ab 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -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.",