diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 99bd3690..51c3022e 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -15,6 +15,7 @@ use Monolog\Handler\SyslogUdpHandler; use Monolog\Level; use Monolog\Logger; +use Symfony\Bundle\MonologBundle\DependencyInjection\Handler\HandlerExtensionInterface; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -349,6 +350,14 @@ */ final class Configuration implements ConfigurationInterface { + /** + * @param list $handlerExtensions + */ + public function __construct( + private array $handlerExtensions = [], + ) { + } + /** * Generates the configuration tree builder. */ @@ -593,7 +602,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->end(); $this->addGelfSection($handlerNode); - $this->addMongoDBSection($handlerNode); $this->addElasticsearchSection($handlerNode); $this->addRedisSection($handlerNode); $this->addPredisSection($handlerNode); @@ -601,116 +609,76 @@ public function getConfigTreeBuilder(): TreeBuilder $this->addVerbosityLevelSection($handlerNode); $this->addChannelsSection($handlerNode); + $types = []; + \assert($handlerNode instanceof ArrayNodeDefinition); + foreach ($this->handlerExtensions as $handlerExtension) { + $types[$handlerExtension->getName()] = []; + $arrayNode = $handlerNode->children()->arrayNode($handlerExtension->getName()); + $handlerExtension->buildArrayNode($arrayNode); + foreach ($arrayNode->getChildNodeDefinitions() as $name => $childNodeDefinition) { + $types[$handlerExtension->getName()][] = $name; + } + } + + foreach ($this->handlerExtensions as $handlerExtension) { + $handlerExtension->buildHandlerValidates($handlerNode); + } + $handlerNode + ->beforeNormalization() + ->always(static function ($v) use ($types) { + if (!\array_key_exists('type', $v)) { + $handlerFields = array_intersect_key($types, $v); + if (0 === \count($handlerFields)) { + throw new InvalidConfigurationException('The handler type could not be determined automatically. Please specify the "type" option.'); + } + + if (1 < \count($handlerFields)) { + throw new InvalidConfigurationException('The handler type could not be determined automatically as multiple handler types are configured: '.implode(', ', array_keys($handlerFields)).'. Please specify the "type" option.'); + } + + $v['type'] = key($handlerFields); + } elseif (isset($types[$type = $v['type'] ?? 'null'])) { + // Migrate legacy flat config: move type-specific fields into + // the dedicated sub-node so that per-type defaults apply even + // when the legacy values rely on defaults. + $v[$type] ??= []; + foreach ($types[$type] as $field) { + if (isset($v[$field])) { + $v[$type][$field] = $v[$field]; + unset($v[$field]); + } elseif (str_ends_with($field, 's')) { + // XML keys produced by fixXmlConfig() use the singular + // form of the field, with dashes for underscores. + $singular = substr($field, 0, -1); + foreach ([$singular, str_replace('_', '-', $singular)] as $xmlKey) { + if (isset($v[$xmlKey])) { + $v[$type][$field] = $v[$xmlKey]; + unset($v[$xmlKey]); + break; + } + } + } + } + } + + return $v; + }) + ->end() ->validate() ->ifTrue(static function ($v) { return 'service' === $v['type'] && !empty($v['formatter']); }) ->thenInvalid('Service handlers can not have a formatter configured in the bundle, you must reconfigure the service itself instead') ->end() - ->validate() - ->ifTrue(static function ($v) { return \in_array($v['type'], ['fingers_crossed', 'buffer', 'filter', 'deduplication', 'sampling'], true) && empty($v['handler']); }) - ->thenInvalid('The handler has to be specified to use a FingersCrossedHandler, BufferHandler, FilterHandler, DeduplicationHandler or SamplingHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'fingers_crossed' === $v['type'] && !empty($v['excluded_http_codes']) && !empty($v['activation_strategy']); }) - ->thenInvalid('You can not use excluded_http_codes together with a custom activation_strategy in a FingersCrossedHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'fingers_crossed' !== $v['type'] && !empty($v['excluded_http_codes']); }) - ->thenInvalid('You can only use excluded_http_codes with a FingersCrossedHandler definition') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'filter' === $v['type'] && 'DEBUG' !== $v['min_level'] && !empty($v['accepted_levels']); }) - ->thenInvalid('You can not use min_level together with accepted_levels in a FilterHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'filter' === $v['type'] && 'EMERGENCY' !== $v['max_level'] && !empty($v['accepted_levels']); }) - ->thenInvalid('You can not use max_level together with accepted_levels in a FilterHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'rollbar' === $v['type'] && !empty($v['id']) && !empty($v['token']); }) - ->thenInvalid('You can not use both an id and a token in a RollbarHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'rollbar' === $v['type'] && empty($v['id']) && empty($v['token']); }) - ->thenInvalid('The id or the token has to be specified to use a RollbarHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'telegram' === $v['type'] && (empty($v['token']) || empty($v['channel'])); }) - ->thenInvalid('The token and channel have to be specified to use a TelegramBotHandler') - ->end() ->validate() ->ifTrue(static function ($v) { return 'service' === $v['type'] && !isset($v['id']); }) ->thenInvalid('The id has to be specified to use a service as handler') ->end() ->validate() - ->ifTrue(static function ($v) { return 'syslogudp' === $v['type'] && !isset($v['host']); }) - ->thenInvalid('The host has to be specified to use a syslogudp as handler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'socket' === $v['type'] && !isset($v['connection_string']); }) - ->thenInvalid('The connection_string has to be specified to use a SocketHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'pushover' === $v['type'] && (empty($v['token']) || empty($v['user'])); }) - ->thenInvalid('The token and user have to be specified to use a PushoverHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'slack' === $v['type'] && (empty($v['token']) || empty($v['channel'])); }) - ->thenInvalid('The token and channel have to be specified to use a SlackHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'slackwebhook' === $v['type'] && (empty($v['webhook_url'])); }) - ->thenInvalid('The webhook_url have to be specified to use a SlackWebhookHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'cube' === $v['type'] && empty($v['url']); }) - ->thenInvalid('The url has to be specified to use a CubeHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'amqp' === $v['type'] && empty($v['exchange']); }) - ->thenInvalid('The exchange has to be specified to use a AmqpHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'loggly' === $v['type'] && empty($v['token']); }) - ->thenInvalid('The token has to be specified to use a LogglyHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'loggly' === $v['type'] && !empty($v['tags']); }) - ->then(static function ($v) { - $invalidTags = preg_grep('/^[a-z0-9][a-z0-9\.\-_]*$/i', $v['tags'], \PREG_GREP_INVERT); - if (!empty($invalidTags)) { - throw new InvalidConfigurationException(\sprintf('The following Loggly tags are invalid: "%s".', implode('", "', $invalidTags))); - } + ->ifTrue(static function ($v) { + $interactiveOnly = $v['console']['interactive_only'] ?? $v['interactive_only'] ?? false; - return $v; + return $interactiveOnly && version_compare(InstalledVersions::getVersion('symfony/monolog-bridge'), '7.4.0', '<'); }) - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'logentries' === $v['type'] && empty($v['token']); }) - ->thenInvalid('The token has to be specified to use a LogEntriesHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'insightops' === $v['type'] && empty($v['token']); }) - ->thenInvalid('The token has to be specified to use a InsightOpsHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'flowdock' === $v['type'] && empty($v['token']); }) - ->thenInvalid('The token has to be specified to use a FlowdockHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'flowdock' === $v['type'] && empty($v['from_email']); }) - ->thenInvalid('The from_email has to be specified to use a FlowdockHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'flowdock' === $v['type'] && empty($v['source']); }) - ->thenInvalid('The source has to be specified to use a FlowdockHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'server_log' === $v['type'] && empty($v['host']); }) - ->thenInvalid('The host has to be specified to use a ServerLogHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return $v['interactive_only'] && version_compare(InstalledVersions::getVersion('symfony/monolog-bridge'), '7.4.0', '<'); }) ->thenInvalid('The interactive_only flag requires symfony/monolog-bridge 7.4 or higher') ->end() ; @@ -743,46 +711,6 @@ private function addGelfSection(ArrayNodeDefinition $handlerNode): void ->end() ->end() ->end() - ->validate() - ->ifTrue(static function ($v) { return 'gelf' === $v['type'] && !isset($v['publisher']); }) - ->thenInvalid('The publisher has to be specified to use a GelfHandler') - ->end() - ; - } - - private function addMongoDBSection(ArrayNodeDefinition $handlerNode): void - { - $handlerNode - ->children() - ->arrayNode('mongodb') - ->canBeUnset() - ->beforeNormalization() - ->ifString() - ->then(static function ($v) { return ['id' => $v]; }) - ->end() - ->children() - ->scalarNode('id') - ->info('ID of a MongoDB\Client service') - ->example('doctrine_mongodb.odm.logs_connection') - ->end() - ->scalarNode('uri')->end() - ->scalarNode('username')->end() - ->scalarNode('password')->end() - ->scalarNode('database')->defaultValue('monolog')->end() - ->scalarNode('collection')->defaultValue('logs')->end() - ->end() - ->validate() - ->ifTrue(static function ($v) { - return !isset($v['id']) && !isset($v['uri']); - }) - ->thenInvalid('The "mongodb" handler configuration requires either a service "id" or a connection "uri".') - ->end() - ->end() - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'mongodb' === $v['type'] && !isset($v['mongodb']); }) - ->thenInvalid('The "mongodb" configuration has to be specified to use a "mongodb" handler type.') - ->end() ; } @@ -921,14 +849,6 @@ private function addMailerSection(ArrayNodeDefinition $handlerNode): void ->end() ->end() ->end() - ->validate() - ->ifTrue(static function ($v) { return 'native_mailer' === $v['type'] && (empty($v['from_email']) || empty($v['to_email']) || empty($v['subject'])); }) - ->thenInvalid('The sender, recipient and subject have to be specified to use a NativeMailerHandler') - ->end() - ->validate() - ->ifTrue(static function ($v) { return 'symfony_mailer' === $v['type'] && empty($v['email_prototype']) && (empty($v['from_email']) || empty($v['to_email']) || empty($v['subject'])); }) - ->thenInvalid('The sender, recipient and subject or an email prototype have to be specified to use the Symfony MailerHandler') - ->end() ; } diff --git a/src/DependencyInjection/Handler/AmqpHandlerExtension.php b/src/DependencyInjection/Handler/AmqpHandlerExtension.php new file mode 100644 index 00000000..3c408d3f --- /dev/null +++ b/src/DependencyInjection/Handler/AmqpHandlerExtension.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\AmqpHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class AmqpHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'amqp'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('exchange')->end() + ->scalarNode('exchange_name')->defaultValue('log')->end() + ->end() + ->example([ + 'exchange' => 'exchange.service_id', + 'exchange_name' => 'log', + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'amqp' === $v['type'] && empty($v['amqp']['exchange'] ?? null); }) + ->thenInvalid('The exchange has to be specified to use a AmqpHandler') + ->end() + ; + } + + /** + * @param array{ + * exchange: string|null, + * exchange_name: string, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(AmqpHandler::class); + $definition->setArguments([ + new Reference($handler['exchange']), + $handler['exchange_name'], + $config['level'], + $config['bubble'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/BufferHandlerExtension.php b/src/DependencyInjection/Handler/BufferHandlerExtension.php new file mode 100644 index 00000000..58b59bf1 --- /dev/null +++ b/src/DependencyInjection/Handler/BufferHandlerExtension.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\BufferHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class BufferHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'buffer'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('handler')->end() + ->scalarNode('buffer_size')->defaultValue(0)->end() + ->booleanNode('flush_on_overflow')->defaultFalse()->end() + ->end() + ->example([ + 'handler' => 'nested', + 'buffer_size' => 0, + 'flush_on_overflow' => false, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'buffer' === $v['type'] && empty($v['buffer']['handler'] ?? null); }) + ->thenInvalid('The handler has to be specified to use a FingersCrossedHandler, BufferHandler, FilterHandler, DeduplicationHandler or SamplingHandler') + ->end() + ; + } + + /** + * @param array{ + * handler: string|null, + * buffer_size: int, + * flush_on_overflow: bool, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $nestedHandlerId = $context->getHandlerId($handler['handler']); + $context->markNestedHandler($nestedHandlerId); + + $definition = new Definition(BufferHandler::class); + $definition->setArguments([ + new Reference($nestedHandlerId), + $handler['buffer_size'], + $config['level'], + $config['bubble'], + $handler['flush_on_overflow'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/ConsoleHandlerExtension.php b/src/DependencyInjection/Handler/ConsoleHandlerExtension.php new file mode 100644 index 00000000..ebf1a885 --- /dev/null +++ b/src/DependencyInjection/Handler/ConsoleHandlerExtension.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Logger; +use Symfony\Bridge\Monolog\Handler\ConsoleHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\DependencyInjection\Definition; + +class ConsoleHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'console'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->arrayNode('verbosity_levels') + ->beforeNormalization() + ->ifArray() + ->then(static function ($v) { + $map = []; + $verbosities = [ + // allow numeric indexed array with ascending verbosity + 0 => 'VERBOSITY_QUIET', + 1 => 'VERBOSITY_NORMAL', + 2 => 'VERBOSITY_VERBOSE', + 3 => 'VERBOSITY_VERY_VERBOSE', + 4 => 'VERBOSITY_DEBUG', + // or array indexed by verbosity constants + OutputInterface::VERBOSITY_QUIET => 'VERBOSITY_QUIET', + OutputInterface::VERBOSITY_NORMAL => 'VERBOSITY_NORMAL', + OutputInterface::VERBOSITY_VERBOSE => 'VERBOSITY_VERBOSE', + OutputInterface::VERBOSITY_VERY_VERBOSE => 'VERBOSITY_VERY_VERBOSE', + OutputInterface::VERBOSITY_DEBUG => 'VERBOSITY_DEBUG', + ]; + // allow numeric indexed array with ascending verbosity, verbosity constants + // or lowercase names of the constants as keys + foreach ($v as $verbosity => $level) { + if (\is_int($verbosity) && isset($verbosities[$verbosity])) { + $map[$verbosities[$verbosity]] = $level; + } else { + $map[strtoupper($verbosity)] = $level; + } + } + + return $map; + }) + ->end() + ->children() + ->scalarNode('VERBOSITY_QUIET')->defaultValue('ERROR')->end() + ->scalarNode('VERBOSITY_NORMAL')->defaultValue('WARNING')->end() + ->scalarNode('VERBOSITY_VERBOSE')->defaultValue('NOTICE')->end() + ->scalarNode('VERBOSITY_VERY_VERBOSE')->defaultValue('INFO')->end() + ->scalarNode('VERBOSITY_DEBUG')->defaultValue('DEBUG')->end() + ->end() + ->validate() + ->always(static function ($v) { + $map = []; + foreach ($v as $verbosity => $level) { + $verbosityConstant = OutputInterface::class.'::'.$verbosity; + + if (!\defined($verbosityConstant)) { + throw new InvalidConfigurationException(\sprintf('The configured verbosity "%s" is invalid as it is not defined in Symfony\Component\Console\Output\OutputInterface.', $verbosity)); + } + + try { + $level = Logger::toMonologLevel($level)->value; + } catch (\Psr\Log\InvalidArgumentException $e) { + throw new InvalidConfigurationException(\sprintf('The configured minimum log level "%s" for verbosity "%s" is invalid as it is not defined in Monolog\Logger.', $level, $verbosity)); + } + + $map[\constant($verbosityConstant)] = $level; + } + + return $map; + }) + ->end() + ->end() + ->variableNode('console_formatter_options') + ->defaultValue([]) + ->validate() + ->ifTrue(static function ($v) { return !\is_array($v); }) + ->thenInvalid('The console_formatter_options must be an array.') + ->end() + ->end() + ->booleanNode('interactive_only')->defaultFalse()->end() + ->end() + ->example([ + 'verbosity_levels' => [ + 'VERBOSITY_NORMAL' => 'WARNING', + 'VERBOSITY_VERBOSE' => 'NOTICE', + ], + 'console_formatter_options' => [], + 'interactive_only' => false, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + // No specific validation + } + + /** + * @param array{ + * verbosity_levels: array, + * console_formatter_options: array, + * interactive_only: bool, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(ConsoleHandler::class); + $definition->setArguments([ + null, + $config['bubble'], + $handler['verbosity_levels'] ?? [], + $handler['console_formatter_options'], + $handler['interactive_only'], + ]); + $definition->addTag('kernel.event_subscriber'); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/CubeHandlerExtension.php b/src/DependencyInjection/Handler/CubeHandlerExtension.php new file mode 100644 index 00000000..622276fe --- /dev/null +++ b/src/DependencyInjection/Handler/CubeHandlerExtension.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\CubeHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class CubeHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'cube'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('url')->end() + ->end() + ->example([ + 'url' => 'udp://127.0.0.1:1180', + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'cube' === $v['type'] && empty($v['cube']['url'] ?? null); }) + ->thenInvalid('The url has to be specified to use a CubeHandler') + ->end() + ; + } + + /** + * @param array{ + * url: string|null, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(CubeHandler::class); + $definition->setArguments([ + $handler['url'], + $config['level'], + $config['bubble'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/DeduplicationHandlerExtension.php b/src/DependencyInjection/Handler/DeduplicationHandlerExtension.php new file mode 100644 index 00000000..9ccf0bbc --- /dev/null +++ b/src/DependencyInjection/Handler/DeduplicationHandlerExtension.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\DeduplicationHandler; +use Monolog\Level; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class DeduplicationHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'deduplication'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('handler')->end() + ->scalarNode('store')->defaultNull()->end() + ->scalarNode('deduplication_level')->defaultValue(Level::Error->value)->end() + ->scalarNode('time')->defaultValue(60)->end() + ->end() + ->example([ + 'handler' => 'nested', + 'store' => null, + 'deduplication_level' => Level::Error->value, + 'time' => 60, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'deduplication' === $v['type'] && empty($v['deduplication']['handler'] ?? null); }) + ->thenInvalid('The handler has to be specified to use a FingersCrossedHandler, BufferHandler, FilterHandler, DeduplicationHandler or SamplingHandler') + ->end() + ; + } + + /** + * @param array{ + * handler: string|null, + * store: string|null, + * deduplication_level: string|int, + * time: int, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $nestedHandlerId = $context->getHandlerId($handler['handler']); + $context->markNestedHandler($nestedHandlerId); + $defaultStore = '%kernel.cache_dir%/monolog_dedup_'.sha1($context->handlerId); + + $definition = new Definition(DeduplicationHandler::class); + $definition->setArguments([ + new Reference($nestedHandlerId), + $handler['store'] ?? $defaultStore, + $handler['deduplication_level'], + $handler['time'], + $config['bubble'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/DefaultHandlerExtension.php b/src/DependencyInjection/Handler/DefaultHandlerExtension.php new file mode 100644 index 00000000..10f0ab2a --- /dev/null +++ b/src/DependencyInjection/Handler/DefaultHandlerExtension.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\AbstractHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +/** + * Handlers using the constructor of {@see AbstractHandler} without adding their own arguments. + */ +final class DefaultHandlerExtension implements HandlerExtensionInterface +{ + /** + * @param non-empty-string $name + * @param class-string $handlerClass + */ + public function __construct( + private string $name, + private string $handlerClass, + ) { + } + + public function getName(): string + { + return $this->name; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + // No specific configuration + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + // No specific validation + } + + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition($this->handlerClass); + $definition->setArguments([ + $config['level'], + $config['bubble'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/ErrorLogHandlerExtension.php b/src/DependencyInjection/Handler/ErrorLogHandlerExtension.php new file mode 100644 index 00000000..775e6c58 --- /dev/null +++ b/src/DependencyInjection/Handler/ErrorLogHandlerExtension.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\ErrorLogHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class ErrorLogHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'error_log'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('message_type')->defaultValue(0)->end() + ->booleanNode('expand_newlines')->defaultFalse()->end() + ->end() + ->example([ + 'message_type' => 0, + 'expand_newlines' => false, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + // No specific validation + } + + /** + * @param array{ + * message_type: int, + * expand_newlines: bool, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(ErrorLogHandler::class); + $definition->setArguments([ + $handler['message_type'], + $config['level'], + $config['bubble'], + $handler['expand_newlines'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/FilterHandlerExtension.php b/src/DependencyInjection/Handler/FilterHandlerExtension.php new file mode 100644 index 00000000..aaf49af7 --- /dev/null +++ b/src/DependencyInjection/Handler/FilterHandlerExtension.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\FilterHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class FilterHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'filter'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('handler')->end() + ->arrayNode('accepted_levels') + ->canBeUnset() + ->prototype('scalar')->end() + ->end() + ->scalarNode('min_level')->defaultValue('DEBUG')->end() + ->scalarNode('max_level')->defaultValue('EMERGENCY')->end() + ->end() + ->example([ + 'handler' => 'nested', + 'accepted_levels' => ['debug', 'info'], + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'filter' === $v['type'] && empty($v['filter']['handler'] ?? null); }) + ->thenInvalid('The handler has to be specified to use a FingersCrossedHandler, BufferHandler, FilterHandler, DeduplicationHandler or SamplingHandler') + ->end() + ->validate() + ->ifTrue(static function ($v) { return 'filter' === $v['type'] && 'DEBUG' !== ($v['filter']['min_level'] ?? 'DEBUG') && !empty($v['filter']['accepted_levels'] ?? []); }) + ->thenInvalid('You can not use min_level together with accepted_levels in a FilterHandler') + ->end() + ->validate() + ->ifTrue(static function ($v) { return 'filter' === $v['type'] && 'EMERGENCY' !== ($v['filter']['max_level'] ?? 'EMERGENCY') && !empty($v['filter']['accepted_levels'] ?? []); }) + ->thenInvalid('You can not use max_level together with accepted_levels in a FilterHandler') + ->end() + ; + } + + /** + * @param array{ + * handler: string|null, + * accepted_levels?: list, + * min_level: string|int, + * max_level: string|int, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $nestedHandlerId = $context->getHandlerId($handler['handler']); + $context->markNestedHandler($nestedHandlerId); + $minLevelOrList = !empty($handler['accepted_levels']) ? $handler['accepted_levels'] : $handler['min_level']; + + $definition = new Definition(FilterHandler::class); + $definition->setArguments([ + new Reference($nestedHandlerId), + $minLevelOrList, + $handler['max_level'], + $config['bubble'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/FingersCrossedHandlerExtension.php b/src/DependencyInjection/Handler/FingersCrossedHandlerExtension.php new file mode 100644 index 00000000..d34d3214 --- /dev/null +++ b/src/DependencyInjection/Handler/FingersCrossedHandlerExtension.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; +use Monolog\Handler\FingersCrossedHandler; +use Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class FingersCrossedHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'fingers_crossed'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('handler')->end() + ->scalarNode('action_level')->defaultValue('WARNING')->end() + ->scalarNode('activation_strategy')->defaultNull()->end() + ->scalarNode('buffer_size')->defaultValue(0)->end() + ->booleanNode('stop_buffering')->defaultTrue()->end() + ->scalarNode('passthru_level')->defaultNull()->end() + ->arrayNode('excluded_http_codes') + ->info('Only for "fingers_crossed" handler type') + ->example([403, 404, [400 => ['^/foo', '^/bar']]]) + ->canBeUnset() + ->beforeNormalization() + ->always(static function ($values) { + if (false === $values) { + return false; + } + + return array_map(static function ($value) { + if (\is_array($value)) { + return isset($value['code']) ? $value : ['code' => key($value), 'urls' => current($value)]; + } + + return ['code' => $value, 'urls' => []]; + }, $values); + }) + ->end() + ->prototype('array') + ->children() + ->scalarNode('code')->end() + ->arrayNode('urls') + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->example([ + 'handler' => 'nested', + 'action_level' => 'WARNING', + 'buffer_size' => 0, + 'stop_buffering' => true, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'fingers_crossed' === $v['type'] && empty($v['fingers_crossed']['handler'] ?? null); }) + ->thenInvalid('The handler has to be specified to use a FingersCrossedHandler, BufferHandler, FilterHandler, DeduplicationHandler or SamplingHandler') + ->end() + ->validate() + ->ifTrue(static function ($v) { return 'fingers_crossed' === $v['type'] && !empty($v['fingers_crossed']['excluded_http_codes'] ?? []) && !empty($v['fingers_crossed']['activation_strategy'] ?? null); }) + ->thenInvalid('You can not use excluded_http_codes together with a custom activation_strategy in a FingersCrossedHandler') + ->end() + ; + } + + /** + * @param array{ + * handler: string|null, + * action_level: string|int, + * activation_strategy: string|null, + * buffer_size: int, + * stop_buffering: bool, + * passthru_level: string|int|null, + * excluded_http_codes?: list}>, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $nestedHandlerId = $context->getHandlerId($handler['handler']); + $context->markNestedHandler($nestedHandlerId); + + $activation = new Definition(ErrorLevelActivationStrategy::class, [$handler['action_level']]); + + if (isset($handler['activation_strategy'])) { + $activation = new Reference($handler['activation_strategy']); + } elseif (!empty($handler['excluded_http_codes'])) { + $activationDef = new Definition(HttpCodeActivationStrategy::class, [ + new Reference('request_stack'), + $handler['excluded_http_codes'], + $activation, + ]); + $context->container->setDefinition($context->handlerId.'.http_code_strategy', $activationDef); + $activation = new Reference($context->handlerId.'.http_code_strategy'); + } + + $definition = new Definition(FingersCrossedHandler::class); + $definition->setArguments([ + new Reference($nestedHandlerId), + $activation, + $handler['buffer_size'], + $config['bubble'], + $handler['stop_buffering'], + $handler['passthru_level'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/FlowdockHandlerExtension.php b/src/DependencyInjection/Handler/FlowdockHandlerExtension.php new file mode 100644 index 00000000..9211d0ab --- /dev/null +++ b/src/DependencyInjection/Handler/FlowdockHandlerExtension.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Formatter\FlowdockFormatter; +use Monolog\Handler\FlowdockHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class FlowdockHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'flowdock'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('token')->end() + ->scalarNode('source')->end() + ->scalarNode('from_email')->end() + ->end() + ->example([ + 'token' => 'example token', + 'source' => 'app', + 'from_email' => 'app@example.com', + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'flowdock' === $v['type'] && empty($v['flowdock']['token'] ?? null); }) + ->thenInvalid('The token has to be specified to use a FlowdockHandler') + ->end() + ->validate() + ->ifTrue(static function ($v) { return 'flowdock' === $v['type'] && empty($v['flowdock']['from_email'] ?? null); }) + ->thenInvalid('The from_email has to be specified to use a FlowdockHandler') + ->end() + ->validate() + ->ifTrue(static function ($v) { return 'flowdock' === $v['type'] && empty($v['flowdock']['source'] ?? null); }) + ->thenInvalid('The source has to be specified to use a FlowdockHandler') + ->end() + ; + } + + /** + * @param array{ + * token: string|null, + * source: string|null, + * from_email: string|null, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(FlowdockHandler::class); + $definition->setArguments([ + $handler['token'], + $config['level'], + $config['bubble'], + ]); + + if (empty($config['formatter'])) { + $formatter = new Definition(FlowdockFormatter::class, [ + $handler['source'], + $handler['from_email'], + ]); + $formatterId = 'monolog.flowdock.formatter.'.sha1($handler['source'].'|'.$handler['from_email']); + $formatter->setPublic(false); + $context->container->setDefinition($formatterId, $formatter); + + $definition->addMethodCall('setFormatter', [new Reference($formatterId)]); + } + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/GelfHandlerExtension.php b/src/DependencyInjection/Handler/GelfHandlerExtension.php new file mode 100644 index 00000000..c9f5c453 --- /dev/null +++ b/src/DependencyInjection/Handler/GelfHandlerExtension.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\GelfHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class GelfHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'gelf'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->arrayNode('publisher') + ->canBeUnset() + ->beforeNormalization() + ->ifString() + ->then(static function ($v) { return ['id' => $v]; }) + ->end() + ->children() + ->scalarNode('id')->end() + ->scalarNode('hostname')->end() + ->scalarNode('port')->defaultValue(12201)->end() + ->scalarNode('chunk_size')->defaultValue(1420)->end() + ->enumNode('encoder')->values(['json', 'compressed_json'])->end() + ->end() + ->validate() + ->ifTrue(static function ($v) { + return !isset($v['id']) && !isset($v['hostname']); + }) + ->thenInvalid('What must be set is either the hostname or the id.') + ->end() + ->end() + ->end() + ->example([ + 'publisher' => [ + 'hostname' => 'localhost', + 'port' => 12201, + 'chunk_size' => 1420, + ], + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'gelf' === $v['type'] && !isset($v['gelf']['publisher']); }) + ->thenInvalid('The publisher has to be specified to use a GelfHandler') + ->end() + ; + } + + /** + * @param array{ + * publisher: array{ + * id?: string, + * hostname?: string, + * port: int, + * chunk_size: int, + * encoder?: 'json'|'compressed_json', + * }|string, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + if (isset($handler['publisher']['id'])) { + $publisher = new Reference($handler['publisher']['id']); + } elseif (class_exists(\Gelf\Transport\UdpTransport::class)) { + $transport = new Definition(\Gelf\Transport\UdpTransport::class, [ + $handler['publisher']['hostname'], + $handler['publisher']['port'], + $handler['publisher']['chunk_size'], + ]); + $transport->setPublic(false); + + if (isset($handler['publisher']['encoder'])) { + if ('compressed_json' === $handler['publisher']['encoder']) { + $encoderClass = \Gelf\Encoder\CompressedJsonEncoder::class; + } elseif ('json' === $handler['publisher']['encoder']) { + $encoderClass = \Gelf\Encoder\JsonEncoder::class; + } else { + throw new \RuntimeException('The gelf message encoder must be either "compressed_json" or "json".'); + } + + $encoder = new Definition($encoderClass); + $encoder->setPublic(false); + + $transport->addMethodCall('setMessageEncoder', [$encoder]); + } + + $publisher = new Definition(\Gelf\Publisher::class, []); + $publisher->addMethodCall('addTransport', [$transport]); + $publisher->setPublic(false); + } else { + throw new \RuntimeException('The gelf handler requires the graylog2/gelf-php package to be installed.'); + } + + $definition = new Definition(GelfHandler::class); + $definition->setArguments([ + $publisher, + $config['level'], + $config['bubble'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/GroupHandlerExtension.php b/src/DependencyInjection/Handler/GroupHandlerExtension.php new file mode 100644 index 00000000..f3c9b86b --- /dev/null +++ b/src/DependencyInjection/Handler/GroupHandlerExtension.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\HandlerInterface; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Handlers grouping several nested handlers (group, whatfailuregroup, fallbackgroup). + */ +final class GroupHandlerExtension implements HandlerExtensionInterface +{ + /** + * @param non-empty-string $name + * @param class-string $handlerClass + */ + public function __construct( + private string $name, + private string $handlerClass, + ) { + } + + public function getName(): string + { + return $this->name; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->arrayNode('members') + ->canBeUnset() + ->performNoDeepMerging() + ->prototype('scalar')->end() + ->end() + ->end() + ->example([ + 'members' => ['handler_a', 'handler_b'], + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + // No specific validation + } + + /** + * @param array{ + * members: list, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $references = []; + foreach ($handler['members'] as $nestedHandler) { + if ($context->isHandlerDisabled($nestedHandler)) { + // a disabled handler is not registered as a service, skip it + continue; + } + $nestedHandlerId = $context->getHandlerId($nestedHandler); + $context->markNestedHandler($nestedHandlerId); + $references[] = new Reference($nestedHandlerId); + } + + $definition = new Definition($this->handlerClass); + $definition->setArguments([ + $references, + $config['bubble'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/HandlerContext.php b/src/DependencyInjection/Handler/HandlerContext.php new file mode 100644 index 00000000..0c6c403c --- /dev/null +++ b/src/DependencyInjection/Handler/HandlerContext.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Context passed to handler extensions when building a handler definition. + * + * @internal + */ +final class HandlerContext +{ + /** + * @param \Closure(string): void $markNestedHandler + * @param \Closure(string): bool $isHandlerDisabled + */ + public function __construct( + public readonly ContainerBuilder $container, + public readonly string $handlerId, + public readonly string $handlerType, + private readonly \Closure $markNestedHandler, + private readonly \Closure $isHandlerDisabled, + ) { + } + + /** + * Returns the service id of a handler service by its configuration name. + */ + public function getHandlerId(string $name): string + { + return \sprintf('monolog.handler.%s', $name); + } + + /** + * Marks a handler as nested so it is never pushed on loggers directly. + */ + public function markNestedHandler(string $handlerId): void + { + ($this->markNestedHandler)($handlerId); + } + + /** + * Whether the given handler is disabled in the current configuration. + */ + public function isHandlerDisabled(string $name): bool + { + return ($this->isHandlerDisabled)($name); + } +} diff --git a/src/DependencyInjection/Handler/HandlerExtensionInterface.php b/src/DependencyInjection/Handler/HandlerExtensionInterface.php new file mode 100644 index 00000000..2d34788f --- /dev/null +++ b/src/DependencyInjection/Handler/HandlerExtensionInterface.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\DependencyInjection\Handler; + +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +interface HandlerExtensionInterface +{ + /** + * Returns the name of the handler and configuration subtree. + */ + public function getName(): string; + + /** + * Add properties for the handler type. + */ + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void; + + /** + * Add validates on the handler node, referencing the final "type" and the + * type-specific subtree added in {@see buildArrayNode()}. + */ + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void; + + /** + * @param array{ + * level: string|int, + * bubble: bool, + * formatter: string|null, + * } $config Generic options + * @param array{} $handler Specific handler options + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition; +} diff --git a/src/DependencyInjection/Handler/InsightOpsHandlerExtension.php b/src/DependencyInjection/Handler/InsightOpsHandlerExtension.php new file mode 100644 index 00000000..e2354684 --- /dev/null +++ b/src/DependencyInjection/Handler/InsightOpsHandlerExtension.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\InsightOpsHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class InsightOpsHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'insightops'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('token')->end() + ->scalarNode('region')->end() + ->booleanNode('use_ssl')->defaultTrue()->end() + ->end() + ->example([ + 'token' => 'example token', + 'region' => null, + 'use_ssl' => true, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'insightops' === $v['type'] && empty($v['insightops']['token'] ?? null); }) + ->thenInvalid('The token has to be specified to use a InsightOpsHandler') + ->end() + ; + } + + /** + * @param array{ + * token: string|null, + * region?: string|null, + * use_ssl: bool, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(InsightOpsHandler::class); + $definition->setArguments([ + $handler['token'], + $handler['region'] ?: 'us', + $handler['use_ssl'], + $config['level'], + $config['bubble'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/KernelResponseHandlerExtension.php b/src/DependencyInjection/Handler/KernelResponseHandlerExtension.php new file mode 100644 index 00000000..7857a5bd --- /dev/null +++ b/src/DependencyInjection/Handler/KernelResponseHandlerExtension.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\AbstractHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +/** + * Handlers listening on the "kernel.response" event (firephp, chromephp). + */ +final class KernelResponseHandlerExtension implements HandlerExtensionInterface +{ + /** + * @param non-empty-string $name + * @param class-string $handlerClass + */ + public function __construct( + private string $name, + private string $handlerClass, + ) { + } + + public function getName(): string + { + return $this->name; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + // No specific configuration + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + // No specific validation + } + + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition($this->handlerClass); + $definition->setArguments([ + $config['level'], + $config['bubble'], + ]); + $definition->addTag('kernel.event_listener', ['event' => 'kernel.response', 'method' => 'onKernelResponse']); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/LogEntriesHandlerExtension.php b/src/DependencyInjection/Handler/LogEntriesHandlerExtension.php new file mode 100644 index 00000000..bd5d1dec --- /dev/null +++ b/src/DependencyInjection/Handler/LogEntriesHandlerExtension.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\LogEntriesHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class LogEntriesHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'logentries'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('token')->end() + ->booleanNode('use_ssl')->defaultTrue()->end() + ->scalarNode('timeout')->end() + ->scalarNode('connection_timeout')->end() + ->end() + ->example([ + 'token' => 'example token', + 'use_ssl' => true, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'logentries' === $v['type'] && empty($v['logentries']['token'] ?? null); }) + ->thenInvalid('The token has to be specified to use a LogEntriesHandler') + ->end() + ; + } + + /** + * @param array{ + * token: string|null, + * use_ssl: bool, + * timeout?: mixed, + * connection_timeout?: mixed, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(LogEntriesHandler::class); + $definition->setArguments([ + $handler['token'], + $handler['use_ssl'], + $config['level'], + $config['bubble'], + ]); + if (isset($handler['timeout'])) { + $definition->addMethodCall('setTimeout', [$handler['timeout']]); + } + if (isset($handler['connection_timeout'])) { + $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); + } + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/LogglyHandlerExtension.php b/src/DependencyInjection/Handler/LogglyHandlerExtension.php new file mode 100644 index 00000000..3c92fa49 --- /dev/null +++ b/src/DependencyInjection/Handler/LogglyHandlerExtension.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\LogglyHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\DependencyInjection\Definition; + +class LogglyHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'loggly'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('token')->end() + ->arrayNode('tags') + ->beforeNormalization() + ->ifString() + ->then(static function ($v) { return explode(',', $v); }) + ->end() + ->beforeNormalization() + ->ifArray() + ->then(static function ($v) { return array_filter(array_map('trim', $v)); }) + ->end() + ->prototype('scalar')->end() + ->end() + ->end() + ->example([ + 'token' => 'example token', + 'tags' => ['foo', 'bar'], + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'loggly' === $v['type'] && empty($v['loggly']['token'] ?? null); }) + ->thenInvalid('The token has to be specified to use a LogglyHandler') + ->end() + ->validate() + ->ifTrue(static function ($v) { return 'loggly' === $v['type'] && !empty($v['loggly']['tags'] ?? []); }) + ->then(static function ($v) { + $invalidTags = preg_grep('/^[a-z0-9][a-z0-9\.\-_]*$/i', $v['loggly']['tags'], \PREG_GREP_INVERT); + if (!empty($invalidTags)) { + throw new InvalidConfigurationException(\sprintf('The following Loggly tags are invalid: "%s".', implode('", "', $invalidTags))); + } + + return $v; + }) + ->end() + ; + } + + /** + * @param array{ + * token: string|null, + * tags?: list, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(LogglyHandler::class); + $definition->setArguments([ + $handler['token'], + $config['level'], + $config['bubble'], + ]); + if (!empty($handler['tags'])) { + $definition->addMethodCall('setTag', [implode(',', $handler['tags'])]); + } + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/MongoDBHandlerExtension.php b/src/DependencyInjection/Handler/MongoDBHandlerExtension.php new file mode 100644 index 00000000..e69e3309 --- /dev/null +++ b/src/DependencyInjection/Handler/MongoDBHandlerExtension.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use MongoDB\Client; +use Monolog\Formatter\MongoDBFormatter; +use Monolog\Handler\MongoDBHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +final class MongoDBHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'mongodb'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->beforeNormalization() + ->ifString() + ->then(function ($v) { return ['id' => $v]; }) + ->end() + ->children() + ->scalarNode('id') + ->info('ID of a MongoDB\Client service') + ->example('doctrine_mongodb.odm.logs_connection') + ->end() + ->scalarNode('uri')->end() + ->scalarNode('username')->end() + ->scalarNode('password')->end() + ->scalarNode('database')->defaultValue('monolog')->end() + ->scalarNode('collection')->defaultValue('logs')->end() + ->end() + ->validate() + ->ifTrue(function ($v) { + return !isset($v['id']) && !isset($v['uri']); + }) + ->thenInvalid('The "mongodb" handler configuration requires either a service "id" or a connection "uri".') + ->end() + ->example([ + 'uri' => 'mongodb://localhost:27017', + 'database' => 'monolog', + 'collection' => 'logs', + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'mongodb' === $v['type'] && !isset($v['mongodb']); }) + ->thenInvalid('The "mongodb" configuration has to be specified to use a "mongodb" handler type.') + ->end() + ; + } + + /** + * @param array{ + * id?: string, + * uri?: string, + * username?: string, + * password?: string, + * database: string, + * collection: string, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(MongoDBHandler::class); + + if (!class_exists(Client::class)) { + throw new \RuntimeException('The "mongodb" handler requires the mongodb/mongodb package to be installed.'); + } + + if (isset($handler['id'])) { + $client = new Reference($handler['id']); + } else { + $uriOptions = ['appname' => 'monolog-bundle']; + + if (isset($handler['username'])) { + $uriOptions['username'] = $handler['username']; + } + + if (isset($handler['password'])) { + $uriOptions['password'] = $handler['password']; + } + + $client = new Definition(Client::class, [ + $handler['uri'], + $uriOptions, + ]); + } + + $definition->setArguments([ + $client, + $handler['database'], + $handler['collection'], + $config['level'], + $config['bubble'], + ]); + + if (empty($config['formatter'])) { + $formatter = new Definition(MongoDBFormatter::class); + $definition->addMethodCall('setFormatter', [$formatter]); + } + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/NativeMailerHandlerExtension.php b/src/DependencyInjection/Handler/NativeMailerHandlerExtension.php new file mode 100644 index 00000000..bf24b185 --- /dev/null +++ b/src/DependencyInjection/Handler/NativeMailerHandlerExtension.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\NativeMailerHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class NativeMailerHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'native_mailer'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('from_email')->end() + ->arrayNode('to_email') + ->prototype('scalar')->end() + ->beforeNormalization() + ->ifString() + ->then(static function ($v) { return [$v]; }) + ->end() + ->end() + ->scalarNode('subject')->end() + ->arrayNode('headers') + ->canBeUnset() + ->scalarPrototype()->end() + ->end() + ->arrayNode('parameters') + ->canBeUnset() + ->scalarPrototype() + ->validate() + ->ifTrue(static function ($v) { return false !== strpbrk($v, "\r\n"); }) + ->thenInvalid('Mailer parameters can not contain newline characters for security reasons.') + ->end() + ->end() + ->end() + ->end() + ->example([ + 'from_email' => 'from@example.com', + 'to_email' => 'to@example.com', + 'subject' => 'subject', + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'native_mailer' === $v['type'] && (empty($v['native_mailer']['from_email'] ?? null) || empty($v['native_mailer']['to_email'] ?? null) || empty($v['native_mailer']['subject'] ?? null)); }) + ->thenInvalid('The sender, recipient and subject have to be specified to use a NativeMailerHandler') + ->end() + ; + } + + /** + * @param array{ + * from_email: string|list|null, + * to_email: list|null, + * subject: string|list|null, + * headers?: list, + * parameters?: list, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(NativeMailerHandler::class); + $definition->setArguments([ + $handler['to_email'], + $handler['subject'], + $handler['from_email'], + $config['level'], + $config['bubble'], + ]); + if (!empty($handler['headers'])) { + $definition->addMethodCall('addHeader', [$handler['headers']]); + } + if (!empty($handler['parameters'])) { + $definition->addMethodCall('addParameter', [$handler['parameters']]); + } + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/NewRelicHandlerExtension.php b/src/DependencyInjection/Handler/NewRelicHandlerExtension.php new file mode 100644 index 00000000..96e49c4e --- /dev/null +++ b/src/DependencyInjection/Handler/NewRelicHandlerExtension.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\NewRelicHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class NewRelicHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'newrelic'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('app_name')->defaultNull()->end() + ->end() + ->example([ + 'app_name' => null, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + // No specific validation + } + + /** + * @param array{ + * app_name: string|null, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(NewRelicHandler::class); + $definition->setArguments([ + $config['level'], + $config['bubble'], + $handler['app_name'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/PushoverHandlerExtension.php b/src/DependencyInjection/Handler/PushoverHandlerExtension.php new file mode 100644 index 00000000..866357a4 --- /dev/null +++ b/src/DependencyInjection/Handler/PushoverHandlerExtension.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\PushoverHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class PushoverHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'pushover'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('token')->end() + ->variableNode('user') + ->validate() + ->ifTrue(static function ($v) { + return !\is_string($v) && !\is_array($v); + }) + ->thenInvalid('User must be a string or an array.') + ->end() + ->end() + ->scalarNode('title')->defaultNull()->end() + ->scalarNode('timeout')->end() + ->scalarNode('connection_timeout')->end() + ->end() + ->example([ + 'token' => 'pushover api token', + 'user' => 'user id or array of ids', + 'title' => null, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'pushover' === $v['type'] && (empty($v['pushover']['token'] ?? null) || empty($v['pushover']['user'] ?? null)); }) + ->thenInvalid('The token and user have to be specified to use a PushoverHandler') + ->end() + ; + } + + /** + * @param array{ + * token: string|null, + * user: string|list|null, + * title: string|null, + * timeout?: mixed, + * connection_timeout?: mixed, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(PushoverHandler::class); + $definition->setArguments([ + $handler['token'], + $handler['user'], + $handler['title'], + $config['level'], + $config['bubble'], + ]); + if (isset($handler['timeout'])) { + $definition->addMethodCall('setTimeout', [$handler['timeout']]); + } + if (isset($handler['connection_timeout'])) { + $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); + } + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/RollbarHandlerExtension.php b/src/DependencyInjection/Handler/RollbarHandlerExtension.php new file mode 100644 index 00000000..d416cd70 --- /dev/null +++ b/src/DependencyInjection/Handler/RollbarHandlerExtension.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\RollbarHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class RollbarHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'rollbar'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('id')->end() + ->scalarNode('token')->end() + ->arrayNode('config') + ->canBeUnset() + ->prototype('scalar')->end() + ->end() + ->end() + ->example([ + 'token' => 'rollbar api token', + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'rollbar' === $v['type'] && !empty($v['rollbar']['id'] ?? null) && !empty($v['rollbar']['token'] ?? null); }) + ->thenInvalid('You can not use both an id and a token in a RollbarHandler') + ->end() + ->validate() + ->ifTrue(static function ($v) { return 'rollbar' === $v['type'] && empty($v['rollbar']['id'] ?? null) && empty($v['rollbar']['token'] ?? null); }) + ->thenInvalid('The id or the token has to be specified to use a RollbarHandler') + ->end() + ; + } + + /** + * @param array{ + * id?: string, + * token?: string, + * config: array, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + if (!empty($handler['id'])) { + $rollbarId = $handler['id']; + } else { + $rollbarConfig = $handler['config'] ?: []; + $rollbarConfig['access_token'] = $handler['token']; + $rollbar = new Definition(\Rollbar\RollbarLogger::class, [ + $rollbarConfig, + ]); + $rollbarId = 'monolog.rollbar.notifier.'.sha1(json_encode($rollbarConfig)); + $rollbar->setPublic(false); + $context->container->setDefinition($rollbarId, $rollbar); + } + + $definition = new Definition(RollbarHandler::class); + $definition->setArguments([ + new Reference($rollbarId), + $config['level'], + $config['bubble'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/RotatingFileHandlerExtension.php b/src/DependencyInjection/Handler/RotatingFileHandlerExtension.php new file mode 100644 index 00000000..c1e5e621 --- /dev/null +++ b/src/DependencyInjection/Handler/RotatingFileHandlerExtension.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\RotatingFileHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class RotatingFileHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'rotating_file'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('path')->defaultValue('%kernel.logs_dir%/%kernel.environment%.log')->end() + ->scalarNode('max_files')->defaultValue(0)->end() + ->scalarNode('file_permission') + ->defaultNull() + ->beforeNormalization() + ->ifString() + ->then(static function ($v) { + if (str_starts_with($v, '0')) { + return octdec($v); + } + + return (int) $v; + }) + ->end() + ->end() + ->booleanNode('use_locking')->defaultFalse()->end() + ->scalarNode('filename_format')->defaultValue('{filename}-{date}')->end() + ->scalarNode('date_format')->defaultValue('Y-m-d')->end() + ->end() + ->example([ + 'path' => '%kernel.logs_dir%/%kernel.environment%.log', + 'max_files' => 0, + 'file_permission' => null, + 'use_locking' => false, + 'filename_format' => '{filename}-{date}', + 'date_format' => 'Y-m-d', + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + // No specific validation + } + + /** + * @param array{ + * path: string, + * max_files: int, + * file_permission: int|null, + * use_locking: bool, + * filename_format: string, + * date_format: string, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(RotatingFileHandler::class); + $definition->setArguments([ + $handler['path'], + $handler['max_files'], + $config['level'], + $config['bubble'], + $handler['file_permission'], + $handler['use_locking'], + ]); + $definition->addMethodCall('setFilenameFormat', [ + $handler['filename_format'], + $handler['date_format'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/SamplingHandlerExtension.php b/src/DependencyInjection/Handler/SamplingHandlerExtension.php new file mode 100644 index 00000000..5371d0af --- /dev/null +++ b/src/DependencyInjection/Handler/SamplingHandlerExtension.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\SamplingHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class SamplingHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'sampling'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('handler')->end() + ->integerNode('factor')->defaultValue(1)->min(1)->end() + ->end() + ->example([ + 'handler' => 'nested', + 'factor' => 10, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'sampling' === $v['type'] && empty($v['sampling']['handler'] ?? null); }) + ->thenInvalid('The handler has to be specified to use a FingersCrossedHandler, BufferHandler, FilterHandler, DeduplicationHandler or SamplingHandler') + ->end() + ; + } + + /** + * @param array{ + * handler: string|null, + * factor: int, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $nestedHandlerId = $context->getHandlerId($handler['handler']); + $context->markNestedHandler($nestedHandlerId); + + $definition = new Definition(SamplingHandler::class); + $definition->setArguments([ + new Reference($nestedHandlerId), + $handler['factor'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/ServerLogHandlerExtension.php b/src/DependencyInjection/Handler/ServerLogHandlerExtension.php new file mode 100644 index 00000000..78d4a242 --- /dev/null +++ b/src/DependencyInjection/Handler/ServerLogHandlerExtension.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Symfony\Bridge\Monolog\Handler\ServerLogHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class ServerLogHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'server_log'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('host')->end() + ->end() + ->example([ + 'host' => '127.0.0.1:9911', + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'server_log' === $v['type'] && empty($v['server_log']['host'] ?? null); }) + ->thenInvalid('The host has to be specified to use a ServerLogHandler') + ->end() + ; + } + + /** + * @param array{ + * host: string|null, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(ServerLogHandler::class); + $definition->setArguments([ + $handler['host'], + $config['level'], + $config['bubble'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/SlackHandlerExtension.php b/src/DependencyInjection/Handler/SlackHandlerExtension.php new file mode 100644 index 00000000..8b675ec2 --- /dev/null +++ b/src/DependencyInjection/Handler/SlackHandlerExtension.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\SlackHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class SlackHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'slack'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('token')->end() + ->scalarNode('channel')->end() + ->scalarNode('bot_name')->defaultValue('Monolog')->end() + ->scalarNode('use_attachment')->defaultTrue()->end() + ->scalarNode('icon_emoji')->defaultNull()->end() + ->scalarNode('use_short_attachment')->defaultFalse()->end() + ->scalarNode('include_extra')->defaultFalse()->end() + ->arrayNode('exclude_fields') + ->canBeUnset() + ->prototype('scalar')->end() + ->end() + ->scalarNode('timeout')->end() + ->scalarNode('connection_timeout')->end() + ->end() + ->example([ + 'token' => 'slack api token', + 'channel' => '#logs', + 'bot_name' => 'Monolog', + 'use_attachment' => true, + 'icon_emoji' => null, + 'use_short_attachment' => false, + 'include_extra' => false, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'slack' === $v['type'] && (empty($v['slack']['token'] ?? null) || empty($v['slack']['channel'] ?? null)); }) + ->thenInvalid('The token and channel have to be specified to use a SlackHandler') + ->end() + ; + } + + /** + * @param array{ + * token: string|null, + * channel: string|null, + * bot_name: string, + * use_attachment: bool, + * icon_emoji: string|null, + * use_short_attachment: bool, + * include_extra: bool, + * exclude_fields: list, + * timeout?: mixed, + * connection_timeout?: mixed, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(SlackHandler::class); + $definition->setArguments([ + $handler['token'], + $handler['channel'], + $handler['bot_name'], + $handler['use_attachment'], + $handler['icon_emoji'], + $config['level'], + $config['bubble'], + $handler['use_short_attachment'], + $handler['include_extra'], + $handler['exclude_fields'], + ]); + if (isset($handler['timeout'])) { + $definition->addMethodCall('setTimeout', [$handler['timeout']]); + } + if (isset($handler['connection_timeout'])) { + $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); + } + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/SlackWebhookHandlerExtension.php b/src/DependencyInjection/Handler/SlackWebhookHandlerExtension.php new file mode 100644 index 00000000..43f89120 --- /dev/null +++ b/src/DependencyInjection/Handler/SlackWebhookHandlerExtension.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\SlackWebhookHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class SlackWebhookHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'slackwebhook'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('webhook_url')->end() + ->scalarNode('channel')->end() + ->scalarNode('bot_name')->defaultValue('Monolog')->end() + ->scalarNode('icon_emoji')->defaultNull()->end() + ->scalarNode('use_attachment')->defaultTrue()->end() + ->scalarNode('use_short_attachment')->defaultFalse()->end() + ->scalarNode('include_extra')->defaultFalse()->end() + ->arrayNode('exclude_fields') + ->canBeUnset() + ->prototype('scalar')->end() + ->end() + ->end() + ->example([ + 'webhook_url' => 'https://hooks.slack.com/services/...', + 'channel' => '#logs', + 'bot_name' => 'Monolog', + 'icon_emoji' => null, + 'use_attachment' => true, + 'use_short_attachment' => false, + 'include_extra' => false, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'slackwebhook' === $v['type'] && empty($v['slackwebhook']['webhook_url'] ?? null); }) + ->thenInvalid('The webhook_url have to be specified to use a SlackWebhookHandler') + ->end() + ; + } + + /** + * @param array{ + * webhook_url: string|null, + * channel: string|null, + * bot_name: string, + * icon_emoji: string|null, + * use_attachment: bool, + * use_short_attachment: bool, + * include_extra: bool, + * exclude_fields: list, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(SlackWebhookHandler::class); + $definition->setArguments([ + $handler['webhook_url'], + $handler['channel'], + $handler['bot_name'], + $handler['use_attachment'], + $handler['icon_emoji'], + $handler['use_short_attachment'], + $handler['include_extra'], + $config['level'], + $config['bubble'], + $handler['exclude_fields'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/SocketHandlerExtension.php b/src/DependencyInjection/Handler/SocketHandlerExtension.php new file mode 100644 index 00000000..424af057 --- /dev/null +++ b/src/DependencyInjection/Handler/SocketHandlerExtension.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\Handler; + +use Monolog\Handler\SocketHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class SocketHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'socket'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('connection_string')->end() + ->scalarNode('timeout')->end() + ->scalarNode('connection_timeout')->end() + ->booleanNode('persistent')->end() + ->end() + ->example([ + 'connection_string' => 'localhost:50505', + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'socket' === $v['type'] && empty($v['socket']['connection_string'] ?? null); }) + ->thenInvalid('The connection_string has to be specified to use a SocketHandler') + ->end() + ; + } + + /** + * @param array{ + * connection_string: string|null, + * timeout?: mixed, + * connection_timeout?: mixed, + * persistent?: bool, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(SocketHandler::class); + $definition->setArguments([ + $handler['connection_string'], + $config['level'], + $config['bubble'], + ]); + if (isset($handler['timeout'])) { + $definition->addMethodCall('setTimeout', [$handler['timeout']]); + } + if (isset($handler['connection_timeout'])) { + $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); + } + if (isset($handler['persistent'])) { + $definition->addMethodCall('setPersistent', [$handler['persistent']]); + } + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/StreamHandlerExtension.php b/src/DependencyInjection/Handler/StreamHandlerExtension.php new file mode 100644 index 00000000..5ab79391 --- /dev/null +++ b/src/DependencyInjection/Handler/StreamHandlerExtension.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\Handler; + +use Monolog\Handler\StreamHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class StreamHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'stream'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('path')->defaultValue('%kernel.logs_dir%/%kernel.environment%.log')->end() + ->scalarNode('file_permission') + ->defaultNull() + ->beforeNormalization() + ->ifString() + ->then(static function ($v) { + if (str_starts_with($v, '0')) { + return octdec($v); + } + + return (int) $v; + }) + ->end() + ->end() + ->booleanNode('use_locking')->defaultFalse()->end() + ->end() + ->example([ + 'path' => '%kernel.logs_dir%/%kernel.environment%.log', + 'file_permission' => null, + 'use_locking' => false, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + // No specific validation + } + + /** + * @param array{ + * path: string, + * file_permission: int|null, + * use_locking: bool, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(StreamHandler::class); + $definition->setArguments([ + $handler['path'], + $config['level'], + $config['bubble'], + $handler['file_permission'], + $handler['use_locking'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/SymfonyMailerHandlerExtension.php b/src/DependencyInjection/Handler/SymfonyMailerHandlerExtension.php new file mode 100644 index 00000000..a4b0b941 --- /dev/null +++ b/src/DependencyInjection/Handler/SymfonyMailerHandlerExtension.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Symfony\Bridge\Monolog\Handler\MailerHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class SymfonyMailerHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'symfony_mailer'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('from_email')->end() + ->arrayNode('to_email') + ->prototype('scalar')->end() + ->beforeNormalization() + ->ifString() + ->then(static function ($v) { return [$v]; }) + ->end() + ->end() + ->scalarNode('subject')->end() + ->scalarNode('content_type')->defaultNull()->end() + ->scalarNode('mailer')->defaultNull()->end() + ->arrayNode('email_prototype') + ->canBeUnset() + ->beforeNormalization() + ->ifString() + ->then(static function ($v) { return ['id' => $v]; }) + ->end() + ->children() + ->scalarNode('id')->isRequired()->end() + ->scalarNode('method')->defaultNull()->end() + ->end() + ->end() + ->end() + ->example([ + 'from_email' => 'from@example.com', + 'to_email' => 'to@example.com', + 'subject' => 'subject', + 'mailer' => 'mailer.mailer', + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { + return 'symfony_mailer' === $v['type'] + && empty($v['symfony_mailer']['email_prototype'] ?? null) + && (empty($v['symfony_mailer']['from_email'] ?? null) || empty($v['symfony_mailer']['to_email'] ?? null) || empty($v['symfony_mailer']['subject'] ?? null)); + }) + ->thenInvalid('The sender, recipient and subject or an email prototype have to be specified to use the Symfony MailerHandler') + ->end() + ; + } + + /** + * @param array{ + * from_email?: string|list, + * to_email?: list, + * subject?: string|list, + * mailer: string|null, + * email_prototype?: array{id: string, method: string|null}, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $mailer = $handler['mailer'] ?: 'mailer.mailer'; + if (isset($handler['email_prototype'])) { + if (!empty($handler['email_prototype']['method'])) { + $prototype = [new Reference($handler['email_prototype']['id']), $handler['email_prototype']['method']]; + } else { + $prototype = new Reference($handler['email_prototype']['id']); + } + } else { + $prototype = (new Definition(\Symfony\Component\Mime\Email::class)) + ->setPublic(false) + ->addMethodCall('from', [$handler['from_email']]) + ->addMethodCall('to', $handler['to_email']) + ->addMethodCall('subject', [$handler['subject']]); + } + + $definition = new Definition(MailerHandler::class); + $definition->setArguments([ + new Reference($mailer), + $prototype, + $config['level'], + $config['bubble'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/SyslogHandlerExtension.php b/src/DependencyInjection/Handler/SyslogHandlerExtension.php new file mode 100644 index 00000000..f09ac3e2 --- /dev/null +++ b/src/DependencyInjection/Handler/SyslogHandlerExtension.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\SyslogHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class SyslogHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'syslog'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('ident')->defaultValue('php')->end() + ->scalarNode('facility')->defaultValue('user')->end() + ->scalarNode('logopts')->defaultValue(\LOG_PID)->end() + ->end() + ->example([ + 'ident' => 'php', + 'facility' => 'user', + 'logopts' => \LOG_PID, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + // No specific validation + } + + /** + * @param array{ + * ident: string, + * facility: string, + * logopts: int, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(SyslogHandler::class); + $definition->setArguments([ + $handler['ident'], + $handler['facility'], + $config['level'], + $config['bubble'], + $handler['logopts'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/SyslogUdpHandlerExtension.php b/src/DependencyInjection/Handler/SyslogUdpHandlerExtension.php new file mode 100644 index 00000000..ff9dcc16 --- /dev/null +++ b/src/DependencyInjection/Handler/SyslogUdpHandlerExtension.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\SyslogUdpHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class SyslogUdpHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'syslogudp'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('host')->end() + ->scalarNode('port')->defaultValue(514)->end() + ->scalarNode('facility')->defaultValue('user')->end() + ->scalarNode('ident')->defaultValue('php')->end() + ->scalarNode('rfc')->defaultValue(SyslogUdpHandler::RFC5424)->end() + ->end() + ->example([ + 'host' => '127.0.0.1', + 'port' => 514, + 'facility' => 'user', + 'ident' => 'php', + 'rfc' => SyslogUdpHandler::RFC5424, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'syslogudp' === $v['type'] && empty($v['syslogudp']['host'] ?? null); }) + ->thenInvalid('The host has to be specified to use a syslogudp as handler') + ->end() + ; + } + + /** + * @param array{ + * host: string|null, + * port: int, + * facility: string, + * ident: string, + * rfc: int, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(SyslogUdpHandler::class); + $definition->setArguments([ + $handler['host'], + $handler['port'], + $handler['facility'], + $config['level'], + $config['bubble'], + $handler['ident'] ?: 'php', + $handler['rfc'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/Handler/TelegramBotHandlerExtension.php b/src/DependencyInjection/Handler/TelegramBotHandlerExtension.php new file mode 100644 index 00000000..1628f75b --- /dev/null +++ b/src/DependencyInjection/Handler/TelegramBotHandlerExtension.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MonologBundle\DependencyInjection\Handler; + +use Monolog\Handler\TelegramBotHandler; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class TelegramBotHandlerExtension implements HandlerExtensionInterface +{ + public function getName(): string + { + return 'telegram'; + } + + public function buildArrayNode(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->canBeUnset() + ->children() + ->scalarNode('token')->end() + ->scalarNode('channel')->defaultNull()->end() + ->scalarNode('parse_mode')->defaultNull()->end() + ->booleanNode('disable_webpage_preview')->defaultNull()->end() + ->booleanNode('disable_notification')->defaultNull()->end() + ->booleanNode('split_long_messages')->defaultFalse()->end() + ->booleanNode('delay_between_messages')->defaultFalse()->end() + ->integerNode('topic')->defaultNull()->end() + ->end() + ->example([ + 'token' => 'bot token', + 'channel' => '-100', + 'parse_mode' => null, + 'disable_webpage_preview' => null, + 'disable_notification' => null, + 'split_long_messages' => false, + 'delay_between_messages' => false, + 'topic' => null, + ]) + ; + } + + public function buildHandlerValidates(ArrayNodeDefinition $handlerNode): void + { + $handlerNode + ->validate() + ->ifTrue(static function ($v) { return 'telegram' === $v['type'] && (empty($v['telegram']['token'] ?? null) || empty($v['telegram']['channel'] ?? null)); }) + ->thenInvalid('The token and channel have to be specified to use a TelegramBotHandler') + ->end() + ; + } + + /** + * @param array{ + * token: string|null, + * channel: string|null, + * parse_mode: string|null, + * disable_webpage_preview: bool|null, + * disable_notification: bool|null, + * split_long_messages: bool, + * delay_between_messages: bool, + * topic: int|null, + * } $handler + */ + public function getDefinition(HandlerContext $context, array $config, array $handler): Definition + { + $definition = new Definition(TelegramBotHandler::class); + $definition->setArguments([ + $handler['token'], + $handler['channel'], + $config['level'], + $config['bubble'], + $handler['parse_mode'], + $handler['disable_webpage_preview'], + $handler['disable_notification'], + $handler['split_long_messages'], + $handler['delay_between_messages'], + $handler['topic'], + ]); + + return $definition; + } +} diff --git a/src/DependencyInjection/MonologExtension.php b/src/DependencyInjection/MonologExtension.php index 6cf1cccf..7691bc13 100644 --- a/src/DependencyInjection/MonologExtension.php +++ b/src/DependencyInjection/MonologExtension.php @@ -13,13 +13,18 @@ use Monolog\Attribute\AsMonologProcessor; use Monolog\Attribute\WithMonologChannel; -use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; +use Monolog\Handler; use Monolog\Handler\HandlerInterface; use Monolog\Processor\ProcessorInterface; use Monolog\Processor\PsrLogMessageProcessor; use Monolog\ResettableInterface; -use Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy; use Symfony\Bridge\Monolog\Processor\TokenProcessor; +use Symfony\Bridge\Monolog\Handler\ChromePhpHandler; +use Symfony\Bridge\Monolog\Handler\FirePHPHandler; +use Symfony\Bundle\MonologBundle\DependencyInjection\Handler as HandlerExtension; +use Symfony\Bundle\MonologBundle\DependencyInjection\Handler\HandlerContext; +use Symfony\Bundle\MonologBundle\DependencyInjection\Handler\HandlerExtensionInterface; +use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Argument\BoundArgument; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -44,6 +49,64 @@ final class MonologExtension extends Extension /** @var array */ private array $disabledHandlers = []; + /** + * @var array + */ + private array $handlerExtensions = []; + + public function __construct() + { + $this->addHandlerExtension(new HandlerExtension\DefaultHandlerExtension('null', Handler\NullHandler::class)); + $this->addHandlerExtension(new HandlerExtension\DefaultHandlerExtension('noop', Handler\NoopHandler::class)); + $this->addHandlerExtension(new HandlerExtension\DefaultHandlerExtension('debug', DebugHandler::class)); + $this->addHandlerExtension(new HandlerExtension\DefaultHandlerExtension('test', Handler\TestHandler::class)); + $this->addHandlerExtension(new HandlerExtension\DefaultHandlerExtension('browser_console', Handler\BrowserConsoleHandler::class)); + $this->addHandlerExtension(new HandlerExtension\KernelResponseHandlerExtension('firephp', FirePHPHandler::class)); + $this->addHandlerExtension(new HandlerExtension\KernelResponseHandlerExtension('chromephp', ChromePhpHandler::class)); + $this->addHandlerExtension(new HandlerExtension\MongoDBHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\StreamHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\RotatingFileHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\SocketHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\SyslogHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\SyslogUdpHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\CubeHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\ErrorLogHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\ServerLogHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\AmqpHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\LogEntriesHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\LogglyHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\InsightOpsHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\FlowdockHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\PushoverHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\TelegramBotHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\RollbarHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\NewRelicHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\ConsoleHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\SlackHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\SlackWebhookHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\NativeMailerHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\SymfonyMailerHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\GelfHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\FingersCrossedHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\FilterHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\BufferHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\DeduplicationHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\SamplingHandlerExtension()); + $this->addHandlerExtension(new HandlerExtension\GroupHandlerExtension('group', Handler\GroupHandler::class)); + $this->addHandlerExtension(new HandlerExtension\GroupHandlerExtension('whatfailuregroup', Handler\WhatFailureGroupHandler::class)); + $this->addHandlerExtension(new HandlerExtension\GroupHandlerExtension('fallbackgroup', Handler\FallbackGroupHandler::class)); + } + + public function addHandlerExtension(HandlerExtensionInterface $extension): void + { + $this->handlerExtensions[$extension->getName()] = $extension; + } + + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface + { + return new Configuration($this->handlerExtensions); + } + /** * Loads the Monolog configuration. * @@ -154,7 +217,8 @@ public function getNamespace(): string private function buildHandler(ContainerBuilder $container, string $name, array $handler): string { $handlerId = $this->getHandlerId($name); - if ('service' === $handler['type']) { + $handlerType = $handler['type']; + if ('service' === $handlerType) { $container->setAlias($handlerId, $handler['id']); if (!empty($handler['nested']) && true === $handler['nested']) { @@ -164,8 +228,13 @@ private function buildHandler(ContainerBuilder $container, string $name, array $ return $handlerId; } - $handlerClass = $this->getHandlerClassByType($handler['type']); - $definition = new Definition($handlerClass); + if (\array_key_exists($handlerType, $this->handlerExtensions)) { + $context = new HandlerContext($container, $handlerId, $handlerType, $this->markNestedHandler(...), $this->isHandlerDisabled(...)); + $definition = $this->handlerExtensions[$handlerType]->getDefinition($context, $handler, $handler[$handlerType] ?? []); + } else { + $handlerClass = $this->getHandlerClassByType($handlerType); + $definition = new Definition($handlerClass); + } if ($handler['include_stacktraces'] || null !== $handler['base_path']) { $configurator = new Definition(FormatterConfigurator::class, [ @@ -176,122 +245,24 @@ private function buildHandler(ContainerBuilder $container, string $name, array $ } if (null === $handler['process_psr_3_messages']['enabled']) { - $handler['process_psr_3_messages']['enabled'] = !isset($handler['handler']) && !$handler['members']; + $subNode = $handler[$handlerType] ?? []; + $handler['process_psr_3_messages']['enabled'] = !isset($handler['handler']) && !$handler['members'] + && empty($subNode['handler'] ?? null) && empty($subNode['members'] ?? null); } - if ($handler['process_psr_3_messages']['enabled'] && method_exists($handlerClass, 'pushProcessor')) { + if ($handler['process_psr_3_messages']['enabled'] && method_exists($definition->getClass(), 'pushProcessor')) { $processorId = $this->buildPsrLogMessageProcessor($container, $handler['process_psr_3_messages']); - $definition->addMethodCall('pushProcessor', [new Reference($processorId)]); + // The PSR-3 message processor must always be pushed first, before any + // handler specific method call added by the handler extension. + $methodCalls = $definition->getMethodCalls(); + array_unshift($methodCalls, ['pushProcessor', [new Reference($processorId)]]); + $definition->setMethodCalls($methodCalls); } - switch ($handler['type']) { - case 'stream': - $definition->setArguments([ - $handler['path'], - $handler['level'], - $handler['bubble'], - $handler['file_permission'], - $handler['use_locking'], - ]); - break; - - case 'console': - $definition->setArguments([ - null, - $handler['bubble'], - $handler['verbosity_levels'] ?? [], - $handler['console_formatter_options'], - $handler['interactive_only'], - ]); - $definition->addTag('kernel.event_subscriber'); - break; - - case 'chromephp': - case 'firephp': - $definition->setArguments([ - $handler['level'], - $handler['bubble'], - ]); - $definition->addTag('kernel.event_listener', ['event' => 'kernel.response', 'method' => 'onKernelResponse']); - break; - - case 'gelf': - if (isset($handler['publisher']['id'])) { - $publisher = new Reference($handler['publisher']['id']); - } elseif (class_exists(\Gelf\Transport\UdpTransport::class)) { - $transport = new Definition(\Gelf\Transport\UdpTransport::class, [ - $handler['publisher']['hostname'], - $handler['publisher']['port'], - $handler['publisher']['chunk_size'], - ]); - $transport->setPublic(false); - - if (isset($handler['publisher']['encoder'])) { - if ('compressed_json' === $handler['publisher']['encoder']) { - $encoderClass = \Gelf\Encoder\CompressedJsonEncoder::class; - } elseif ('json' === $handler['publisher']['encoder']) { - $encoderClass = \Gelf\Encoder\JsonEncoder::class; - } else { - throw new \RuntimeException('The gelf message encoder must be either "compressed_json" or "json".'); - } - - $encoder = new Definition($encoderClass); - $encoder->setPublic(false); - - $transport->addMethodCall('setMessageEncoder', [$encoder]); - } - - $publisher = new Definition(\Gelf\Publisher::class, []); - $publisher->addMethodCall('addTransport', [$transport]); - $publisher->setPublic(false); - } else { - throw new \RuntimeException('The gelf handler requires the graylog2/gelf-php package to be installed.'); - } - - $definition->setArguments([ - $publisher, - $handler['level'], - $handler['bubble'], - ]); - break; - - case 'mongodb': - if (!class_exists(\MongoDB\Client::class)) { - throw new \RuntimeException('The "mongodb" handler requires the mongodb/mongodb package to be installed.'); - } - - if (isset($handler['mongodb']['id'])) { - $client = new Reference($handler['mongodb']['id']); - } else { - $uriOptions = ['appname' => 'monolog-bundle']; - - if (isset($handler['mongodb']['username'])) { - $uriOptions['username'] = $handler['mongodb']['username']; - } - - if (isset($handler['mongodb']['password'])) { - $uriOptions['password'] = $handler['mongodb']['password']; - } - - $client = new Definition(\MongoDB\Client::class, [ - $handler['mongodb']['uri'], - $uriOptions, - ]); - } - - $definition->setArguments([ - $client, - $handler['mongodb']['database'], - $handler['mongodb']['collection'], - $handler['level'], - $handler['bubble'], - ]); - - if (empty($handler['formatter'])) { - $formatter = new Definition(\Monolog\Formatter\MongoDBFormatter::class); - $definition->addMethodCall('setFormatter', [$formatter]); - } - break; + switch ($handlerType) { + case 'elasticsearch': + trigger_deprecation('symfony/monolog-bundle', '3.8', 'The "elasticsearch" handler type is deprecated in MonologBundle since version 3.8.0, use the "elastica" type instead, or switch to the official Elastic client using the "elastic_search" type.'); + // no break case 'elastica': case 'elastic_search': @@ -353,21 +324,6 @@ private function buildHandler(ContainerBuilder $container, string $name, array $ ]); break; - case 'telegram': - $definition->setArguments([ - $handler['token'], - $handler['channel'], - $handler['level'], - $handler['bubble'], - $handler['parse_mode'], - $handler['disable_webpage_preview'], - $handler['disable_notification'], - $handler['split_long_messages'], - $handler['delay_between_messages'], - $handler['topic'], - ]); - break; - case 'redis': case 'predis': if (isset($handler['redis']['id'])) { @@ -406,386 +362,15 @@ private function buildHandler(ContainerBuilder $container, string $name, array $ ]); break; - case 'rotating_file': - $definition->setArguments([ - $handler['path'], - $handler['max_files'], - $handler['level'], - $handler['bubble'], - $handler['file_permission'], - $handler['use_locking'], - ]); - $definition->addMethodCall('setFilenameFormat', [ - $handler['filename_format'], - $handler['date_format'], - ]); - break; - - case 'fingers_crossed': - $nestedHandlerId = $this->getHandlerId($handler['handler']); - $this->markNestedHandler($nestedHandlerId); - - $activation = new Definition(ErrorLevelActivationStrategy::class, [$handler['action_level']]); - - if (isset($handler['activation_strategy'])) { - $activation = new Reference($handler['activation_strategy']); - } elseif (!empty($handler['excluded_http_codes'])) { - $activationDef = new Definition(HttpCodeActivationStrategy::class, [ - new Reference('request_stack'), - $handler['excluded_http_codes'], - $activation, - ]); - $container->setDefinition($handlerId.'.http_code_strategy', $activationDef); - $activation = new Reference($handlerId.'.http_code_strategy'); - } - - $definition->setArguments([ - new Reference($nestedHandlerId), - $activation, - $handler['buffer_size'], - $handler['bubble'], - $handler['stop_buffering'], - $handler['passthru_level'], - ]); - break; - - case 'filter': - $nestedHandlerId = $this->getHandlerId($handler['handler']); - $this->markNestedHandler($nestedHandlerId); - $minLevelOrList = !empty($handler['accepted_levels']) ? $handler['accepted_levels'] : $handler['min_level']; - - $definition->setArguments([ - new Reference($nestedHandlerId), - $minLevelOrList, - $handler['max_level'], - $handler['bubble'], - ]); - break; - - case 'buffer': - $nestedHandlerId = $this->getHandlerId($handler['handler']); - $this->markNestedHandler($nestedHandlerId); - - $definition->setArguments([ - new Reference($nestedHandlerId), - $handler['buffer_size'], - $handler['level'], - $handler['bubble'], - $handler['flush_on_overflow'], - ]); - break; - - case 'deduplication': - $nestedHandlerId = $this->getHandlerId($handler['handler']); - $this->markNestedHandler($nestedHandlerId); - $defaultStore = '%kernel.cache_dir%/monolog_dedup_'.sha1($handlerId); - - $definition->setArguments([ - new Reference($nestedHandlerId), - $handler['store'] ?? $defaultStore, - $handler['deduplication_level'], - $handler['time'], - $handler['bubble'], - ]); - break; - - case 'group': - case 'whatfailuregroup': - case 'fallbackgroup': - $references = []; - foreach ($handler['members'] as $nestedHandler) { - if (isset($this->disabledHandlers[$nestedHandler])) { - // a disabled handler is not registered as a service, skip it - continue; - } - $nestedHandlerId = $this->getHandlerId($nestedHandler); - $this->markNestedHandler($nestedHandlerId); - $references[] = new Reference($nestedHandlerId); - } - - $definition->setArguments([ - $references, - $handler['bubble'], - ]); - break; - - case 'syslog': - $definition->setArguments([ - $handler['ident'], - $handler['facility'], - $handler['level'], - $handler['bubble'], - $handler['logopts'], - ]); - break; - - case 'syslogudp': - $definition->setArguments([ - $handler['host'], - $handler['port'], - $handler['facility'], - $handler['level'], - $handler['bubble'], - $handler['ident'] ?: 'php', - $handler['rfc'], - ]); - break; - - case 'native_mailer': - $definition->setArguments([ - $handler['to_email'], - $handler['subject'], - $handler['from_email'], - $handler['level'], - $handler['bubble'], - ]); - if (!empty($handler['headers'])) { - $definition->addMethodCall('addHeader', [$handler['headers']]); - } - if (!empty($handler['parameters'])) { - $definition->addMethodCall('addParameter', [$handler['parameters']]); - } - break; - - case 'symfony_mailer': - $mailer = $handler['mailer'] ?: 'mailer.mailer'; - if (isset($handler['email_prototype'])) { - if (!empty($handler['email_prototype']['method'])) { - $prototype = [new Reference($handler['email_prototype']['id']), $handler['email_prototype']['method']]; - } else { - $prototype = new Reference($handler['email_prototype']['id']); + default: + if (!isset($this->handlerExtensions[$handlerType])) { + $nullWarning = ''; + if ('' == $handlerType) { + $nullWarning = ', if you meant to define a null handler in a yaml config, make sure you quote "null" so it does not get converted to a php null'; } - } else { - $prototype = (new Definition(\Symfony\Component\Mime\Email::class)) - ->setPublic(false) - ->addMethodCall('from', [$handler['from_email']]) - ->addMethodCall('to', $handler['to_email']) - ->addMethodCall('subject', [$handler['subject']]); - } - $definition->setArguments([ - new Reference($mailer), - $prototype, - $handler['level'], - $handler['bubble'], - ]); - break; - - case 'socket': - $definition->setArguments([ - $handler['connection_string'], - $handler['level'], - $handler['bubble'], - ]); - if (isset($handler['timeout'])) { - $definition->addMethodCall('setTimeout', [$handler['timeout']]); - } - if (isset($handler['connection_timeout'])) { - $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); - } - if (isset($handler['persistent'])) { - $definition->addMethodCall('setPersistent', [$handler['persistent']]); - } - break; - - case 'pushover': - $definition->setArguments([ - $handler['token'], - $handler['user'], - $handler['title'], - $handler['level'], - $handler['bubble'], - ]); - if (isset($handler['timeout'])) { - $definition->addMethodCall('setTimeout', [$handler['timeout']]); - } - if (isset($handler['connection_timeout'])) { - $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); - } - break; - - case 'slack': - $definition->setArguments([ - $handler['token'], - $handler['channel'], - $handler['bot_name'], - $handler['use_attachment'], - $handler['icon_emoji'], - $handler['level'], - $handler['bubble'], - $handler['use_short_attachment'], - $handler['include_extra'], - $handler['exclude_fields'], - ]); - if (isset($handler['timeout'])) { - $definition->addMethodCall('setTimeout', [$handler['timeout']]); - } - if (isset($handler['connection_timeout'])) { - $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); - } - break; - - case 'slackwebhook': - $definition->setArguments([ - $handler['webhook_url'], - $handler['channel'], - $handler['bot_name'], - $handler['use_attachment'], - $handler['icon_emoji'], - $handler['use_short_attachment'], - $handler['include_extra'], - $handler['level'], - $handler['bubble'], - $handler['exclude_fields'], - ]); - break; - - case 'cube': - $definition->setArguments([ - $handler['url'], - $handler['level'], - $handler['bubble'], - ]); - break; - case 'amqp': - $definition->setArguments([ - new Reference($handler['exchange']), - $handler['exchange_name'], - $handler['level'], - $handler['bubble'], - ]); - break; - - case 'error_log': - $definition->setArguments([ - $handler['message_type'], - $handler['level'], - $handler['bubble'], - $handler['expand_newlines'], - ]); - break; - - case 'loggly': - $definition->setArguments([ - $handler['token'], - $handler['level'], - $handler['bubble'], - ]); - if (!empty($handler['tags'])) { - $definition->addMethodCall('setTag', [implode(',', $handler['tags'])]); + throw new \InvalidArgumentException(\sprintf('Invalid handler type "%s" given for handler "%s".'.$nullWarning, $handler['type'], $name)); } - break; - - case 'logentries': - $definition->setArguments([ - $handler['token'], - $handler['use_ssl'], - $handler['level'], - $handler['bubble'], - ]); - if (isset($handler['timeout'])) { - $definition->addMethodCall('setTimeout', [$handler['timeout']]); - } - if (isset($handler['connection_timeout'])) { - $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); - } - break; - - case 'insightops': - $definition->setArguments([ - $handler['token'], - $handler['region'] ?: 'us', - $handler['use_ssl'], - $handler['level'], - $handler['bubble'], - ]); - break; - - case 'flowdock': - $definition->setArguments([ - $handler['token'], - $handler['level'], - $handler['bubble'], - ]); - - if (empty($handler['formatter'])) { - $formatter = new Definition(\Monolog\Formatter\FlowdockFormatter::class, [ - $handler['source'], - $handler['from_email'], - ]); - $formatterId = 'monolog.flowdock.formatter.'.sha1($handler['source'].'|'.$handler['from_email']); - $formatter->setPublic(false); - $container->setDefinition($formatterId, $formatter); - - $definition->addMethodCall('setFormatter', [new Reference($formatterId)]); - } - break; - - case 'rollbar': - if (!empty($handler['id'])) { - $rollbarId = $handler['id']; - } else { - $config = $handler['config'] ?: []; - $config['access_token'] = $handler['token']; - $rollbar = new Definition(\Rollbar\RollbarLogger::class, [ - $config, - ]); - $rollbarId = 'monolog.rollbar.notifier.'.sha1(json_encode($config)); - $rollbar->setPublic(false); - $container->setDefinition($rollbarId, $rollbar); - } - - $definition->setArguments([ - new Reference($rollbarId), - $handler['level'], - $handler['bubble'], - ]); - break; - - case 'newrelic': - $definition->setArguments([ - $handler['level'], - $handler['bubble'], - $handler['app_name'], - ]); - break; - - case 'server_log': - $definition->setArguments([ - $handler['host'], - $handler['level'], - $handler['bubble'], - ]); - break; - - case 'sampling': - $nestedHandlerId = $this->getHandlerId($handler['handler']); - $this->markNestedHandler($nestedHandlerId); - - $definition->setArguments([ - new Reference($nestedHandlerId), - $handler['factor'], - ]); - break; - - // Handlers using the constructor of AbstractHandler without adding their own arguments - case 'browser_console': - case 'test': - case 'null': - case 'noop': - $definition->setArguments([ - $handler['level'], - $handler['bubble'], - ]); - break; - - default: - $nullWarning = ''; - if ('' == $handler['type']) { - $nullWarning = ', if you meant to define a null handler in a yaml config, make sure you quote "null" so it does not get converted to a php null'; - } - - throw new \InvalidArgumentException(\sprintf('Invalid handler type "%s" given for handler "%s".'.$nullWarning, $handler['type'], $name)); } if (!empty($handler['nested']) && true === $handler['nested']) { @@ -796,7 +381,7 @@ private function buildHandler(ContainerBuilder $container, string $name, array $ $definition->addMethodCall('setFormatter', [new Reference($handler['formatter'])]); } - if (!\in_array($handlerId, $this->nestedHandlers) && is_subclass_of($handlerClass, ResettableInterface::class)) { + if (!\in_array($handlerId, $this->nestedHandlers) && is_subclass_of($definition->getClass(), ResettableInterface::class)) { $definition->addTag('kernel.reset', ['method' => 'reset']); } @@ -819,6 +404,11 @@ private function markNestedHandler(string $nestedHandlerId): void $this->nestedHandlers[] = $nestedHandlerId; } + private function isHandlerDisabled(string $name): bool + { + return isset($this->disabledHandlers[$name]); + } + private function getHandlerId(string $name): string { return \sprintf('monolog.handler.%s', $name); diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 464bba5c..58b56779 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -16,10 +16,10 @@ use Monolog\Level; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use Symfony\Bundle\MonologBundle\DependencyInjection\Configuration; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Bundle\MonologBundle\DependencyInjection\Configuration; class ConfigurationTest extends TestCase { diff --git a/tests/DependencyInjection/MonologExtensionTest.php b/tests/DependencyInjection/MonologExtensionTest.php index b33c0035..840ba4f5 100644 --- a/tests/DependencyInjection/MonologExtensionTest.php +++ b/tests/DependencyInjection/MonologExtensionTest.php @@ -72,6 +72,758 @@ public function testLoadWithCustomValues() $this->assertDICConstructorArguments($handler, ['/tmp/symfony.log', 'ERROR', false, 0666, true]); } + public function testStreamHandlerWithSubNodeConfig() + { + $container = $this->getContainer([['handlers' => [ + 'custom' => ['type' => 'stream', 'stream' => ['path' => '/tmp/sub.log', 'file_permission' => '0640', 'use_locking' => true], 'level' => 'WARNING', 'bubble' => false], + ]]]); + + $handler = $container->getDefinition('monolog.handler.custom'); + $this->assertDICDefinitionClass($handler, \Monolog\Handler\StreamHandler::class); + $this->assertDICConstructorArguments($handler, ['/tmp/sub.log', 'WARNING', false, 0640, true]); + } + + public function testStreamHandlerTypeInferredFromSubNode() + { + $container = $this->getContainer([['handlers' => [ + 'custom' => ['stream' => ['path' => '/tmp/inferred.log']], + ]]]); + + $handler = $container->getDefinition('monolog.handler.custom'); + $this->assertDICDefinitionClass($handler, \Monolog\Handler\StreamHandler::class); + $this->assertDICConstructorArguments($handler, ['/tmp/inferred.log', 'DEBUG', true, null, false]); + } + + #[DataProvider('provideConvertedHandlersWithSubNodeConfig')] + public function testConvertedHandlersWithSubNodeConfig(string $name, array $handlers, string $expectedClass, array $expectedArgs, array $expectedMethodCalls = []): void + { + $container = $this->getContainer([['handlers' => $handlers]], $this->handlerServiceDependencies()); + + $handler = $container->getDefinition('monolog.handler.'.$name); + $this->assertDICDefinitionClass($handler, $expectedClass); + $this->assertDICConstructorArguments($handler, $expectedArgs); + + $methodCalls = $handler->getMethodCalls(); + $this->assertCount(\count($expectedMethodCalls), $methodCalls); + foreach ($expectedMethodCalls as $pos => [$method, $args]) { + $this->assertDICDefinitionMethodCallAt($pos, $handler, $method, $args); + } + } + + public static function provideConvertedHandlersWithSubNodeConfig(): iterable + { + $nested = static fn (string $name) => ['type' => 'stream', 'path' => '/tmp/'.$name.'.log']; + $psr3 = [new Reference('monolog.processor.psr_log_message')]; + + yield 'rotating_file' => ['rotating', ['rotating' => [ + 'type' => 'rotating_file', + 'level' => 'WARNING', + 'rotating_file' => [ + 'path' => '/tmp/rot.log', + 'max_files' => 5, + 'file_permission' => '0600', + 'use_locking' => true, + 'filename_format' => '{filename}-{date}', + 'date_format' => 'Y-m-d', + ], + ]], \Monolog\Handler\RotatingFileHandler::class, ['/tmp/rot.log', 5, 'WARNING', true, 0600, true], [ + ['pushProcessor', $psr3], + ['setFilenameFormat', ['{filename}-{date}', 'Y-m-d']], + ]]; + + yield 'socket' => ['socket', ['socket' => [ + 'type' => 'socket', + 'socket' => [ + 'connection_string' => 'localhost:9000', + 'timeout' => 2, + 'connection_timeout' => 0.7, + 'persistent' => false, + ], + ]], \Monolog\Handler\SocketHandler::class, ['localhost:9000', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ['setTimeout', [2]], + ['setConnectionTimeout', [0.7]], + ['setPersistent', [false]], + ]]; + + yield 'syslog' => ['syslog', ['syslog' => [ + 'type' => 'syslog', + 'syslog' => ['ident' => 'myapp', 'facility' => 'local0', 'logopts' => 1], + ]], \Monolog\Handler\SyslogHandler::class, ['myapp', 'local0', 'DEBUG', true, 1], [ + ['pushProcessor', $psr3], + ]]; + + yield 'syslogudp' => ['syslogudp', ['syslogudp' => [ + 'type' => 'syslogudp', + 'syslogudp' => [ + 'host' => '127.0.0.2', + 'port' => 1514, + 'facility' => 'user', + 'ident' => 'php', + 'rfc' => SyslogUdpHandler::RFC3164, + ], + ]], \Monolog\Handler\SyslogUdpHandler::class, ['127.0.0.2', 1514, 'user', 'DEBUG', true, 'php', SyslogUdpHandler::RFC3164], [ + ['pushProcessor', $psr3], + ]]; + + yield 'cube' => ['cube', ['cube' => [ + 'type' => 'cube', + 'cube' => ['url' => 'udp://127.0.0.1:1180'], + ]], \Monolog\Handler\CubeHandler::class, ['udp://127.0.0.1:1180', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'error_log' => ['error_log', ['error_log' => [ + 'type' => 'error_log', + 'error_log' => ['message_type' => 4, 'expand_newlines' => true], + ]], \Monolog\Handler\ErrorLogHandler::class, [4, 'DEBUG', true, true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'server_log' => ['server_log', ['server_log' => [ + 'type' => 'server_log', + 'server_log' => ['host' => '0:9911'], + ]], \Symfony\Bridge\Monolog\Handler\ServerLogHandler::class, ['0:9911', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'amqp' => ['amqp', ['amqp' => [ + 'type' => 'amqp', + 'amqp' => ['exchange' => 'my.exchange', 'exchange_name' => 'logs'], + ]], \Monolog\Handler\AmqpHandler::class, [new Reference('my.exchange'), 'logs', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'logentries' => ['logentries', ['logentries' => [ + 'type' => 'logentries', + 'logentries' => ['token' => 'mytoken', 'use_ssl' => false], + ]], \Monolog\Handler\LogEntriesHandler::class, ['mytoken', false, 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'loggly' => ['loggly', ['loggly' => [ + 'type' => 'loggly', + 'loggly' => ['token' => 'mytoken', 'tags' => ['foo', 'bar']], + ]], \Monolog\Handler\LogglyHandler::class, ['mytoken', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ['setTag', ['foo,bar']], + ]]; + + yield 'insightops' => ['insightops', ['insightops' => [ + 'type' => 'insightops', + 'insightops' => ['token' => 'mytoken', 'region' => 'eu', 'use_ssl' => false], + ]], \Monolog\Handler\InsightOpsHandler::class, ['mytoken', 'eu', false, 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'flowdock' => ['flowdock', ['flowdock' => [ + 'type' => 'flowdock', + 'flowdock' => ['token' => 'mytoken', 'source' => 'src', 'from_email' => 'f@example.com'], + ]], \Monolog\Handler\FlowdockHandler::class, ['mytoken', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ['setFormatter', [new Reference('monolog.flowdock.formatter.'.sha1('src|f@example.com'))]], + ]]; + + yield 'pushover' => ['pushover', ['pushover' => [ + 'type' => 'pushover', + 'level' => 'ERROR', + 'pushover' => ['token' => 'token1', 'user' => 'user1', 'title' => 'My Title'], + ]], \Monolog\Handler\PushoverHandler::class, ['token1', 'user1', 'My Title', 'ERROR', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'telegram' => ['telegram', ['telegram' => [ + 'type' => 'telegram', + 'telegram' => ['token' => 'bot-token', 'channel' => '-100', 'parse_mode' => 'HTML'], + ]], \Monolog\Handler\TelegramBotHandler::class, ['bot-token', '-100', 'DEBUG', true, 'HTML', null, null, false, false, null], [ + ['pushProcessor', $psr3], + ]]; + + yield 'rollbar' => ['rollbar', ['rollbar' => [ + 'type' => 'rollbar', + 'rollbar' => ['token' => 'TOKEN'], + ]], RollbarHandler::class, [new Reference('monolog.rollbar.notifier.'.sha1(json_encode(['access_token' => 'TOKEN']))), 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'newrelic' => ['newrelic', ['newrelic' => [ + 'type' => 'newrelic', + 'newrelic' => ['app_name' => 'myapp'], + ]], \Monolog\Handler\NewRelicHandler::class, ['DEBUG', true, 'myapp'], [ + ['pushProcessor', $psr3], + ]]; + + yield 'console' => ['console', ['console' => [ + 'type' => 'console', + 'console' => [ + 'verbosity_levels' => [500, 400, 300, 250, 200], + 'console_formatter_options' => [], + 'interactive_only' => false, + ], + ]], \Symfony\Bridge\Monolog\Handler\ConsoleHandler::class, [ + null, + true, + [ + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_QUIET => 500, + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL => 400, + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_VERBOSE => 300, + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE => 250, + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_DEBUG => 200, + ], + [], + false, + ], [ + ['pushProcessor', $psr3], + ]]; + + yield 'firephp' => ['firephp', ['firephp' => [ + 'type' => 'firephp', + 'level' => 'WARNING', + ]], \Symfony\Bridge\Monolog\Handler\FirePHPHandler::class, ['WARNING', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'chromephp' => ['chromephp', ['chromephp' => [ + 'type' => 'chromephp', + 'level' => 'WARNING', + ]], \Symfony\Bridge\Monolog\Handler\ChromePhpHandler::class, ['WARNING', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'slack' => ['slack', ['slack' => [ + 'type' => 'slack', + 'slack' => [ + 'token' => 'slack-token', + 'channel' => '#logs', + 'exclude_fields' => ['field1'], + ], + ]], \Monolog\Handler\SlackHandler::class, ['slack-token', '#logs', 'Monolog', true, null, 'DEBUG', true, false, false, ['field1']], [ + ['pushProcessor', $psr3], + ]]; + + yield 'slackwebhook' => ['slackwebhook', ['slackwebhook' => [ + 'type' => 'slackwebhook', + 'slackwebhook' => [ + 'webhook_url' => 'https://hooks.slack.com/services/...', + 'channel' => '#logs', + 'exclude_fields' => ['field1'], + ], + ]], \Monolog\Handler\SlackWebhookHandler::class, ['https://hooks.slack.com/services/...', '#logs', 'Monolog', true, null, false, false, 'DEBUG', true, ['field1']], [ + ['pushProcessor', $psr3], + ]]; + + yield 'gelf' => ['gelf', ['gelf' => [ + 'type' => 'gelf', + 'gelf' => ['publisher' => ['id' => 'gelf.publisher']], + ]], \Monolog\Handler\GelfHandler::class, [new Reference('gelf.publisher'), 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'fingers_crossed' => ['fingers_crossed', [ + 'fingers_crossed' => [ + 'type' => 'fingers_crossed', + 'fingers_crossed' => ['handler' => 'nested', 'buffer_size' => 30], + ], + 'nested' => $nested('nested'), + ], \Monolog\Handler\FingersCrossedHandler::class, [ + new Reference('monolog.handler.nested'), + new Definition(\Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy::class, ['WARNING']), + 30, + true, + true, + null, + ]]; + + yield 'filter' => ['filter', [ + 'filter' => [ + 'type' => 'filter', + 'filter' => ['handler' => 'nested', 'accepted_levels' => ['WARNING', 'ERROR']], + ], + 'nested' => $nested('nested'), + ], \Monolog\Handler\FilterHandler::class, [ + new Reference('monolog.handler.nested'), + ['WARNING', 'ERROR'], + 'EMERGENCY', + true, + ]]; + + yield 'buffer' => ['buffer', [ + 'buffer' => [ + 'type' => 'buffer', + 'buffer' => ['handler' => 'nested', 'buffer_size' => 5], + ], + 'nested' => $nested('nested'), + ], \Monolog\Handler\BufferHandler::class, [ + new Reference('monolog.handler.nested'), + 5, + 'DEBUG', + true, + false, + ]]; + + yield 'deduplication' => ['deduplication', [ + 'deduplication' => [ + 'type' => 'deduplication', + 'deduplication' => ['handler' => 'nested', 'time' => 60], + ], + 'nested' => $nested('nested'), + ], \Monolog\Handler\DeduplicationHandler::class, [ + new Reference('monolog.handler.nested'), + '%kernel.cache_dir%/monolog_dedup_'.sha1('monolog.handler.deduplication'), + \Monolog\Level::Error->value, + 60, + true, + ]]; + + yield 'sampling' => ['sampling', [ + 'sampling' => [ + 'type' => 'sampling', + 'sampling' => ['handler' => 'nested', 'factor' => 10], + ], + 'nested' => $nested('nested'), + ], \Monolog\Handler\SamplingHandler::class, [ + new Reference('monolog.handler.nested'), + 10, + ]]; + + yield 'group' => ['group', [ + 'group' => ['type' => 'group', 'group' => ['members' => ['a', 'b']]], + 'a' => $nested('a'), + 'b' => $nested('b'), + ], \Monolog\Handler\GroupHandler::class, [ + [new Reference('monolog.handler.a'), new Reference('monolog.handler.b')], + true, + ]]; + + yield 'whatfailuregroup' => ['whatfailuregroup', [ + 'whatfailuregroup' => ['type' => 'whatfailuregroup', 'whatfailuregroup' => ['members' => ['a', 'b']]], + 'a' => $nested('a'), + 'b' => $nested('b'), + ], \Monolog\Handler\WhatFailureGroupHandler::class, [ + [new Reference('monolog.handler.a'), new Reference('monolog.handler.b')], + true, + ]]; + + yield 'fallbackgroup' => ['fallbackgroup', [ + 'fallbackgroup' => ['type' => 'fallbackgroup', 'fallbackgroup' => ['members' => ['a', 'b']]], + 'a' => $nested('a'), + 'b' => $nested('b'), + ], \Monolog\Handler\FallbackGroupHandler::class, [ + [new Reference('monolog.handler.a'), new Reference('monolog.handler.b')], + true, + ]]; + + yield 'native_mailer' => ['native_mailer', ['native_mailer' => [ + 'type' => 'native_mailer', + 'native_mailer' => [ + 'from_email' => 'f@example.com', + 'to_email' => ['t@example.com'], + 'subject' => 'Subject', + 'headers' => ['Foo: bar'], + 'parameters' => ['--foo'], + ], + ]], \Monolog\Handler\NativeMailerHandler::class, [['t@example.com'], 'Subject', 'f@example.com', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ['addHeader', [['Foo: bar']]], + ['addParameter', [['--foo']]], + ]]; + + $symfonyMailerPrototype = (new Definition(\Symfony\Component\Mime\Email::class)) + ->setPublic(false) + ->addMethodCall('from', ['f@example.com']) + ->addMethodCall('to', ['t@example.com']) + ->addMethodCall('subject', ['Subject']); + + yield 'symfony_mailer' => ['symfony_mailer', ['symfony_mailer' => [ + 'type' => 'symfony_mailer', + 'symfony_mailer' => [ + 'from_email' => 'f@example.com', + 'to_email' => ['t@example.com'], + 'subject' => 'Subject', + ], + ]], \Symfony\Bridge\Monolog\Handler\MailerHandler::class, [ + new Reference('mailer.mailer'), + $symfonyMailerPrototype, + 'DEBUG', + true, + ], [ + ['pushProcessor', $psr3], + ]]; + } + + #[DataProvider('provideConvertedHandlersWithLegacyFlatConfig')] + public function testConvertedHandlersWithLegacyFlatConfig(string $name, array $handlers, string $expectedClass, array $expectedArgs, array $expectedMethodCalls = []): void + { + $container = $this->getContainer([['handlers' => $handlers]], $this->handlerServiceDependencies()); + + $handler = $container->getDefinition('monolog.handler.'.$name); + $this->assertDICDefinitionClass($handler, $expectedClass); + $this->assertDICConstructorArguments($handler, $expectedArgs); + + $methodCalls = $handler->getMethodCalls(); + $this->assertCount(\count($expectedMethodCalls), $methodCalls); + foreach ($expectedMethodCalls as $pos => [$method, $args]) { + $this->assertDICDefinitionMethodCallAt($pos, $handler, $method, $args); + } + } + + public static function provideConvertedHandlersWithLegacyFlatConfig(): iterable + { + $nested = static fn (string $name) => ['type' => 'stream', 'path' => '/tmp/'.$name.'.log']; + $psr3 = [new Reference('monolog.processor.psr_log_message')]; + + yield 'rotating_file' => ['rotating', ['rotating' => [ + 'type' => 'rotating_file', + 'level' => 'WARNING', + 'path' => '/tmp/rot.log', + 'max_files' => 5, + 'file_permission' => '0600', + 'use_locking' => true, + 'filename_format' => '{filename}-{date}', + 'date_format' => 'Y-m-d', + ]], \Monolog\Handler\RotatingFileHandler::class, ['/tmp/rot.log', 5, 'WARNING', true, 0600, true], [ + ['pushProcessor', $psr3], + ['setFilenameFormat', ['{filename}-{date}', 'Y-m-d']], + ]]; + + yield 'socket' => ['socket', ['socket' => [ + 'type' => 'socket', + 'connection_string' => 'localhost:9000', + 'timeout' => 2, + 'connection_timeout' => 0.7, + 'persistent' => false, + ]], \Monolog\Handler\SocketHandler::class, ['localhost:9000', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ['setTimeout', [2]], + ['setConnectionTimeout', [0.7]], + ['setPersistent', [false]], + ]]; + + yield 'syslog' => ['syslog', ['syslog' => [ + 'type' => 'syslog', + 'ident' => 'myapp', + 'facility' => 'local0', + 'logopts' => 1, + ]], \Monolog\Handler\SyslogHandler::class, ['myapp', 'local0', 'DEBUG', true, 1], [ + ['pushProcessor', $psr3], + ]]; + + yield 'syslogudp' => ['syslogudp', ['syslogudp' => [ + 'type' => 'syslogudp', + 'host' => '127.0.0.2', + 'port' => 1514, + 'facility' => 'user', + 'ident' => 'php', + 'rfc' => SyslogUdpHandler::RFC3164, + ]], \Monolog\Handler\SyslogUdpHandler::class, ['127.0.0.2', 1514, 'user', 'DEBUG', true, 'php', SyslogUdpHandler::RFC3164], [ + ['pushProcessor', $psr3], + ]]; + + yield 'cube' => ['cube', ['cube' => [ + 'type' => 'cube', + 'url' => 'udp://127.0.0.1:1180', + ]], \Monolog\Handler\CubeHandler::class, ['udp://127.0.0.1:1180', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'error_log' => ['error_log', ['error_log' => [ + 'type' => 'error_log', + 'message_type' => 4, + 'expand_newlines' => true, + ]], \Monolog\Handler\ErrorLogHandler::class, [4, 'DEBUG', true, true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'server_log' => ['server_log', ['server_log' => [ + 'type' => 'server_log', + 'host' => '0:9911', + ]], \Symfony\Bridge\Monolog\Handler\ServerLogHandler::class, ['0:9911', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'amqp' => ['amqp', ['amqp' => [ + 'type' => 'amqp', + 'exchange' => 'my.exchange', + 'exchange_name' => 'logs', + ]], \Monolog\Handler\AmqpHandler::class, [new Reference('my.exchange'), 'logs', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'logentries' => ['logentries', ['logentries' => [ + 'type' => 'logentries', + 'token' => 'mytoken', + 'use_ssl' => false, + ]], \Monolog\Handler\LogEntriesHandler::class, ['mytoken', false, 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'loggly' => ['loggly', ['loggly' => [ + 'type' => 'loggly', + 'token' => 'mytoken', + 'tags' => ['foo', 'bar'], + ]], \Monolog\Handler\LogglyHandler::class, ['mytoken', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ['setTag', ['foo,bar']], + ]]; + + yield 'insightops' => ['insightops', ['insightops' => [ + 'type' => 'insightops', + 'token' => 'mytoken', + 'region' => 'eu', + 'use_ssl' => false, + ]], \Monolog\Handler\InsightOpsHandler::class, ['mytoken', 'eu', false, 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'flowdock' => ['flowdock', ['flowdock' => [ + 'type' => 'flowdock', + 'token' => 'mytoken', + 'source' => 'src', + 'from_email' => 'f@example.com', + ]], \Monolog\Handler\FlowdockHandler::class, ['mytoken', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ['setFormatter', [new Reference('monolog.flowdock.formatter.'.sha1('src|f@example.com'))]], + ]]; + + yield 'pushover' => ['pushover', ['pushover' => [ + 'type' => 'pushover', + 'level' => 'ERROR', + 'token' => 'token1', + 'user' => 'user1', + 'title' => 'My Title', + ]], \Monolog\Handler\PushoverHandler::class, ['token1', 'user1', 'My Title', 'ERROR', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'telegram' => ['telegram', ['telegram' => [ + 'type' => 'telegram', + 'token' => 'bot-token', + 'channel' => '-100', + 'parse_mode' => 'HTML', + ]], \Monolog\Handler\TelegramBotHandler::class, ['bot-token', '-100', 'DEBUG', true, 'HTML', null, null, false, false, null], [ + ['pushProcessor', $psr3], + ]]; + + yield 'rollbar' => ['rollbar', ['rollbar' => [ + 'type' => 'rollbar', + 'token' => 'TOKEN', + ]], RollbarHandler::class, [new Reference('monolog.rollbar.notifier.'.sha1(json_encode(['access_token' => 'TOKEN']))), 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'newrelic' => ['newrelic', ['newrelic' => [ + 'type' => 'newrelic', + 'app_name' => 'myapp', + ]], \Monolog\Handler\NewRelicHandler::class, ['DEBUG', true, 'myapp'], [ + ['pushProcessor', $psr3], + ]]; + + yield 'console' => ['console', ['console' => [ + 'type' => 'console', + 'verbosity_levels' => [500, 400, 300, 250, 200], + 'console_formatter_options' => [], + 'interactive_only' => false, + ]], \Symfony\Bridge\Monolog\Handler\ConsoleHandler::class, [ + null, + true, + [ + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_QUIET => 500, + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL => 400, + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_VERBOSE => 300, + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE => 250, + \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_DEBUG => 200, + ], + [], + false, + ], [ + ['pushProcessor', $psr3], + ]]; + + yield 'firephp' => ['firephp', ['firephp' => [ + 'type' => 'firephp', + 'level' => 'WARNING', + ]], \Symfony\Bridge\Monolog\Handler\FirePHPHandler::class, ['WARNING', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'chromephp' => ['chromephp', ['chromephp' => [ + 'type' => 'chromephp', + 'level' => 'WARNING', + ]], \Symfony\Bridge\Monolog\Handler\ChromePhpHandler::class, ['WARNING', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'slack' => ['slack', ['slack' => [ + 'type' => 'slack', + 'token' => 'slack-token', + 'channel' => '#logs', + 'exclude_fields' => ['field1'], + ]], \Monolog\Handler\SlackHandler::class, ['slack-token', '#logs', 'Monolog', true, null, 'DEBUG', true, false, false, ['field1']], [ + ['pushProcessor', $psr3], + ]]; + + yield 'slackwebhook' => ['slackwebhook', ['slackwebhook' => [ + 'type' => 'slackwebhook', + 'webhook_url' => 'https://hooks.slack.com/services/...', + 'channel' => '#logs', + 'exclude_fields' => ['field1'], + ]], \Monolog\Handler\SlackWebhookHandler::class, ['https://hooks.slack.com/services/...', '#logs', 'Monolog', true, null, false, false, 'DEBUG', true, ['field1']], [ + ['pushProcessor', $psr3], + ]]; + + yield 'gelf' => ['gelf', ['gelf' => [ + 'type' => 'gelf', + 'publisher' => ['id' => 'gelf.publisher'], + ]], \Monolog\Handler\GelfHandler::class, [new Reference('gelf.publisher'), 'DEBUG', true], [ + ['pushProcessor', $psr3], + ]]; + + yield 'fingers_crossed' => ['fingers_crossed', [ + 'fingers_crossed' => [ + 'type' => 'fingers_crossed', + 'action_level' => 'WARNING', + 'handler' => 'nested', + 'buffer_size' => 30, + 'stop_buffering' => true, + 'passthru_level' => null, + ], + 'nested' => $nested('nested'), + ], \Monolog\Handler\FingersCrossedHandler::class, [ + new Reference('monolog.handler.nested'), + new Definition(\Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy::class, ['WARNING']), + 30, + true, + true, + null, + ]]; + + yield 'filter' => ['filter', [ + 'filter' => [ + 'type' => 'filter', + 'handler' => 'nested', + 'accepted_levels' => ['WARNING', 'ERROR'], + ], + 'nested' => $nested('nested'), + ], \Monolog\Handler\FilterHandler::class, [ + new Reference('monolog.handler.nested'), + ['WARNING', 'ERROR'], + 'EMERGENCY', + true, + ]]; + + yield 'buffer' => ['buffer', [ + 'buffer' => [ + 'type' => 'buffer', + 'handler' => 'nested', + 'buffer_size' => 5, + 'flush_on_overflow' => false, + ], + 'nested' => $nested('nested'), + ], \Monolog\Handler\BufferHandler::class, [ + new Reference('monolog.handler.nested'), + 5, + 'DEBUG', + true, + false, + ]]; + + yield 'deduplication' => ['deduplication', [ + 'deduplication' => [ + 'type' => 'deduplication', + 'handler' => 'nested', + 'time' => 60, + ], + 'nested' => $nested('nested'), + ], \Monolog\Handler\DeduplicationHandler::class, [ + new Reference('monolog.handler.nested'), + '%kernel.cache_dir%/monolog_dedup_'.sha1('monolog.handler.deduplication'), + \Monolog\Level::Error->value, + 60, + true, + ]]; + + yield 'sampling' => ['sampling', [ + 'sampling' => [ + 'type' => 'sampling', + 'handler' => 'nested', + 'factor' => 10, + ], + 'nested' => $nested('nested'), + ], \Monolog\Handler\SamplingHandler::class, [ + new Reference('monolog.handler.nested'), + 10, + ]]; + + yield 'group' => ['group', [ + 'group' => ['type' => 'group', 'members' => ['a', 'b']], + 'a' => $nested('a'), + 'b' => $nested('b'), + ], \Monolog\Handler\GroupHandler::class, [ + [new Reference('monolog.handler.a'), new Reference('monolog.handler.b')], + true, + ]]; + + yield 'whatfailuregroup' => ['whatfailuregroup', [ + 'whatfailuregroup' => ['type' => 'whatfailuregroup', 'members' => ['a', 'b']], + 'a' => $nested('a'), + 'b' => $nested('b'), + ], \Monolog\Handler\WhatFailureGroupHandler::class, [ + [new Reference('monolog.handler.a'), new Reference('monolog.handler.b')], + true, + ]]; + + yield 'fallbackgroup' => ['fallbackgroup', [ + 'fallbackgroup' => ['type' => 'fallbackgroup', 'members' => ['a', 'b']], + 'a' => $nested('a'), + 'b' => $nested('b'), + ], \Monolog\Handler\FallbackGroupHandler::class, [ + [new Reference('monolog.handler.a'), new Reference('monolog.handler.b')], + true, + ]]; + + yield 'native_mailer' => ['native_mailer', ['native_mailer' => [ + 'type' => 'native_mailer', + 'from_email' => 'f@example.com', + 'to_email' => ['t@example.com'], + 'subject' => 'Subject', + 'headers' => ['Foo: bar'], + 'parameters' => ['--foo'], + ]], \Monolog\Handler\NativeMailerHandler::class, [['t@example.com'], 'Subject', 'f@example.com', 'DEBUG', true], [ + ['pushProcessor', $psr3], + ['addHeader', [['Foo: bar']]], + ['addParameter', [['--foo']]], + ]]; + + $symfonyMailerPrototype = (new Definition(\Symfony\Component\Mime\Email::class)) + ->setPublic(false) + ->addMethodCall('from', ['f@example.com']) + ->addMethodCall('to', ['t@example.com']) + ->addMethodCall('subject', ['Subject']); + + yield 'symfony_mailer' => ['symfony_mailer', ['symfony_mailer' => [ + 'type' => 'symfony_mailer', + 'from_email' => 'f@example.com', + 'to_email' => ['t@example.com'], + 'subject' => 'Subject', + ]], \Symfony\Bridge\Monolog\Handler\MailerHandler::class, [ + new Reference('mailer.mailer'), + $symfonyMailerPrototype, + 'DEBUG', + true, + ], [ + ['pushProcessor', $psr3], + ]]; + } + + private function handlerServiceDependencies(): array + { + return [ + 'gelf.publisher' => new Definition(\stdClass::class), + 'my.exchange' => new Definition(\stdClass::class), + 'mailer.mailer' => new Definition(\stdClass::class), + ]; + } + public function testLoadWithTimezone() { $container = $this->getContainer([['timezone' => 'Europe/Paris', 'handlers' => ['main' => ['type' => 'stream']]]]); @@ -716,6 +1468,11 @@ public function testMongoDB() 'uri' => 'mongodb://localhost:27018', ], ], + 'mongodb_without_explicit_type' => [ + 'mongodb' => [ + 'uri' => 'mongodb://localhost:27018', + ], + ], ], ]]; @@ -759,6 +1516,13 @@ public function testMongoDB() $this->assertDICConstructorArguments($client, ['mongodb://localhost:27018', ['appname' => 'monolog-bundle']]); $this->assertDICConstructorArguments($handler, [$client, 'monolog', 'logs', 'DEBUG', true]); $this->assertDICDefinitionMethodCallAt(1, $handler, 'setFormatter', [$formatter]); + + // MongoDB handler without an explicit type + $handler = $container->getDefinition('monolog.handler.mongodb_without_explicit_type'); + $this->assertDICDefinitionClass($handler, MongoDBHandler::class); + $client = $handler->getArgument(0); + $this->assertDICDefinitionClass($client, 'MongoDB\Client'); + $this->assertDICConstructorArguments($client, ['mongodb://localhost:27018', ['appname' => 'monolog-bundle']]); } public function testBasePathOption()