ConsoleFormatter.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Bridge\Monolog\Formatter;
  11. use Monolog\Formatter\FormatterInterface;
  12. use Monolog\Logger;
  13. use Symfony\Component\Console\Formatter\OutputFormatter;
  14. use Symfony\Component\VarDumper\Cloner\Data;
  15. use Symfony\Component\VarDumper\Cloner\Stub;
  16. use Symfony\Component\VarDumper\Cloner\VarCloner;
  17. use Symfony\Component\VarDumper\Dumper\CliDumper;
  18. /**
  19. * Formats incoming records for console output by coloring them depending on log level.
  20. *
  21. * @author Tobias Schultze <http://tobion.de>
  22. * @author Grégoire Pineau <lyrixx@lyrixx.info>
  23. */
  24. class ConsoleFormatter implements FormatterInterface
  25. {
  26. public const SIMPLE_FORMAT = "%datetime% %start_tag%%level_name%%end_tag% <comment>[%channel%]</> %message%%context%%extra%\n";
  27. public const SIMPLE_DATE = 'H:i:s';
  28. private const LEVEL_COLOR_MAP = [
  29. Logger::DEBUG => 'fg=white',
  30. Logger::INFO => 'fg=green',
  31. Logger::NOTICE => 'fg=blue',
  32. Logger::WARNING => 'fg=cyan',
  33. Logger::ERROR => 'fg=yellow',
  34. Logger::CRITICAL => 'fg=red',
  35. Logger::ALERT => 'fg=red',
  36. Logger::EMERGENCY => 'fg=white;bg=red',
  37. ];
  38. private $options;
  39. private $cloner;
  40. private $outputBuffer;
  41. private $dumper;
  42. /**
  43. * Available options:
  44. * * format: The format of the outputted log string. The following placeholders are supported: %datetime%, %start_tag%, %level_name%, %end_tag%, %channel%, %message%, %context%, %extra%;
  45. * * date_format: The format of the outputted date string;
  46. * * colors: If true, the log string contains ANSI code to add color;
  47. * * multiline: If false, "context" and "extra" are dumped on one line.
  48. */
  49. public function __construct(array $options = [])
  50. {
  51. $this->options = array_replace([
  52. 'format' => self::SIMPLE_FORMAT,
  53. 'date_format' => self::SIMPLE_DATE,
  54. 'colors' => true,
  55. 'multiline' => false,
  56. 'level_name_format' => '%-9s',
  57. 'ignore_empty_context_and_extra' => true,
  58. ], $options);
  59. if (class_exists(VarCloner::class)) {
  60. $this->cloner = new VarCloner();
  61. $this->cloner->addCasters([
  62. '*' => [$this, 'castObject'],
  63. ]);
  64. $this->outputBuffer = fopen('php://memory', 'r+');
  65. if ($this->options['multiline']) {
  66. $output = $this->outputBuffer;
  67. } else {
  68. $output = [$this, 'echoLine'];
  69. }
  70. $this->dumper = new CliDumper($output, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
  71. }
  72. }
  73. /**
  74. * {@inheritdoc}
  75. *
  76. * @return mixed
  77. */
  78. public function formatBatch(array $records)
  79. {
  80. foreach ($records as $key => $record) {
  81. $records[$key] = $this->format($record);
  82. }
  83. return $records;
  84. }
  85. /**
  86. * {@inheritdoc}
  87. *
  88. * @return mixed
  89. */
  90. public function format(array $record)
  91. {
  92. $record = $this->replacePlaceHolder($record);
  93. if (!$this->options['ignore_empty_context_and_extra'] || !empty($record['context'])) {
  94. $context = ($this->options['multiline'] ? "\n" : ' ').$this->dumpData($record['context']);
  95. } else {
  96. $context = '';
  97. }
  98. if (!$this->options['ignore_empty_context_and_extra'] || !empty($record['extra'])) {
  99. $extra = ($this->options['multiline'] ? "\n" : ' ').$this->dumpData($record['extra']);
  100. } else {
  101. $extra = '';
  102. }
  103. $formatted = strtr($this->options['format'], [
  104. '%datetime%' => $record['datetime'] instanceof \DateTimeInterface
  105. ? $record['datetime']->format($this->options['date_format'])
  106. : $record['datetime'],
  107. '%start_tag%' => sprintf('<%s>', self::LEVEL_COLOR_MAP[$record['level']]),
  108. '%level_name%' => sprintf($this->options['level_name_format'], $record['level_name']),
  109. '%end_tag%' => '</>',
  110. '%channel%' => $record['channel'],
  111. '%message%' => $this->replacePlaceHolder($record)['message'],
  112. '%context%' => $context,
  113. '%extra%' => $extra,
  114. ]);
  115. return $formatted;
  116. }
  117. /**
  118. * @internal
  119. */
  120. public function echoLine(string $line, int $depth, string $indentPad)
  121. {
  122. if (-1 !== $depth) {
  123. fwrite($this->outputBuffer, $line);
  124. }
  125. }
  126. /**
  127. * @internal
  128. */
  129. public function castObject($v, array $a, Stub $s, bool $isNested): array
  130. {
  131. if ($this->options['multiline']) {
  132. return $a;
  133. }
  134. if ($isNested && !$v instanceof \DateTimeInterface) {
  135. $s->cut = -1;
  136. $a = [];
  137. }
  138. return $a;
  139. }
  140. private function replacePlaceHolder(array $record): array
  141. {
  142. $message = $record['message'];
  143. if (false === strpos($message, '{')) {
  144. return $record;
  145. }
  146. $context = $record['context'];
  147. $replacements = [];
  148. foreach ($context as $k => $v) {
  149. // Remove quotes added by the dumper around string.
  150. $v = trim($this->dumpData($v, false), '"');
  151. $v = OutputFormatter::escape($v);
  152. $replacements['{'.$k.'}'] = sprintf('<comment>%s</>', $v);
  153. }
  154. $record['message'] = strtr($message, $replacements);
  155. return $record;
  156. }
  157. private function dumpData($data, bool $colors = null): string
  158. {
  159. if (null === $this->dumper) {
  160. return '';
  161. }
  162. if (null === $colors) {
  163. $this->dumper->setColors($this->options['colors']);
  164. } else {
  165. $this->dumper->setColors($colors);
  166. }
  167. if (!$data instanceof Data) {
  168. $data = $this->cloner->cloneVar($data);
  169. }
  170. $data = $data->withRefHandles(false);
  171. $this->dumper->dump($data);
  172. $dump = stream_get_contents($this->outputBuffer, -1, 0);
  173. rewind($this->outputBuffer);
  174. ftruncate($this->outputBuffer, 0);
  175. return rtrim($dump);
  176. }
  177. }