ServerLogCommand.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  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\Command;
  11. use Monolog\Formatter\FormatterInterface;
  12. use Monolog\Logger;
  13. use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter;
  14. use Symfony\Bridge\Monolog\Handler\ConsoleHandler;
  15. use Symfony\Component\Console\Command\Command;
  16. use Symfony\Component\Console\Exception\LogicException;
  17. use Symfony\Component\Console\Exception\RuntimeException;
  18. use Symfony\Component\Console\Input\InputInterface;
  19. use Symfony\Component\Console\Input\InputOption;
  20. use Symfony\Component\Console\Output\OutputInterface;
  21. use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
  22. /**
  23. * @author Grégoire Pineau <lyrixx@lyrixx.info>
  24. */
  25. class ServerLogCommand extends Command
  26. {
  27. private const BG_COLOR = ['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow'];
  28. private $el;
  29. private $handler;
  30. protected static $defaultName = 'server:log';
  31. public function isEnabled()
  32. {
  33. if (!class_exists(ConsoleFormatter::class)) {
  34. return false;
  35. }
  36. // based on a symfony/symfony package, it crashes due a missing FormatterInterface from monolog/monolog
  37. if (!interface_exists(FormatterInterface::class)) {
  38. return false;
  39. }
  40. return parent::isEnabled();
  41. }
  42. protected function configure()
  43. {
  44. if (!class_exists(ConsoleFormatter::class)) {
  45. return;
  46. }
  47. $this
  48. ->addOption('host', null, InputOption::VALUE_REQUIRED, 'The server host', '0.0.0.0:9911')
  49. ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The line format', ConsoleFormatter::SIMPLE_FORMAT)
  50. ->addOption('date-format', null, InputOption::VALUE_REQUIRED, 'The date format', ConsoleFormatter::SIMPLE_DATE)
  51. ->addOption('filter', null, InputOption::VALUE_REQUIRED, 'An expression to filter log. Example: "level > 200 or channel in [\'app\', \'doctrine\']"')
  52. ->setDescription('Start a log server that displays logs in real time')
  53. ->setHelp(<<<'EOF'
  54. <info>%command.name%</info> starts a log server to display in real time the log
  55. messages generated by your application:
  56. <info>php %command.full_name%</info>
  57. To get the information as a machine readable format, use the
  58. <comment>--filter</> option:
  59. <info>php %command.full_name% --filter=port</info>
  60. EOF
  61. )
  62. ;
  63. }
  64. protected function execute(InputInterface $input, OutputInterface $output)
  65. {
  66. $filter = $input->getOption('filter');
  67. if ($filter) {
  68. if (!class_exists(ExpressionLanguage::class)) {
  69. throw new LogicException('Package "symfony/expression-language" is required to use the "filter" option.');
  70. }
  71. $this->el = new ExpressionLanguage();
  72. }
  73. $this->handler = new ConsoleHandler($output, true, [
  74. OutputInterface::VERBOSITY_NORMAL => Logger::DEBUG,
  75. ]);
  76. $this->handler->setFormatter(new ConsoleFormatter([
  77. 'format' => str_replace('\n', "\n", $input->getOption('format')),
  78. 'date_format' => $input->getOption('date-format'),
  79. 'colors' => $output->isDecorated(),
  80. 'multiline' => OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity(),
  81. ]));
  82. if (false === strpos($host = $input->getOption('host'), '://')) {
  83. $host = 'tcp://'.$host;
  84. }
  85. if (!$socket = stream_socket_server($host, $errno, $errstr)) {
  86. throw new RuntimeException(sprintf('Server start failed on "%s": ', $host).$errstr.' '.$errno);
  87. }
  88. foreach ($this->getLogs($socket) as $clientId => $message) {
  89. $record = unserialize(base64_decode($message));
  90. // Impossible to decode the message, give up.
  91. if (false === $record) {
  92. continue;
  93. }
  94. if ($filter && !$this->el->evaluate($filter, $record)) {
  95. continue;
  96. }
  97. $this->displayLog($output, $clientId, $record);
  98. }
  99. return 0;
  100. }
  101. private function getLogs($socket): iterable
  102. {
  103. $sockets = [(int) $socket => $socket];
  104. $write = [];
  105. while (true) {
  106. $read = $sockets;
  107. stream_select($read, $write, $write, null);
  108. foreach ($read as $stream) {
  109. if ($socket === $stream) {
  110. $stream = stream_socket_accept($socket);
  111. $sockets[(int) $stream] = $stream;
  112. } elseif (feof($stream)) {
  113. unset($sockets[(int) $stream]);
  114. fclose($stream);
  115. } else {
  116. yield (int) $stream => fgets($stream);
  117. }
  118. }
  119. }
  120. }
  121. private function displayLog(OutputInterface $output, int $clientId, array $record)
  122. {
  123. if (isset($record['log_id'])) {
  124. $clientId = unpack('H*', $record['log_id'])[1];
  125. }
  126. $logBlock = sprintf('<bg=%s> </>', self::BG_COLOR[$clientId % 8]);
  127. $output->write($logBlock);
  128. $this->handler->handle($record);
  129. }
  130. }