diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index e2439304faec..9deec49664ae 100644 --- a/src/Symfony/Bridge/Monolog/CHANGELOG.md +++ b/src/Symfony/Bridge/Monolog/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * The `RouteProcessor` class has been made final * Added `ElasticsearchLogstashHandler` +* Added the `ServerLogCommand`. Backport from the deprecated WebServerBundle 4.3.0 ----- diff --git a/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php b/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php new file mode 100644 index 000000000000..60e7341db9e2 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Command; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter; +use Symfony\Bridge\Monolog\Handler\ConsoleHandler; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + +/** + * @author Grégoire Pineau + */ +class ServerLogCommand extends Command +{ + private static $bgColor = ['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow']; + + private $el; + private $handler; + + protected static $defaultName = 'server:log'; + + public function isEnabled() + { + if (!class_exists(ConsoleFormatter::class)) { + return false; + } + + // based on a symfony/symfony package, it crashes due a missing FormatterInterface from monolog/monolog + if (!interface_exists(FormatterInterface::class)) { + return false; + } + + return parent::isEnabled(); + } + + protected function configure() + { + if (!class_exists(ConsoleFormatter::class)) { + return; + } + + $this + ->addOption('host', null, InputOption::VALUE_REQUIRED, 'The server host', '0.0.0.0:9911') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The line format', ConsoleFormatter::SIMPLE_FORMAT) + ->addOption('date-format', null, InputOption::VALUE_REQUIRED, 'The date format', ConsoleFormatter::SIMPLE_DATE) + ->addOption('filter', null, InputOption::VALUE_REQUIRED, 'An expression to filter log. Example: "level > 200 or channel in [\'app\', \'doctrine\']"') + ->setDescription('Starts a log server that displays logs in real time') + ->setHelp(<<<'EOF' +%command.name% starts a log server to display in real time the log +messages generated by your application: + + php %command.full_name% + +To get the information as a machine readable format, use the +--filter option: + +php %command.full_name% --filter=port +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $filter = $input->getOption('filter'); + if ($filter) { + if (!class_exists(ExpressionLanguage::class)) { + throw new LogicException('Package "symfony/expression-language" is required to use the "filter" option.'); + } + $this->el = new ExpressionLanguage(); + } + + $this->handler = new ConsoleHandler($output, true, [ + OutputInterface::VERBOSITY_NORMAL => Logger::DEBUG, + ]); + + $this->handler->setFormatter(new ConsoleFormatter([ + 'format' => str_replace('\n', "\n", $input->getOption('format')), + 'date_format' => $input->getOption('date-format'), + 'colors' => $output->isDecorated(), + 'multiline' => OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity(), + ])); + + if (false === strpos($host = $input->getOption('host'), '://')) { + $host = 'tcp://'.$host; + } + + if (!$socket = stream_socket_server($host, $errno, $errstr)) { + throw new RuntimeException(sprintf('Server start failed on "%s": %s %s.', $host, $errstr, $errno)); + } + + foreach ($this->getLogs($socket) as $clientId => $message) { + $record = unserialize(base64_decode($message)); + + // Impossible to decode the message, give up. + if (false === $record) { + continue; + } + + if ($filter && !$this->el->evaluate($filter, $record)) { + continue; + } + + $this->displayLog($output, $clientId, $record); + } + + return 0; + } + + private function getLogs($socket): iterable + { + $sockets = [(int) $socket => $socket]; + $write = []; + + while (true) { + $read = $sockets; + stream_select($read, $write, $write, null); + + foreach ($read as $stream) { + if ($socket === $stream) { + $stream = stream_socket_accept($socket); + $sockets[(int) $stream] = $stream; + } elseif (feof($stream)) { + unset($sockets[(int) $stream]); + fclose($stream); + } else { + yield (int) $stream => fgets($stream); + } + } + } + } + + private function displayLog(OutputInterface $output, int $clientId, array $record) + { + if (isset($record['log_id'])) { + $clientId = unpack('H*', $record['log_id'])[1]; + } + $logBlock = sprintf(' ', self::$bgColor[$clientId % 8]); + $output->write($logBlock); + + $this->handler->handle($record); + } +} diff --git a/src/Symfony/Bundle/DebugBundle/DebugBundle.php b/src/Symfony/Bundle/DebugBundle/DebugBundle.php index 04fd50761274..fd7d8e72d6a4 100644 --- a/src/Symfony/Bundle/DebugBundle/DebugBundle.php +++ b/src/Symfony/Bundle/DebugBundle/DebugBundle.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\DebugBundle; use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\DumpDataCollectorPass; +use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\RemoveWebServerBundleLoggerPass; use Symfony\Component\Console\Application; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -52,6 +53,7 @@ public function build(ContainerBuilder $container) parent::build($container); $container->addCompilerPass(new DumpDataCollectorPass()); + $container->addCompilerPass(new RemoveWebServerBundleLoggerPass()); } public function registerCommands(Application $application) diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/RemoveWebServerBundleLoggerPass.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/RemoveWebServerBundleLoggerPass.php new file mode 100644 index 000000000000..aca150c81610 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/RemoveWebServerBundleLoggerPass.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\DebugBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Jérémy Derussé + */ +class RemoveWebServerBundleLoggerPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if ($container->hasDefinition('web_server.command.server_log') && $container->hasDefinition('monolog.command.server_log')) { + $container->removeDefinition('web_server.command.server_log'); + } + } +} diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php index 8309db19ecd7..2528cce5b83a 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\DebugBundle\DependencyInjection; +use Symfony\Bridge\Monolog\Command\ServerLogCommand; use Symfony\Bundle\DebugBundle\Command\ServerDumpPlaceholderCommand; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -90,6 +91,10 @@ public function load(array $configs, ContainerBuilder $container) ]]) ; } + + if (!class_exists(ServerLogCommand::class)) { + $container->removeDefinition('monolog.command.server_log'); + } } /** diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml index 49772b46962f..c7cc5725cf8f 100644 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml @@ -107,5 +107,9 @@ + + + + diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php index f38fd6fa58ee..1eb1916a3f12 100644 --- a/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php @@ -26,7 +26,7 @@ /** * @author Grégoire Pineau * - * @deprecated since Symfony 4.4, to be removed in 5.0; the new Symfony local server has more features, you can use it instead. + * @deprecated since Symfony 4.4, to be removed in 5.0; use ServerLogCommand from symfony/monolog-bridge instead */ class ServerLogCommand extends Command { @@ -80,7 +80,7 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', E_USER_DEPRECATED); + @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. Use the DebugBundle combined with MonologBridge instead.', E_USER_DEPRECATED); $filter = $input->getOption('filter'); if ($filter) {