diff --git a/DependencyInjection/Compiler/AutoExcludedChannelsPass.php b/DependencyInjection/Compiler/AutoExcludedChannelsPass.php new file mode 100644 index 00000000..1f33541e --- /dev/null +++ b/DependencyInjection/Compiler/AutoExcludedChannelsPass.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Excludes all specified channels from handlers with a non-exclusive channel list. + * Needs to run before {@see LoggerChannelPass}. + */ +class AutoExcludedChannelsPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if ($autoExcludedChannels = array_values($container->getParameter('monolog.auto_excluded_channels'))) { + $this->processChannels($container, $autoExcludedChannels); + } + + $container->getParameterBag()->remove('monolog.auto_excluded_channels'); + } + + private function processChannels(ContainerBuilder $container, array $autoExcludedChannels): void + { + $processedHandlers = []; + + /** @var array}> $handlersToChannels */ + $handlersToChannels = $container->getParameter('monolog.handlers_to_channels'); + + foreach ($handlersToChannels as $id => &$handlersToChannel) { + if (isset($handlersToChannel['type']) && 'exclusive' !== $handlersToChannel['type']) { + continue; + } + + $handlerName = substr($id, 16); // remove "monolog.handler." + + if (null === $handlersToChannel) { + $handlersToChannel = [ + 'type' => 'exclusive', + 'elements' => $autoExcludedChannels, + ]; + $processedHandlers[$handlerName] = $autoExcludedChannels; + + continue; + } + + foreach ($autoExcludedChannels as $autoExcludedChannel) { + if (false !== $index = array_search('!'.$autoExcludedChannel, $handlersToChannel['elements'], true)) { + array_splice($handlersToChannel['elements'], $index, 1); + if (!$handlersToChannel['elements']) { + $handlersToChannel = null; + } + } elseif (!\in_array($autoExcludedChannel, $handlersToChannel['elements'], true)) { + $handlersToChannel['elements'][] = $autoExcludedChannel; + $processedHandlers[$handlerName][] = $autoExcludedChannel; + } + } + } + + $container->setParameter('monolog.handlers_to_channels', $handlersToChannels); + + foreach ($processedHandlers as $handlerName => $excludedChannels) { + $container->log($this, sprintf( + 'Auto-excluded the following channels from the "%s" handler: "%s".', + $handlerName, + implode('", "', $excludedChannels) + )); + } + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 7dea0247..b6053cdf 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -385,6 +385,7 @@ public function getConfigTreeBuilder() $handlers = $rootNode ->fixXmlConfig('channel') + ->fixXmlConfig('auto_excluded_channel') ->fixXmlConfig('handler') ->children() ->scalarNode('use_microseconds')->defaultTrue()->end() @@ -392,6 +393,10 @@ public function getConfigTreeBuilder() ->canBeUnset() ->prototype('scalar')->end() ->end() + ->arrayNode('auto_excluded_channels') + ->canBeUnset() + ->prototype('scalar')->end() + ->end() ->arrayNode('handlers'); $handlers diff --git a/DependencyInjection/MonologExtension.php b/DependencyInjection/MonologExtension.php index 0c5bee27..49a865df 100644 --- a/DependencyInjection/MonologExtension.php +++ b/DependencyInjection/MonologExtension.php @@ -100,6 +100,7 @@ public function load(array $configs, ContainerBuilder $container) } $container->setParameter('monolog.additional_channels', isset($config['channels']) ? $config['channels'] : []); + $container->setParameter('monolog.auto_excluded_channels', $config['auto_excluded_channels'] ?? []); if (method_exists($container, 'registerForAutoconfiguration')) { if (interface_exists(ProcessorInterface::class)) { diff --git a/MonologBundle.php b/MonologBundle.php index 12e0cb67..51d275c5 100644 --- a/MonologBundle.php +++ b/MonologBundle.php @@ -14,13 +14,15 @@ use Monolog\Formatter\JsonFormatter; use Monolog\Formatter\LineFormatter; use Monolog\Handler\HandlerInterface; +use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\AddProcessorsPass; use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\AddSwiftMailerTransportPass; -use Symfony\Component\HttpKernel\Bundle\Bundle; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\LoggerChannelPass; +use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\AutoExcludedChannelsPass; use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\DebugHandlerPass; -use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\AddProcessorsPass; use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\FixEmptyLoggerPass; +use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\LoggerChannelPass; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; /** * @author Jordi Boggiano @@ -31,6 +33,7 @@ public function build(ContainerBuilder $container) { parent::build($container); + $container->addCompilerPass(new AutoExcludedChannelsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10); $container->addCompilerPass($channelPass = new LoggerChannelPass()); if (!class_exists('Symfony\Bridge\Monolog\Processor\DebugProcessor') || !class_exists('Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddDebugLogProcessorPass')) { $container->addCompilerPass(new DebugHandlerPass($channelPass)); diff --git a/Resources/config/schema/monolog-1.0.xsd b/Resources/config/schema/monolog-1.0.xsd index b00e969e..5be1183f 100644 --- a/Resources/config/schema/monolog-1.0.xsd +++ b/Resources/config/schema/monolog-1.0.xsd @@ -11,6 +11,7 @@ + diff --git a/Tests/DependencyInjection/Compiler/AutoExcludedChannelsPassTest.php b/Tests/DependencyInjection/Compiler/AutoExcludedChannelsPassTest.php new file mode 100644 index 00000000..e08f8196 --- /dev/null +++ b/Tests/DependencyInjection/Compiler/AutoExcludedChannelsPassTest.php @@ -0,0 +1,203 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\Tests\DependencyInjection\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\AutoExcludedChannelsPass; +use Symfony\Bundle\MonologBundle\MonologBundle; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class AutoExcludedChannelsPassTest extends TestCase +{ + /** + * @group legacy + * + * @dataProvider handlerChannels + */ + public function testProcess(?array $autoExcludedChannels, array $handlers, ?array $expectedChannels, array $expectedLog): void + { + $container = new ContainerBuilder(); + + $bundle = new MonologBundle(); + $container->registerExtension($bundle->getContainerExtension()); + $bundle->build($container); + + $container->loadFromExtension('monolog', [ + 'channels' => ['channel1', 'channel2', 'channel3', 'channel4'], + 'auto_excluded_channels' => $autoExcludedChannels, + 'handlers' => $handlers, + ]); + + $container->compile(); + + $this->assertSame($expectedChannels, $container->getParameter('monolog.handlers_to_channels')); + $this->assertFalse($container->hasParameter('monolog.auto_excluded_channels')); + + $this->assertSame($expectedLog, array_values(array_filter( + $container->getCompiler()->getLog(), + function (string $log) { + return 0 === strpos($log, AutoExcludedChannelsPass::class); + } + ))); + } + + public static function handlerChannels(): iterable + { + yield 'No auto-excluded channels' => [ + null, + [ + 'foo' => [ + 'type' => 'console', + 'channels' => ['!channel1'], + ], + ], + ['monolog.handler.foo' => ['type' => 'exclusive', 'elements' => ['channel1']]], + [], + ]; + yield 'Empty auto-excluded channels array' => [ + [], + [ + 'foo' => [ + 'type' => 'console', + 'channels' => ['!channel1'], + ], + ], + ['monolog.handler.foo' => ['type' => 'exclusive', 'elements' => ['channel1']]], + [], + ]; + + yield 'No channels' => [ + ['channel1'], + [ + 'foo' => [ + 'type' => 'console', + 'channels' => null, + ], + ], + ['monolog.handler.foo' => ['type' => 'exclusive', 'elements' => ['channel1']]], + [self::getLog('foo', ['channel1'])], + ]; + yield 'Empty channels array' => [ + ['channel1'], + [ + 'foo' => [ + 'type' => 'console', + 'channels' => [], + ], + ], + ['monolog.handler.foo' => ['type' => 'exclusive', 'elements' => ['channel1']]], + [self::getLog('foo', ['channel1'])], + ]; + + yield 'Inclusive' => [ + ['channel2'], + [ + 'foo' => [ + 'type' => 'console', + 'channels' => ['channel1'], + ], + ], + ['monolog.handler.foo' => ['type' => 'inclusive', 'elements' => ['channel1']]], + [], + ]; + + yield 'Exclusive without exception' => [ + ['channel2'], + [ + 'foo' => [ + 'type' => 'console', + 'channels' => ['!channel1'], + ], + ], + ['monolog.handler.foo' => ['type' => 'exclusive', 'elements' => ['channel1', 'channel2']]], + [self::getLog('foo', ['channel2'])], + ]; + yield 'Exclusive with exception' => [ + ['channel2'], + [ + 'foo' => [ + 'type' => 'console', + 'channels' => ['!channel1', '!!channel2'], + ], + ], + ['monolog.handler.foo' => ['type' => 'exclusive', 'elements' => ['channel1']]], + [], + ]; + yield 'Exclusive with only an exception' => [ + ['channel1'], + [ + 'foo' => [ + 'type' => 'console', + 'channels' => ['!!channel1'], + ], + ], + ['monolog.handler.foo' => null], + [], + ]; + + yield 'Explicitly excluded' => [ + ['channel1'], + [ + 'foo' => [ + 'type' => 'console', + 'channels' => ['!channel1'], + ], + ], + ['monolog.handler.foo' => ['type' => 'exclusive', 'elements' => ['channel1']]], + [], + ]; + + yield 'Multiple auto-excluded channels' => [ + ['channel1', 'channel3'], + [ + 'foo' => [ + 'type' => 'console', + 'channels' => ['!channel2'], + ], + ], + ['monolog.handler.foo' => ['type' => 'exclusive', 'elements' => ['channel2', 'channel1', 'channel3']]], + [self::getLog('foo', ['channel1', 'channel3'])], + ]; + + yield 'Multiple handlers' => [ + ['channel1', 'channel3'], + [ + 'foo' => [ + 'type' => 'console', + 'channels' => ['!channel2'], + ], + 'bar' => [ + 'type' => 'console', + 'channels' => ['channel1', 'channel4'], + ], + 'baz' => [ + 'type' => 'console', + 'channels' => ['!!channel1', '!channel2'], + ], + ], + [ + 'monolog.handler.baz' => ['type' => 'exclusive', 'elements' => ['channel2', 'channel3']], + 'monolog.handler.bar' => ['type' => 'inclusive', 'elements' => ['channel1', 'channel4']], + 'monolog.handler.foo' => ['type' => 'exclusive', 'elements' => ['channel2', 'channel1', 'channel3']], + ], + [ + self::getLog('baz', ['channel3']), + self::getLog('foo', ['channel1', 'channel3']), + ], + ]; + } + + private static function getLog(string $handler, array $channels): string + { + return sprintf('%s: Auto-excluded the following channels from the "%s" handler: "%s".', AutoExcludedChannelsPass::class, $handler, implode('", "', $channels)); + } +} diff --git a/Tests/MonologBundleTest.php b/Tests/MonologBundleTest.php new file mode 100644 index 00000000..9f1341f8 --- /dev/null +++ b/Tests/MonologBundleTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\AutoExcludedChannelsPass; +use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\LoggerChannelPass; +use Symfony\Bundle\MonologBundle\MonologBundle; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class MonologBundleTest extends TestCase +{ + /** + * @group legacy + */ + public function testAutoExcludedChannelsPassIsRegisteredWithCorrectPriority() + { + $container = new ContainerBuilder(); + + (new MonologBundle())->build($container); + + $compilerPassIndexes = []; + foreach ($container->getCompilerPassConfig()->getBeforeOptimizationPasses() as $i => $compilerPass) { + $compilerPassIndexes[\get_class($compilerPass)] = $i; + } + + $this->assertArrayHasKey(LoggerChannelPass::class, $compilerPassIndexes); + $this->assertArrayHasKey(AutoExcludedChannelsPass::class, $compilerPassIndexes); + + $this->assertGreaterThan( + $compilerPassIndexes[AutoExcludedChannelsPass::class], + $compilerPassIndexes[LoggerChannelPass::class] + ); + } +}