Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* Add TelegramBotHandler `topic` support
* Deprecate `sentry` and `raven` handler, use a `service` handler with [`sentry/sentry-symfony`](https://docs.sentry.io/platforms/php/guides/symfony/logs/) instead
* Add configuration for Gelf encoders
* Add `priority` field to `processor` tag

## 3.10.0 (2023-11-06)

Expand Down
1 change: 1 addition & 0 deletions config/monolog.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
->parent('monolog.logger_prototype')
->args(['index_0' => 'app'])
->call('useMicrosecondTimestamps', [param('monolog.use_microseconds')])
->tag('monolog.channel_logger')

->set('monolog.logger_prototype', Logger::class)
->args([abstract_arg('channel')])
Expand Down
79 changes: 50 additions & 29 deletions src/DependencyInjection/Compiler/AddProcessorsPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@

namespace Symfony\Bundle\MonologBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
* Registers processors in Monolog loggers or handlers.
Expand All @@ -25,48 +26,68 @@
*/
class AddProcessorsPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;

public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('monolog.logger')) {
return;
}

$indexedTags = [];
$i = 1;

foreach ($container->findTaggedServiceIds('monolog.processor') as $id => $tags) {
if (array_any($tags, $closure = function (array $tag) { return (bool) $tag; })) {
$tags = array_filter($tags, $closure);
}

foreach ($tags as $tag) {
if (!empty($tag['channel']) && !empty($tag['handler'])) {
throw new \InvalidArgumentException(\sprintf('you cannot specify both the "handler" and "channel" attributes for the "monolog.processor" tag on service "%s".', $id));
}
foreach ($tags as &$tag) {
$indexedTags[$tag['index'] = $i++] = $tag;
}
unset($tag);
$definition = $container->getDefinition($id);
$definition->setTags(array_merge($definition->getTags(), ['monolog.processor' => $tags]));
}

if (!empty($tag['handler'])) {
$definition = $container->findDefinition(\sprintf('monolog.handler.%s', $tag['handler']));
$parentDef = $definition;
while (!$parentDef->getClass() && $parentDef instanceof ChildDefinition) {
$parentDef = $container->findDefinition($parentDef->getParent());
}
$class = $container->getParameterBag()->resolveValue($parentDef->getClass());
if (!method_exists($class, 'pushProcessor')) {
throw new \InvalidArgumentException(\sprintf('The "%s" handler does not accept processors.', $tag['handler']));
}
} elseif (!empty($tag['channel'])) {
if ('app' === $tag['channel']) {
$definition = $container->getDefinition('monolog.logger');
} else {
$definition = $container->getDefinition(\sprintf('monolog.logger.%s', $tag['channel']));
}
} else {
$definition = $container->getDefinition('monolog.logger_prototype');
}
$taggedIteratorArgument = new TaggedIteratorArgument('monolog.processor', 'index', null, true);
// array_reverse is used because ProcessableHandlerTrait::pushProcessor prepends processors to the beginning of the stack
foreach (array_reverse($this->findAndSortTaggedServices($taggedIteratorArgument, $container), true) as $index => $reference) {
$tag = $indexedTags[$index];

if (!empty($tag['method'])) {
$processor = [new Reference($id), $tag['method']];
} else {
// If no method is defined, fallback to use __invoke
$processor = new Reference($id);
if (!empty($tag['channel']) && !empty($tag['handler'])) {
throw new \InvalidArgumentException(\sprintf('You cannot specify both the "handler" and "channel" attributes for the "monolog.processor" tag on service "%s".', $reference));
}

if (!empty($tag['handler'])) {
$parentDef = $container->findDefinition(\sprintf('monolog.handler.%s', $tag['handler']));
$definitions = [$parentDef];
while (!$parentDef->getClass() && $parentDef instanceof ChildDefinition) {
$parentDef = $container->findDefinition($parentDef->getParent());
}
$class = $container->getParameterBag()->resolveValue($parentDef->getClass());
if (!method_exists($class, 'pushProcessor')) {
throw new \InvalidArgumentException(\sprintf('The "%s" handler does not accept processors.', $tag['handler']));
}
} elseif (!empty($tag['channel'])) {
$loggerId = 'app' === $tag['channel'] ? 'monolog.logger' : \sprintf('monolog.logger.%s', $tag['channel']);
$definitions = [$container->getDefinition($loggerId)];
} elseif ($loggerIds = $container->findTaggedServiceIds('monolog.channel_logger')) {
$definitions = [];
foreach ($loggerIds as $loggerId => $tags) {
$definitions[] = $container->getDefinition($loggerId);
}
} else {
$definitions = [$container->getDefinition('monolog.logger_prototype')];
}

if (!empty($tag['method'])) {
$processor = [$reference, $tag['method']];
} else {
// If no method is defined, fallback to use __invoke
$processor = $reference;
}
foreach ($definitions as $definition) {
$definition->addMethodCall('pushProcessor', [$processor]);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/Compiler/LoggerChannelPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ protected function createLogger(string $channel, string $loggerId, ContainerBuil
if (!\in_array($channel, $this->channels)) {
$logger = new ChildDefinition('monolog.logger_prototype');
$logger->replaceArgument(0, $channel);
$logger->addTag('monolog.channel_logger');
$container->setDefinition($loggerId, $logger);
$this->channels[] = $channel;
}
Expand Down
79 changes: 73 additions & 6 deletions tests/DependencyInjection/Compiler/AddProcessorsPassTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;

class AddProcessorsPassTest extends TestCase
{
Expand All @@ -34,12 +35,40 @@ public function testHandlerProcessors()
$service = $container->getDefinition('monolog.handler.test');
$calls = $service->getMethodCalls();
$this->assertCount(1, $calls);
$this->assertEquals(['pushProcessor', [new Reference('test')]], $calls[0]);
$this->assertEquals(['pushProcessor', [new TypedReference('test', 'TestClass')]], $calls[0]);

$service = $container->getDefinition('handler_test');
$calls = $service->getMethodCalls();
$this->assertCount(1, $calls);
$this->assertEquals(['pushProcessor', [new Reference('test2')]], $calls[0]);
$this->assertEquals(['pushProcessor', [new TypedReference('test2', 'TestClass')]], $calls[0]);

$service = $container->getDefinition('monolog.handler.priority_test');
$calls = $service->getMethodCalls();
$this->assertCount(5, $calls);
$this->assertEquals(['pushProcessor', [new TypedReference('processor-10', 'TestClass')]], $calls[0]);
$this->assertEquals(['pushProcessor', [new TypedReference('processor+10', 'TestClass')]], $calls[1]);
$this->assertEquals(['pushProcessor', [new TypedReference('processor+20', 'TestClass')]], $calls[2]);
$this->assertEquals(['pushProcessor', [new TypedReference('processor+20', 'TestClass')]], $calls[2]);
$this->assertEquals(['pushProcessor', [new TypedReference('processor+25+35', 'TestClass')]], $calls[3]);
$this->assertEquals(['pushProcessor', [new TypedReference('processor+35+25', 'TestClass')]], $calls[4]);

$service = $container->getDefinition('monolog.handler.priority_test_2');
$calls = $service->getMethodCalls();
$this->assertCount(2, $calls);
$this->assertEquals(['pushProcessor', [new TypedReference('processor+35+25', 'TestClass')]], $calls[0]);
$this->assertEquals(['pushProcessor', [new TypedReference('processor+25+35', 'TestClass')]], $calls[1]);

$service = $container->getDefinition('monolog.logger');
$calls = $service->getMethodCalls();
$this->assertCount(2, $calls);
$this->assertEquals(['useMicrosecondTimestamps', ['%monolog.use_microseconds%']], $calls[0]);
$this->assertEquals(['pushProcessor', [new TypedReference('processor_all_channels+0', 'TestClass')]], $calls[1]);

$service = $container->getDefinition('monolog.logger.test');
$calls = $service->getMethodCalls();
$this->assertCount(2, $calls);
$this->assertEquals(['pushProcessor', [new TypedReference('processor_test_channel-25', 'TestClass')]], $calls[0]);
$this->assertEquals(['pushProcessor', [new TypedReference('processor_all_channels+0', 'TestClass')]], $calls[1]);
}

public function testFailureOnHandlerWithoutPushProcessor()
Expand Down Expand Up @@ -97,7 +126,7 @@ public static function provideEmptyTagsData(): iterable
{
yield 'with empty tag' => [
[[]],
[['pushProcessor', [new Reference('TestClass')]], ['useMicrosecondTimestamps', ['%monolog.use_microseconds%']]],
[['useMicrosecondTimestamps', ['%monolog.use_microseconds%']], ['pushProcessor', [new Reference('TestClass')]]],
[['pushProcessor', [new Reference('TestClass')]]],
];

Expand All @@ -115,7 +144,7 @@ public static function provideEmptyTagsData(): iterable

yield 'with method and no channel' => [
[[], ['method' => 'foo']],
[['pushProcessor', [[new Reference('TestClass'), 'foo']]], ['useMicrosecondTimestamps', ['%monolog.use_microseconds%']]],
[['useMicrosecondTimestamps', ['%monolog.use_microseconds%']], ['pushProcessor', [[new Reference('TestClass'), 'foo']]]],
[['pushProcessor', [[new Reference('TestClass'), 'foo']]]],
];
}
Expand All @@ -126,24 +155,62 @@ protected function getContainer()
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../../config'));
$loader->load('monolog.php');

$container->setParameter('monolog.additional_channels', ['test']);
$container->setParameter('monolog.handlers_to_channels', []);

$definition = $container->getDefinition('monolog.logger_prototype');
$container->setParameter('monolog.handler.console.class', ConsoleHandler::class);
$container->setDefinition('monolog.handler.test', new Definition('%monolog.handler.console.class%', [100, false]));
$container->setDefinition('handler_test', new Definition('%monolog.handler.console.class%', [100, false]));
$container->setDefinition('monolog.handler.priority_test', new Definition('%monolog.handler.console.class%', [100, false]));
$container->setDefinition('monolog.handler.priority_test_2', new Definition('%monolog.handler.console.class%', [100, false]));
$container->setAlias('monolog.handler.test2', 'handler_test');
$definition->addMethodCall('pushHandler', [new Reference('monolog.handler.test')]);
$definition->addMethodCall('pushHandler', [new Reference('monolog.handler.test2')]);
$definition->addMethodCall('pushHandler', [new Reference('monolog.handler.priority_test')]);
$definition->addMethodCall('pushHandler', [new Reference('monolog.handler.priority_test_2')]);

$service = new Definition('TestClass', ['false', new Reference('logger')]);
$service = new Definition('TestClass');
$service->addTag('monolog.processor', ['handler' => 'test']);
$container->setDefinition('test', $service);

$service = new Definition('TestClass', ['false', new Reference('logger')]);
$service = new Definition('TestClass');
$service->addTag('monolog.processor', ['handler' => 'test2']);
$container->setDefinition('test2', $service);

$service = new Definition('TestClass');
$service->addTag('monolog.processor', ['handler' => 'priority_test', 'priority' => 10]);
$container->setDefinition('processor+10', $service);

$service = new Definition('TestClass');
$service->addTag('monolog.processor', ['handler' => 'priority_test', 'priority' => -10]);
$container->setDefinition('processor-10', $service);

$service = new Definition('TestClass');
$service->addTag('monolog.processor', ['handler' => 'priority_test', 'priority' => 20]);
$container->setDefinition('processor+20', $service);

$service = new Definition('TestClass');
$service->addTag('monolog.processor', ['handler' => 'priority_test', 'priority' => 35]);
$service->addTag('monolog.processor', ['handler' => 'priority_test_2', 'priority' => 25]);
$container->setDefinition('processor+35+25', $service);

$service = new Definition('TestClass');
$service->addTag('monolog.processor', ['handler' => 'priority_test', 'priority' => 25]);
$service->addTag('monolog.processor', ['handler' => 'priority_test_2', 'priority' => 35]);
$container->setDefinition('processor+25+35', $service);

$service = new Definition('TestClass');
$service->addTag('monolog.processor', ['priority' => 0]);
$container->setDefinition('processor_all_channels+0', $service);

$service = new Definition('TestClass');
$service->addTag('monolog.processor', ['channel' => 'test', 'priority' => -25]);
$container->setDefinition('processor_test_channel-25', $service);

$container->getCompilerPassConfig()->setOptimizationPasses([]);
$container->getCompilerPassConfig()->setRemovingPasses([]);
$container->addCompilerPass(new LoggerChannelPass());
$container->addCompilerPass(new AddProcessorsPass());
$container->compile();

Expand Down