├── README.md ├── LICENSE ├── composer.json ├── src ├── MonologBundle.php └── DependencyInjection │ ├── Compiler │ ├── AddProcessorsPass.php │ └── LoggerChannelPass.php │ ├── MonologExtension.php │ └── Configuration.php ├── config ├── monolog.php └── schema │ └── monolog-1.0.xsd └── CHANGELOG.md /README.md: -------------------------------------------------------------------------------- 1 | MonologBundle 2 | ============= 3 | 4 | The `MonologBundle` provides integration of the [Monolog](https://github.com/Seldaek/monolog) 5 | library into the Symfony framework. 6 | 7 | More information in the official [documentation](https://symfony.com/doc/current/logging.html). 8 | 9 | License 10 | ======= 11 | 12 | This bundle is released under the [MIT license](LICENSE) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-2019 Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/monolog-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Symfony MonologBundle", 5 | "keywords": ["log", "logging"], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabien@symfony.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2", 20 | "composer-runtime-api": "^2.0", 21 | "monolog/monolog": "^3.5", 22 | "symfony/config": "^7.3 || ^8.0", 23 | "symfony/dependency-injection": "^7.3 || ^8.0", 24 | "symfony/http-kernel": "^7.3 || ^8.0", 25 | "symfony/monolog-bridge": "^7.3 || ^8.0", 26 | "symfony/polyfill-php84": "^1.30" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^11.5.41 || ^12.3", 30 | "symfony/console": "^7.3 || ^8.0", 31 | "symfony/yaml": "^7.3 || ^8.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { "Symfony\\Bundle\\MonologBundle\\": "src" } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { "Symfony\\Bundle\\MonologBundle\\Tests\\": "tests" } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/MonologBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\MonologBundle; 13 | 14 | use Monolog\Formatter\JsonFormatter; 15 | use Monolog\Formatter\LineFormatter; 16 | use Monolog\Handler\HandlerInterface; 17 | use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\AddProcessorsPass; 18 | use Symfony\Bundle\MonologBundle\DependencyInjection\Compiler\LoggerChannelPass; 19 | use Symfony\Component\DependencyInjection\ContainerBuilder; 20 | use Symfony\Component\HttpKernel\Bundle\Bundle; 21 | 22 | /** 23 | * @author Jordi Boggiano 24 | */ 25 | final class MonologBundle extends Bundle 26 | { 27 | public function build(ContainerBuilder $container): void 28 | { 29 | parent::build($container); 30 | 31 | $container->addCompilerPass(new LoggerChannelPass()); 32 | $container->addCompilerPass(new AddProcessorsPass()); 33 | } 34 | 35 | /** 36 | * @internal 37 | */ 38 | public static function includeStacktraces(HandlerInterface $handler): void 39 | { 40 | $formatter = $handler->getFormatter(); 41 | if ($formatter instanceof LineFormatter || $formatter instanceof JsonFormatter) { 42 | $formatter->includeStacktraces(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /config/monolog.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\DependencyInjection\Loader\Configurator; 13 | 14 | use Monolog\Formatter\ChromePHPFormatter; 15 | use Monolog\Formatter\GelfMessageFormatter; 16 | use Monolog\Formatter\HtmlFormatter; 17 | use Monolog\Formatter\JsonFormatter; 18 | use Monolog\Formatter\LineFormatter; 19 | use Monolog\Formatter\LogglyFormatter; 20 | use Monolog\Formatter\LogstashFormatter; 21 | use Monolog\Formatter\NormalizerFormatter; 22 | use Monolog\Formatter\ScalarFormatter; 23 | use Monolog\Formatter\SyslogFormatter; 24 | use Monolog\Formatter\WildfireFormatter; 25 | use Monolog\Logger; 26 | use Psr\Log\LoggerInterface; 27 | use Symfony\Component\HttpClient\HttpClient; 28 | use Symfony\Contracts\HttpClient\HttpClientInterface; 29 | 30 | return static function (ContainerConfigurator $container) { 31 | $container->services() 32 | 33 | ->alias('logger', 'monolog.logger') 34 | ->alias(LoggerInterface::class, 'logger') 35 | 36 | ->set('monolog.logger') 37 | ->parent('monolog.logger_prototype') 38 | ->args(['index_0' => 'app']) 39 | ->call('useMicrosecondTimestamps', [param('monolog.use_microseconds')]) 40 | ->tag('monolog.channel_logger') 41 | 42 | ->set('monolog.logger_prototype', Logger::class) 43 | ->args([abstract_arg('channel')]) 44 | ->abstract() 45 | 46 | // Formatters 47 | ->set('monolog.formatter.chrome_php', ChromePHPFormatter::class) 48 | ->set('monolog.formatter.gelf_message', GelfMessageFormatter::class) 49 | ->set('monolog.formatter.html', HtmlFormatter::class) 50 | ->set('monolog.formatter.json', JsonFormatter::class) 51 | ->set('monolog.formatter.line', LineFormatter::class) 52 | ->set('monolog.formatter.syslog', SyslogFormatter::class) 53 | ->set('monolog.formatter.loggly', LogglyFormatter::class) 54 | ->set('monolog.formatter.normalizer', NormalizerFormatter::class) 55 | ->set('monolog.formatter.scalar', ScalarFormatter::class) 56 | ->set('monolog.formatter.wildfire', WildfireFormatter::class) 57 | 58 | ->set('monolog.formatter.logstash', LogstashFormatter::class) 59 | ->args(['app']) 60 | 61 | ->set('monolog.http_client', HttpClientInterface::class) 62 | ->factory([HttpClient::class, 'create']) 63 | ; 64 | }; 65 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/AddProcessorsPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\MonologBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; 15 | use Symfony\Component\DependencyInjection\ChildDefinition; 16 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 17 | use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | 20 | /** 21 | * Registers processors in Monolog loggers or handlers. 22 | * 23 | * @author Christophe Coevoet 24 | * 25 | * @internal 26 | */ 27 | class AddProcessorsPass implements CompilerPassInterface 28 | { 29 | use PriorityTaggedServiceTrait; 30 | 31 | public function process(ContainerBuilder $container): void 32 | { 33 | if (!$container->hasDefinition('monolog.logger')) { 34 | return; 35 | } 36 | 37 | $indexedTags = []; 38 | $i = 1; 39 | 40 | foreach ($container->findTaggedServiceIds('monolog.processor') as $id => $tags) { 41 | if (array_any($tags, $closure = function (array $tag) { return (bool) $tag; })) { 42 | $tags = array_values(array_filter($tags, $closure)); 43 | } 44 | 45 | foreach ($tags as &$tag) { 46 | $indexedTags[$tag['index'] = $i++] = $tag; 47 | } 48 | unset($tag); 49 | $definition = $container->getDefinition($id); 50 | $definition->setTags(array_merge($definition->getTags(), ['monolog.processor' => $tags])); 51 | } 52 | 53 | $taggedIteratorArgument = new TaggedIteratorArgument('monolog.processor', 'index', null, true); 54 | // array_reverse is used because ProcessableHandlerTrait::pushProcessor prepends processors to the beginning of the stack 55 | foreach (array_reverse($this->findAndSortTaggedServices($taggedIteratorArgument, $container), true) as $index => $reference) { 56 | $tag = $indexedTags[$index]; 57 | 58 | if (!empty($tag['channel']) && !empty($tag['handler'])) { 59 | throw new \InvalidArgumentException(\sprintf('You cannot specify both the "handler" and "channel" attributes for the "monolog.processor" tag on service "%s".', $reference)); 60 | } 61 | 62 | if (!empty($tag['handler'])) { 63 | $parentDef = $container->findDefinition(\sprintf('monolog.handler.%s', $tag['handler'])); 64 | $definitions = [$parentDef]; 65 | while (!$parentDef->getClass() && $parentDef instanceof ChildDefinition) { 66 | $parentDef = $container->findDefinition($parentDef->getParent()); 67 | } 68 | $class = $container->getParameterBag()->resolveValue($parentDef->getClass()); 69 | if (!method_exists($class, 'pushProcessor')) { 70 | throw new \InvalidArgumentException(\sprintf('The "%s" handler does not accept processors.', $tag['handler'])); 71 | } 72 | } elseif (!empty($tag['channel'])) { 73 | $loggerId = 'app' === $tag['channel'] ? 'monolog.logger' : \sprintf('monolog.logger.%s', $tag['channel']); 74 | $definitions = [$container->getDefinition($loggerId)]; 75 | } elseif ($loggerIds = $container->findTaggedServiceIds('monolog.channel_logger')) { 76 | $definitions = []; 77 | foreach ($loggerIds as $loggerId => $tags) { 78 | $definitions[] = $container->getDefinition($loggerId); 79 | } 80 | } else { 81 | $definitions = [$container->getDefinition('monolog.logger_prototype')]; 82 | } 83 | 84 | if (!empty($tag['method'])) { 85 | $processor = [$reference, $tag['method']]; 86 | } else { 87 | // If no method is defined, fallback to use __invoke 88 | $processor = $reference; 89 | } 90 | foreach ($definitions as $definition) { 91 | $definition->addMethodCall('pushProcessor', [$processor]); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/LoggerChannelPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\MonologBundle\DependencyInjection\Compiler; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Symfony\Component\DependencyInjection\Argument\BoundArgument; 16 | use Symfony\Component\DependencyInjection\ChildDefinition; 17 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; 20 | use Symfony\Component\DependencyInjection\Reference; 21 | 22 | /** 23 | * Replaces the default logger by another one with its own channel for tagged services. 24 | * 25 | * @author Christophe Coevoet 26 | * 27 | * @internal 28 | */ 29 | class LoggerChannelPass implements CompilerPassInterface 30 | { 31 | public function process(ContainerBuilder $container): void 32 | { 33 | if (!$container->hasDefinition('monolog.logger')) { 34 | return; 35 | } 36 | 37 | /** @var list $createdLoggers */ 38 | $createdLoggers = ['app']; 39 | 40 | // create channels necessary for the handlers 41 | foreach ($container->findTaggedServiceIds('monolog.logger') as $id => $tags) { 42 | foreach ($tags as $tag) { 43 | if (empty($tag['channel']) || 'app' === $tag['channel']) { 44 | continue; 45 | } 46 | 47 | $resolvedChannel = $container->getParameterBag()->resolveValue($tag['channel']); 48 | 49 | $definition = $container->getDefinition($id); 50 | $loggerId = \sprintf('monolog.logger.%s', $resolvedChannel); 51 | $this->createLogger($resolvedChannel, $loggerId, $container, $createdLoggers); 52 | 53 | foreach ($definition->getArguments() as $index => $argument) { 54 | if ($argument instanceof Reference && 'logger' === (string) $argument) { 55 | $definition->replaceArgument($index, $this->changeReference($argument, $loggerId)); 56 | } 57 | } 58 | 59 | $calls = $definition->getMethodCalls(); 60 | foreach ($calls as $i => $call) { 61 | foreach ($call[1] as $index => $argument) { 62 | if ($argument instanceof Reference && 'logger' === (string) $argument) { 63 | $calls[$i][1][$index] = $this->changeReference($argument, $loggerId); 64 | } 65 | } 66 | } 67 | $definition->setMethodCalls($calls); 68 | 69 | $binding = new BoundArgument(new Reference($loggerId)); 70 | 71 | // Mark the binding as used already, to avoid reporting it as unused if the service does not use a 72 | // logger injected through the LoggerInterface alias. 73 | $values = $binding->getValues(); 74 | $values[2] = true; 75 | $binding->setValues($values); 76 | 77 | $bindings = $definition->getBindings(); 78 | $bindings[LoggerInterface::class] = $binding; 79 | $definition->setBindings($bindings); 80 | } 81 | } 82 | 83 | // create additional channels 84 | foreach ($container->getParameter('monolog.additional_channels') as $chan) { 85 | if ('app' === $chan) { 86 | continue; 87 | } 88 | $loggerId = \sprintf('monolog.logger.%s', $chan); 89 | $this->createLogger($chan, $loggerId, $container, $createdLoggers); 90 | $container->getDefinition($loggerId)->setPublic(true); 91 | } 92 | $container->getParameterBag()->remove('monolog.additional_channels'); 93 | 94 | // wire handlers to channels 95 | $handlersToChannels = $container->getParameter('monolog.handlers_to_channels'); 96 | foreach ($handlersToChannels as $handler => $channels) { 97 | foreach ($this->processChannels($channels, $createdLoggers) as $channel) { 98 | try { 99 | $logger = $container->getDefinition('app' === $channel ? 'monolog.logger' : 'monolog.logger.'.$channel); 100 | } catch (InvalidArgumentException $e) { 101 | throw new \InvalidArgumentException(\sprintf('Monolog configuration error: The logging channel "%s" assigned to the "%s" handler does not exist.', $channel, substr($handler, 16)), 0, $e); 102 | } 103 | $logger->addMethodCall('pushHandler', [new Reference($handler)]); 104 | } 105 | } 106 | } 107 | 108 | protected function processChannels(?array $configuration, array $createdLoggers): array 109 | { 110 | if (null === $configuration) { 111 | return $createdLoggers; 112 | } 113 | 114 | if ('inclusive' === $configuration['type']) { 115 | return $configuration['elements'] ?: $createdLoggers; 116 | } 117 | 118 | return array_diff($createdLoggers, $configuration['elements']); 119 | } 120 | 121 | /** 122 | * Create new logger from the monolog.logger_prototype. 123 | */ 124 | protected function createLogger(string $channel, string $loggerId, ContainerBuilder $container, array &$createdLoggers): void 125 | { 126 | if (!\in_array($channel, $createdLoggers, true)) { 127 | $logger = new ChildDefinition('monolog.logger_prototype'); 128 | $logger->replaceArgument(0, $channel); 129 | $logger->addTag('monolog.channel_logger'); 130 | $container->setDefinition($loggerId, $logger); 131 | $createdLoggers[] = $channel; 132 | } 133 | 134 | $container->registerAliasForArgument($loggerId, LoggerInterface::class, $channel.'.logger'); 135 | } 136 | 137 | /** 138 | * Creates a copy of a reference and alters the service ID. 139 | */ 140 | private function changeReference(Reference $reference, string $serviceId): Reference 141 | { 142 | return new Reference($serviceId, $reference->getInvalidBehavior()); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | # 4.0.1 (2025-12-09) 4 | 5 | * Fix `rollbar` handler to use `RollbarLogger` 6 | * Fix `monolog.processor` attributes to use consecutive keys 7 | 8 | ## 4.0.0 (2025-11-27) 9 | 10 | * Add support for Symfony 8.0 11 | * Drop support for PHP < 8.2 12 | * Drop support for Symfony < 7.3 13 | * Drop support for Monolog < 3.5 14 | * Remove abstract `monolog.activation_strategy.not_found` and `monolog.handler.fingers_crossed.error_level_activation_strategy` service definitions 15 | * Remove `excluded_404s` option, use `excluded_http_codes` instead 16 | * Remove `console_formater_options` option, use `console_formatter_options` instead 17 | * Remove `elasticsearch` type, use `elastica` or `elastic_search` instead 18 | * Remove `mongo` type, use `mongodb` instead 19 | * Remove `sentry` and `raven` types, use a `service` type with [`sentry/sentry-symfony`](https://docs.sentry.io/platforms/php/guides/symfony/logs/) instead 20 | * Remove `DebugHandlerPass` 21 | * Remove support for the `DebugHandler` 22 | 23 | ## 3.11.1 (2025-12-09) 24 | 25 | * Fix `rollbar` handler to use `RollbarLogger` with Monolog 2+ 26 | * Fix `monolog.processor` attributes to use consecutive keys 27 | 28 | ## 3.11.0 (2025-11-27) 29 | 30 | * Reorganize files to match the "Reusable Bundles" structure 31 | * Migrate services configuration to PHP 32 | * Add `console.interactive_only` flag 33 | * Add `slack.exclude_fields` and `slackwebhook.exclude_fields` configuration 34 | * Add a processor to all loggers only when tags do not specify a channel or handler 35 | * Deprecate abstract `monolog.activation_strategy.not_found` and `monolog.handler.fingers_crossed.error_level_activation_strategy` service definitions 36 | * Drop support for PHP < 8.1 37 | * Drop support for Symfony < 6.4 38 | * Add TelegramBotHandler `topic` support 39 | * Deprecate `sentry` and `raven` handler, use a `service` handler with [`sentry/sentry-symfony`](https://docs.sentry.io/platforms/php/guides/symfony/logs/) instead 40 | * Add configuration for Gelf encoders 41 | * Fix `host` configuration for `elastic_search` handler 42 | * Add `hosts` configuration for `elastica` handler 43 | * Add `enabled` option to `handlers` configuration 44 | * Add `priority` field to `processor` tag 45 | * Add `mongodb` handler and deprecate `mongo` 46 | * Add `monolog.formatter.syslog` service definition to format RFC5424-compliant messages 47 | 48 | ## 3.10.0 (2023-11-06) 49 | 50 | * Add configuration support for SamplingHandler 51 | 52 | ## 3.9.0 (2023-11-06) 53 | 54 | * Add support for the `WithMonologChannel` attribute of Monolog 3.5.0 to autoconfigure the `monolog.logger` tag 55 | * Add support for Symfony 7 56 | * Remove support for Symfony 4 57 | * Mark classes as internal when relevant 58 | * Add support for env placeholders in the `level` option of handlers 59 | 60 | ## 3.8.0 (2022-05-10) 61 | 62 | * Deprecated ambiguous `elasticsearch` type, use `elastica` instead 63 | * Added support for Monolog 3.0 (requires symfony/monolog-bridge 6.1) 64 | * Added support for `AsMonologProcessor` to autoconfigure processors 65 | * Added support for `FallbackGroupHandler` 66 | * Added support for `ElasticsearchHandler` as `elastic_search` type 67 | * Added support for `ElasticaHandler` as `elastica` type 68 | * Added support for `TelegramBotHandler` as `telegram` 69 | * Added `fill_extra_context` flag for `sentry` handlers 70 | * Added support for configuring PsrLogMessageProcessor (`date_format` and `remove_used_context_fields`) 71 | * Fixed issue on Windows + PHP 8, workaround for https://github.com/php/php-src/issues/8315 72 | * Fixed MongoDBHandler support when no client id is provided 73 | 74 | ## 3.7.1 (2021-11-05) 75 | 76 | * Indicate compatibility with Symfony 6 77 | 78 | ## 3.7.0 (2021-03-31) 79 | 80 | * Use `ActivationStrategy` instead of `actionLevel` when available 81 | * Register resettable processors (`ResettableInterface`) for autoconfiguration (tag: `kernel.reset`) 82 | * Drop support for Symfony 3.4 83 | * Drop support for PHP < 7.1 84 | * Fix call to undefined method pushProcessor on handler that does not implement ProcessableHandlerInterface 85 | * Use "use_locking" option with rotating file handler 86 | * Add ability to specify custom Sentry hub service 87 | 88 | ## 3.6.0 (2020-10-06) 89 | 90 | * Added support for Symfony Mailer 91 | * Added support for setting log levels from parameters or environment variables 92 | 93 | ## 3.5.0 (2019-11-13) 94 | 95 | * Added support for Monolog 2.0 96 | * Added `sentry` type to use sentry 2.0 client 97 | * Added `insightops` handler 98 | * Added possibility for auto-wire monolog channel according to the type-hinted aliases, introduced in the Symfony 4.2 99 | 100 | ## 3.4.0 (2019-06-20) 101 | 102 | * Deprecate "excluded_404s" option 103 | * Flush loggers on `kernel.reset` 104 | * Register processors (`ProcessorInterface`) for autoconfiguration (tag: `monolog.processor`) 105 | * Expose configuration for the `ConsoleHandler` 106 | * Fixed psr-3 processing being applied to all handlers, only leaf ones are now processing 107 | * Fixed regression when `app` channel is defined explicitly 108 | * Fixed handlers marked as nested not being ignored properly from the stack 109 | * Added support for Redis configuration 110 | * Drop support for Symfony <3 111 | 112 | ## 3.3.1 (2018-11-04) 113 | 114 | * Fixed compatibility with Symfony 4.2 115 | 116 | ## 3.3.0 (2018-06-04) 117 | 118 | * Fixed the autowiring of the channel logger in autoconfigured services 119 | * Added timeouts to the pushover, hipchat, slack handlers 120 | * Dropped support for PHP 5.3, 5.4, and HHVM 121 | * Added configuration for HttpCodeActivationStrategy 122 | * Deprecated "excluded_404s" option for Symfony >= 3.4 123 | 124 | ## 3.2.0 (2018-03-05) 125 | 126 | * Removed randomness from the container build 127 | * Fixed support for the `monolog.logger` tag specifying a channel in combination with Symfony 3.4+ autowiring 128 | * Fixed visibility of channels configured explicitly in the bundle config (they are now public in Symfony 4 too) 129 | * Fixed invalid service definitions 130 | 131 | ## 3.1.2 (2017-11-06) 132 | 133 | * fix invalid usage of count() 134 | 135 | ## 3.1.1 (2017-09-26) 136 | 137 | * added support for Symfony 4 138 | 139 | ## 3.1.0 (2017-03-26) 140 | 141 | * Added support for server_log handler 142 | * Allow configuring VERBOSITY_QUIET in console handlers 143 | * Fixed autowiring 144 | * Fixed slackbot handler not escaping channel names properly 145 | * Fixed slackbot handler requiring `slack_team` instead of `team` to be configured 146 | 147 | ## 3.0.3 (2017-01-10) 148 | 149 | * Fixed deprecation notices when using Symfony 3.3+ and PHP7+ 150 | 151 | ## 3.0.2 (2017-01-03) 152 | 153 | * Revert disabling DebugHandler in CLI environments 154 | * Update configuration for slack handlers for Monolog 1.22 new options 155 | * Revert the removal of the DebugHandlerPass (needed for Symfony <3.2) 156 | 157 | ## 3.0.1 (2016-11-15) 158 | 159 | * Removed obsolete code (DebugHandlerPass) 160 | 161 | ## 3.0.0 (2016-11-06) 162 | 163 | * Removed class parameters for the container configuration 164 | * Bumped minimum version of supported Symfony version to 2.7 165 | * Removed `NotFoundActivationStrategy` (the bundle now uses the class from MonologBridge) 166 | -------------------------------------------------------------------------------- /config/schema/monolog-1.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /src/DependencyInjection/MonologExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\MonologBundle\DependencyInjection; 13 | 14 | use Monolog\Attribute\AsMonologProcessor; 15 | use Monolog\Attribute\WithMonologChannel; 16 | use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; 17 | use Monolog\Handler\HandlerInterface; 18 | use Monolog\Processor\ProcessorInterface; 19 | use Monolog\Processor\PsrLogMessageProcessor; 20 | use Monolog\ResettableInterface; 21 | use Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy; 22 | use Symfony\Bridge\Monolog\Processor\TokenProcessor; 23 | use Symfony\Bundle\MonologBundle\MonologBundle; 24 | use Symfony\Component\Config\FileLocator; 25 | use Symfony\Component\DependencyInjection\Argument\BoundArgument; 26 | use Symfony\Component\DependencyInjection\ChildDefinition; 27 | use Symfony\Component\DependencyInjection\ContainerBuilder; 28 | use Symfony\Component\DependencyInjection\Definition; 29 | use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; 30 | use Symfony\Component\DependencyInjection\Reference; 31 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 32 | use Symfony\Contracts\HttpClient\HttpClientInterface; 33 | 34 | /** 35 | * MonologExtension is an extension for the Monolog library. 36 | * 37 | * @author Jordi Boggiano 38 | * @author Christophe Coevoet 39 | */ 40 | final class MonologExtension extends Extension 41 | { 42 | /** @var list */ 43 | private array $nestedHandlers = []; 44 | 45 | /** 46 | * Loads the Monolog configuration. 47 | * 48 | * @param array $configs An array of configuration settings 49 | * @param ContainerBuilder $container A ContainerBuilder instance 50 | */ 51 | public function load(array $configs, ContainerBuilder $container): void 52 | { 53 | $configuration = $this->getConfiguration($configs, $container); 54 | $config = $this->processConfiguration($configuration, $configs); 55 | 56 | if (isset($config['handlers'])) { 57 | $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config')); 58 | $loader->load('monolog.php'); 59 | 60 | $container->setParameter('monolog.use_microseconds', $config['use_microseconds']); 61 | 62 | $handlers = []; 63 | 64 | foreach ($config['handlers'] as $name => $handler) { 65 | if (!$handler['enabled']) { 66 | continue; 67 | } 68 | $handlers[$handler['priority']][] = [ 69 | 'id' => $this->buildHandler($container, $name, $handler), 70 | 'channels' => empty($handler['channels']) ? null : $handler['channels'], 71 | ]; 72 | } 73 | 74 | ksort($handlers); 75 | $sortedHandlers = []; 76 | foreach ($handlers as $priorityHandlers) { 77 | foreach (array_reverse($priorityHandlers) as $handler) { 78 | $sortedHandlers[] = $handler; 79 | } 80 | } 81 | 82 | $handlersToChannels = []; 83 | foreach ($sortedHandlers as $handler) { 84 | if (!\in_array($handler['id'], $this->nestedHandlers)) { 85 | $handlersToChannels[$handler['id']] = $handler['channels']; 86 | } 87 | } 88 | $container->setParameter('monolog.handlers_to_channels', $handlersToChannels); 89 | } 90 | 91 | $container->setParameter('monolog.additional_channels', $config['channels'] ?? []); 92 | 93 | $container->registerForAutoconfiguration(ProcessorInterface::class) 94 | ->addTag('monolog.processor'); 95 | $container->registerForAutoconfiguration(ResettableInterface::class) 96 | ->addTag('kernel.reset', ['method' => 'reset']); 97 | $container->registerForAutoconfiguration(TokenProcessor::class) 98 | ->addTag('monolog.processor'); 99 | 100 | if (interface_exists(HttpClientInterface::class)) { 101 | $handlerAutoconfiguration = $container->registerForAutoconfiguration(HandlerInterface::class); 102 | $handlerAutoconfiguration->setBindings($handlerAutoconfiguration->getBindings() + [ 103 | HttpClientInterface::class => new BoundArgument(new Reference('monolog.http_client'), false), 104 | ]); 105 | } 106 | 107 | $container->registerAttributeForAutoconfiguration(AsMonologProcessor::class, static function (ChildDefinition $definition, AsMonologProcessor $attribute, \Reflector $reflector): void { 108 | $tagAttributes = get_object_vars($attribute); 109 | if ($reflector instanceof \ReflectionMethod) { 110 | if (isset($tagAttributes['method'])) { 111 | throw new \LogicException(\sprintf('AsMonologProcessor attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name)); 112 | } 113 | 114 | $tagAttributes['method'] = $reflector->getName(); 115 | } 116 | 117 | $definition->addTag('monolog.processor', $tagAttributes); 118 | }); 119 | $container->registerAttributeForAutoconfiguration(WithMonologChannel::class, static function (ChildDefinition $definition, WithMonologChannel $attribute): void { 120 | $definition->addTag('monolog.logger', ['channel' => $attribute->channel]); 121 | }); 122 | } 123 | 124 | public function getXsdValidationBasePath(): string 125 | { 126 | return __DIR__.'/../../config/schema'; 127 | } 128 | 129 | public function getNamespace(): string 130 | { 131 | return 'http://symfony.com/schema/dic/monolog'; 132 | } 133 | 134 | private function buildHandler(ContainerBuilder $container, string $name, array $handler): string 135 | { 136 | $handlerId = $this->getHandlerId($name); 137 | if ('service' === $handler['type']) { 138 | $container->setAlias($handlerId, $handler['id']); 139 | 140 | if (!empty($handler['nested']) && true === $handler['nested']) { 141 | $this->markNestedHandler($handlerId); 142 | } 143 | 144 | return $handlerId; 145 | } 146 | 147 | $handlerClass = $this->getHandlerClassByType($handler['type']); 148 | $definition = new Definition($handlerClass); 149 | 150 | if ($handler['include_stacktraces']) { 151 | $definition->setConfigurator([MonologBundle::class, 'includeStacktraces']); 152 | } 153 | 154 | if (null === $handler['process_psr_3_messages']['enabled']) { 155 | $handler['process_psr_3_messages']['enabled'] = !isset($handler['handler']) && !$handler['members']; 156 | } 157 | 158 | if ($handler['process_psr_3_messages']['enabled'] && method_exists($handlerClass, 'pushProcessor')) { 159 | $processorId = $this->buildPsrLogMessageProcessor($container, $handler['process_psr_3_messages']); 160 | $definition->addMethodCall('pushProcessor', [new Reference($processorId)]); 161 | } 162 | 163 | switch ($handler['type']) { 164 | case 'stream': 165 | $definition->setArguments([ 166 | $handler['path'], 167 | $handler['level'], 168 | $handler['bubble'], 169 | $handler['file_permission'], 170 | $handler['use_locking'], 171 | ]); 172 | break; 173 | 174 | case 'console': 175 | $definition->setArguments([ 176 | null, 177 | $handler['bubble'], 178 | $handler['verbosity_levels'] ?? [], 179 | $handler['console_formatter_options'], 180 | $handler['interactive_only'], 181 | ]); 182 | $definition->addTag('kernel.event_subscriber'); 183 | break; 184 | 185 | case 'chromephp': 186 | case 'firephp': 187 | $definition->setArguments([ 188 | $handler['level'], 189 | $handler['bubble'], 190 | ]); 191 | $definition->addTag('kernel.event_listener', ['event' => 'kernel.response', 'method' => 'onKernelResponse']); 192 | break; 193 | 194 | case 'gelf': 195 | if (isset($handler['publisher']['id'])) { 196 | $publisher = new Reference($handler['publisher']['id']); 197 | } elseif (class_exists(\Gelf\Transport\UdpTransport::class)) { 198 | $transport = new Definition(\Gelf\Transport\UdpTransport::class, [ 199 | $handler['publisher']['hostname'], 200 | $handler['publisher']['port'], 201 | $handler['publisher']['chunk_size'], 202 | ]); 203 | $transport->setPublic(false); 204 | 205 | if (isset($handler['publisher']['encoder'])) { 206 | if ('compressed_json' === $handler['publisher']['encoder']) { 207 | $encoderClass = \Gelf\Encoder\CompressedJsonEncoder::class; 208 | } elseif ('json' === $handler['publisher']['encoder']) { 209 | $encoderClass = \Gelf\Encoder\JsonEncoder::class; 210 | } else { 211 | throw new \RuntimeException('The gelf message encoder must be either "compressed_json" or "json".'); 212 | } 213 | 214 | $encoder = new Definition($encoderClass); 215 | $encoder->setPublic(false); 216 | 217 | $transport->addMethodCall('setMessageEncoder', [$encoder]); 218 | } 219 | 220 | $publisher = new Definition(\Gelf\Publisher::class, []); 221 | $publisher->addMethodCall('addTransport', [$transport]); 222 | $publisher->setPublic(false); 223 | } else { 224 | throw new \RuntimeException('The gelf handler requires the graylog2/gelf-php package to be installed.'); 225 | } 226 | 227 | $definition->setArguments([ 228 | $publisher, 229 | $handler['level'], 230 | $handler['bubble'], 231 | ]); 232 | break; 233 | 234 | case 'mongodb': 235 | if (!class_exists(\MongoDB\Client::class)) { 236 | throw new \RuntimeException('The "mongodb" handler requires the mongodb/mongodb package to be installed.'); 237 | } 238 | 239 | if (isset($handler['mongodb']['id'])) { 240 | $client = new Reference($handler['mongodb']['id']); 241 | } else { 242 | $uriOptions = ['appname' => 'monolog-bundle']; 243 | 244 | if (isset($handler['mongodb']['username'])) { 245 | $uriOptions['username'] = $handler['mongodb']['username']; 246 | } 247 | 248 | if (isset($handler['mongodb']['password'])) { 249 | $uriOptions['password'] = $handler['mongodb']['password']; 250 | } 251 | 252 | $client = new Definition(\MongoDB\Client::class, [ 253 | $handler['mongodb']['uri'], 254 | $uriOptions, 255 | ]); 256 | } 257 | 258 | $definition->setArguments([ 259 | $client, 260 | $handler['mongodb']['database'], 261 | $handler['mongodb']['collection'], 262 | $handler['level'], 263 | $handler['bubble'], 264 | ]); 265 | 266 | if (empty($handler['formatter'])) { 267 | $formatter = new Definition(\Monolog\Formatter\MongoDBFormatter::class); 268 | $definition->addMethodCall('setFormatter', [$formatter]); 269 | } 270 | break; 271 | 272 | case 'elastica': 273 | case 'elastic_search': 274 | if (isset($handler['elasticsearch']['id'])) { 275 | $client = new Reference($handler['elasticsearch']['id']); 276 | } else { 277 | if ('elastic_search' === $handler['type']) { 278 | // v8 has a new Elastic\ prefix 279 | $client = new Definition(class_exists(\Elastic\Elasticsearch\Client::class) ? \Elastic\Elasticsearch\Client::class : \Elasticsearch\Client::class); 280 | $factory = class_exists(\Elastic\Elasticsearch\ClientBuilder::class) ? \Elastic\Elasticsearch\ClientBuilder::class : \Elasticsearch\ClientBuilder::class; 281 | $client->setFactory([$factory, 'fromConfig']); 282 | $clientArguments = [ 283 | 'hosts' => $handler['elasticsearch']['hosts'] ?? [$handler['elasticsearch']['host']], 284 | ]; 285 | 286 | if (isset($handler['elasticsearch']['user'], $handler['elasticsearch']['password'])) { 287 | $clientArguments['basicAuthentication'] = [$handler['elasticsearch']['user'], $handler['elasticsearch']['password']]; 288 | } 289 | } else { 290 | $client = new Definition(\Elastica\Client::class); 291 | 292 | if (isset($handler['elasticsearch']['hosts'])) { 293 | $clientArguments = [ 294 | 'hosts' => $handler['elasticsearch']['hosts'], 295 | 'transport' => $handler['elasticsearch']['transport'], 296 | ]; 297 | } else { 298 | $clientArguments = [ 299 | 'host' => $handler['elasticsearch']['host'], 300 | 'port' => $handler['elasticsearch']['port'], 301 | 'transport' => $handler['elasticsearch']['transport'], 302 | ]; 303 | } 304 | 305 | if (isset($handler['elasticsearch']['user'], $handler['elasticsearch']['password'])) { 306 | $clientArguments['headers'] = [ 307 | 'Authorization' => 'Basic '.base64_encode($handler['elasticsearch']['user'].':'.$handler['elasticsearch']['password']), 308 | ]; 309 | } 310 | } 311 | 312 | $client->setArguments([ 313 | $clientArguments, 314 | ]); 315 | 316 | $client->setPublic(false); 317 | } 318 | 319 | // elastica handler definition 320 | $definition->setArguments([ 321 | $client, 322 | [ 323 | 'index' => $handler['index'], 324 | 'type' => $handler['document_type'], 325 | 'ignore_error' => $handler['ignore_error'], 326 | ], 327 | $handler['level'], 328 | $handler['bubble'], 329 | ]); 330 | break; 331 | 332 | case 'telegram': 333 | $definition->setArguments([ 334 | $handler['token'], 335 | $handler['channel'], 336 | $handler['level'], 337 | $handler['bubble'], 338 | $handler['parse_mode'], 339 | $handler['disable_webpage_preview'], 340 | $handler['disable_notification'], 341 | $handler['split_long_messages'], 342 | $handler['delay_between_messages'], 343 | $handler['topic'], 344 | ]); 345 | break; 346 | 347 | case 'redis': 348 | case 'predis': 349 | if (isset($handler['redis']['id'])) { 350 | $clientId = $handler['redis']['id']; 351 | } elseif ('redis' === $handler['type']) { 352 | if (!class_exists(\Redis::class)) { 353 | throw new \RuntimeException('The \Redis class is not available.'); 354 | } 355 | 356 | $client = new Definition(\Redis::class); 357 | $client->addMethodCall('connect', [$handler['redis']['host'], $handler['redis']['port']]); 358 | $client->addMethodCall('auth', [$handler['redis']['password']]); 359 | $client->addMethodCall('select', [$handler['redis']['database']]); 360 | $client->setPublic(false); 361 | $clientId = uniqid('monolog.redis.client.', true); 362 | $container->setDefinition($clientId, $client); 363 | } else { 364 | if (!class_exists(\Predis\Client::class)) { 365 | throw new \RuntimeException('The \Predis\Client class is not available.'); 366 | } 367 | 368 | $client = new Definition(\Predis\Client::class); 369 | $client->setArguments([ 370 | $handler['redis']['host'], 371 | ]); 372 | $client->setPublic(false); 373 | 374 | $clientId = uniqid('monolog.predis.client.', true); 375 | $container->setDefinition($clientId, $client); 376 | } 377 | $definition->setArguments([ 378 | new Reference($clientId), 379 | $handler['redis']['key_name'], 380 | $handler['level'], 381 | $handler['bubble'], 382 | ]); 383 | break; 384 | 385 | case 'rotating_file': 386 | $definition->setArguments([ 387 | $handler['path'], 388 | $handler['max_files'], 389 | $handler['level'], 390 | $handler['bubble'], 391 | $handler['file_permission'], 392 | $handler['use_locking'], 393 | ]); 394 | $definition->addMethodCall('setFilenameFormat', [ 395 | $handler['filename_format'], 396 | $handler['date_format'], 397 | ]); 398 | break; 399 | 400 | case 'fingers_crossed': 401 | $nestedHandlerId = $this->getHandlerId($handler['handler']); 402 | $this->markNestedHandler($nestedHandlerId); 403 | 404 | $activation = new Definition(ErrorLevelActivationStrategy::class, [$handler['action_level']]); 405 | 406 | if (isset($handler['activation_strategy'])) { 407 | $activation = new Reference($handler['activation_strategy']); 408 | } elseif (!empty($handler['excluded_http_codes'])) { 409 | $activationDef = new Definition(HttpCodeActivationStrategy::class, [ 410 | new Reference('request_stack'), 411 | $handler['excluded_http_codes'], 412 | $activation, 413 | ]); 414 | $container->setDefinition($handlerId.'.http_code_strategy', $activationDef); 415 | $activation = new Reference($handlerId.'.http_code_strategy'); 416 | } 417 | 418 | $definition->setArguments([ 419 | new Reference($nestedHandlerId), 420 | $activation, 421 | $handler['buffer_size'], 422 | $handler['bubble'], 423 | $handler['stop_buffering'], 424 | $handler['passthru_level'], 425 | ]); 426 | break; 427 | 428 | case 'filter': 429 | $nestedHandlerId = $this->getHandlerId($handler['handler']); 430 | $this->markNestedHandler($nestedHandlerId); 431 | $minLevelOrList = !empty($handler['accepted_levels']) ? $handler['accepted_levels'] : $handler['min_level']; 432 | 433 | $definition->setArguments([ 434 | new Reference($nestedHandlerId), 435 | $minLevelOrList, 436 | $handler['max_level'], 437 | $handler['bubble'], 438 | ]); 439 | break; 440 | 441 | case 'buffer': 442 | $nestedHandlerId = $this->getHandlerId($handler['handler']); 443 | $this->markNestedHandler($nestedHandlerId); 444 | 445 | $definition->setArguments([ 446 | new Reference($nestedHandlerId), 447 | $handler['buffer_size'], 448 | $handler['level'], 449 | $handler['bubble'], 450 | $handler['flush_on_overflow'], 451 | ]); 452 | break; 453 | 454 | case 'deduplication': 455 | $nestedHandlerId = $this->getHandlerId($handler['handler']); 456 | $this->markNestedHandler($nestedHandlerId); 457 | $defaultStore = '%kernel.cache_dir%/monolog_dedup_'.sha1($handlerId); 458 | 459 | $definition->setArguments([ 460 | new Reference($nestedHandlerId), 461 | $handler['store'] ?? $defaultStore, 462 | $handler['deduplication_level'], 463 | $handler['time'], 464 | $handler['bubble'], 465 | ]); 466 | break; 467 | 468 | case 'group': 469 | case 'whatfailuregroup': 470 | case 'fallbackgroup': 471 | $references = []; 472 | foreach ($handler['members'] as $nestedHandler) { 473 | $nestedHandlerId = $this->getHandlerId($nestedHandler); 474 | $this->markNestedHandler($nestedHandlerId); 475 | $references[] = new Reference($nestedHandlerId); 476 | } 477 | 478 | $definition->setArguments([ 479 | $references, 480 | $handler['bubble'], 481 | ]); 482 | break; 483 | 484 | case 'syslog': 485 | $definition->setArguments([ 486 | $handler['ident'], 487 | $handler['facility'], 488 | $handler['level'], 489 | $handler['bubble'], 490 | $handler['logopts'], 491 | ]); 492 | break; 493 | 494 | case 'syslogudp': 495 | $definition->setArguments([ 496 | $handler['host'], 497 | $handler['port'], 498 | $handler['facility'], 499 | $handler['level'], 500 | $handler['bubble'], 501 | ]); 502 | if ($handler['ident']) { 503 | $definition->addArgument($handler['ident']); 504 | } 505 | break; 506 | 507 | case 'native_mailer': 508 | $definition->setArguments([ 509 | $handler['to_email'], 510 | $handler['subject'], 511 | $handler['from_email'], 512 | $handler['level'], 513 | $handler['bubble'], 514 | ]); 515 | if (!empty($handler['headers'])) { 516 | $definition->addMethodCall('addHeader', [$handler['headers']]); 517 | } 518 | break; 519 | 520 | case 'symfony_mailer': 521 | $mailer = $handler['mailer'] ?: 'mailer.mailer'; 522 | if (isset($handler['email_prototype'])) { 523 | if (!empty($handler['email_prototype']['method'])) { 524 | $prototype = [new Reference($handler['email_prototype']['id']), $handler['email_prototype']['method']]; 525 | } else { 526 | $prototype = new Reference($handler['email_prototype']['id']); 527 | } 528 | } else { 529 | $prototype = (new Definition(\Symfony\Component\Mime\Email::class)) 530 | ->setPublic(false) 531 | ->addMethodCall('from', [$handler['from_email']]) 532 | ->addMethodCall('to', $handler['to_email']) 533 | ->addMethodCall('subject', [$handler['subject']]); 534 | } 535 | $definition->setArguments([ 536 | new Reference($mailer), 537 | $prototype, 538 | $handler['level'], 539 | $handler['bubble'], 540 | ]); 541 | break; 542 | 543 | case 'socket': 544 | $definition->setArguments([ 545 | $handler['connection_string'], 546 | $handler['level'], 547 | $handler['bubble'], 548 | ]); 549 | if (isset($handler['timeout'])) { 550 | $definition->addMethodCall('setTimeout', [$handler['timeout']]); 551 | } 552 | if (isset($handler['connection_timeout'])) { 553 | $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); 554 | } 555 | if (isset($handler['persistent'])) { 556 | $definition->addMethodCall('setPersistent', [$handler['persistent']]); 557 | } 558 | break; 559 | 560 | case 'pushover': 561 | $definition->setArguments([ 562 | $handler['token'], 563 | $handler['user'], 564 | $handler['title'], 565 | $handler['level'], 566 | $handler['bubble'], 567 | ]); 568 | if (isset($handler['timeout'])) { 569 | $definition->addMethodCall('setTimeout', [$handler['timeout']]); 570 | } 571 | if (isset($handler['connection_timeout'])) { 572 | $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); 573 | } 574 | break; 575 | 576 | case 'slack': 577 | $definition->setArguments([ 578 | $handler['token'], 579 | $handler['channel'], 580 | $handler['bot_name'], 581 | $handler['use_attachment'], 582 | $handler['icon_emoji'], 583 | $handler['level'], 584 | $handler['bubble'], 585 | $handler['use_short_attachment'], 586 | $handler['include_extra'], 587 | $handler['exclude_fields'], 588 | ]); 589 | if (isset($handler['timeout'])) { 590 | $definition->addMethodCall('setTimeout', [$handler['timeout']]); 591 | } 592 | if (isset($handler['connection_timeout'])) { 593 | $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); 594 | } 595 | break; 596 | 597 | case 'slackwebhook': 598 | $definition->setArguments([ 599 | $handler['webhook_url'], 600 | $handler['channel'], 601 | $handler['bot_name'], 602 | $handler['use_attachment'], 603 | $handler['icon_emoji'], 604 | $handler['use_short_attachment'], 605 | $handler['include_extra'], 606 | $handler['level'], 607 | $handler['bubble'], 608 | $handler['exclude_fields'], 609 | ]); 610 | break; 611 | 612 | case 'cube': 613 | $definition->setArguments([ 614 | $handler['url'], 615 | $handler['level'], 616 | $handler['bubble'], 617 | ]); 618 | break; 619 | 620 | case 'amqp': 621 | $definition->setArguments([ 622 | new Reference($handler['exchange']), 623 | $handler['exchange_name'], 624 | $handler['level'], 625 | $handler['bubble'], 626 | ]); 627 | break; 628 | 629 | case 'error_log': 630 | $definition->setArguments([ 631 | $handler['message_type'], 632 | $handler['level'], 633 | $handler['bubble'], 634 | ]); 635 | break; 636 | 637 | case 'loggly': 638 | $definition->setArguments([ 639 | $handler['token'], 640 | $handler['level'], 641 | $handler['bubble'], 642 | ]); 643 | if (!empty($handler['tags'])) { 644 | $definition->addMethodCall('setTag', [implode(',', $handler['tags'])]); 645 | } 646 | break; 647 | 648 | case 'logentries': 649 | $definition->setArguments([ 650 | $handler['token'], 651 | $handler['use_ssl'], 652 | $handler['level'], 653 | $handler['bubble'], 654 | ]); 655 | if (isset($handler['timeout'])) { 656 | $definition->addMethodCall('setTimeout', [$handler['timeout']]); 657 | } 658 | if (isset($handler['connection_timeout'])) { 659 | $definition->addMethodCall('setConnectionTimeout', [$handler['connection_timeout']]); 660 | } 661 | break; 662 | 663 | case 'insightops': 664 | $definition->setArguments([ 665 | $handler['token'], 666 | $handler['region'] ?: 'us', 667 | $handler['use_ssl'], 668 | $handler['level'], 669 | $handler['bubble'], 670 | ]); 671 | break; 672 | 673 | case 'flowdock': 674 | $definition->setArguments([ 675 | $handler['token'], 676 | $handler['level'], 677 | $handler['bubble'], 678 | ]); 679 | 680 | if (empty($handler['formatter'])) { 681 | $formatter = new Definition(\Monolog\Formatter\FlowdockFormatter::class, [ 682 | $handler['source'], 683 | $handler['from_email'], 684 | ]); 685 | $formatterId = 'monolog.flowdock.formatter.'.sha1($handler['source'].'|'.$handler['from_email']); 686 | $formatter->setPublic(false); 687 | $container->setDefinition($formatterId, $formatter); 688 | 689 | $definition->addMethodCall('setFormatter', [new Reference($formatterId)]); 690 | } 691 | break; 692 | 693 | case 'rollbar': 694 | if (!empty($handler['id'])) { 695 | $rollbarId = $handler['id']; 696 | } else { 697 | $config = $handler['config'] ?: []; 698 | $config['access_token'] = $handler['token']; 699 | $rollbar = new Definition(\Rollbar\RollbarLogger::class, [ 700 | $config, 701 | ]); 702 | $rollbarId = 'monolog.rollbar.notifier.'.sha1(json_encode($config)); 703 | $rollbar->setPublic(false); 704 | $container->setDefinition($rollbarId, $rollbar); 705 | } 706 | 707 | $definition->setArguments([ 708 | new Reference($rollbarId), 709 | $handler['level'], 710 | $handler['bubble'], 711 | ]); 712 | break; 713 | 714 | case 'newrelic': 715 | $definition->setArguments([ 716 | $handler['level'], 717 | $handler['bubble'], 718 | $handler['app_name'], 719 | ]); 720 | break; 721 | 722 | case 'server_log': 723 | $definition->setArguments([ 724 | $handler['host'], 725 | $handler['level'], 726 | $handler['bubble'], 727 | ]); 728 | break; 729 | 730 | case 'sampling': 731 | $nestedHandlerId = $this->getHandlerId($handler['handler']); 732 | $this->markNestedHandler($nestedHandlerId); 733 | 734 | $definition->setArguments([ 735 | new Reference($nestedHandlerId), 736 | $handler['factor'], 737 | ]); 738 | break; 739 | 740 | // Handlers using the constructor of AbstractHandler without adding their own arguments 741 | case 'browser_console': 742 | case 'test': 743 | case 'null': 744 | case 'noop': 745 | $definition->setArguments([ 746 | $handler['level'], 747 | $handler['bubble'], 748 | ]); 749 | break; 750 | 751 | default: 752 | $nullWarning = ''; 753 | if ('' == $handler['type']) { 754 | $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'; 755 | } 756 | 757 | throw new \InvalidArgumentException(\sprintf('Invalid handler type "%s" given for handler "%s".'.$nullWarning, $handler['type'], $name)); 758 | } 759 | 760 | if (!empty($handler['nested']) && true === $handler['nested']) { 761 | $this->markNestedHandler($handlerId); 762 | } 763 | 764 | if (!empty($handler['formatter'])) { 765 | $definition->addMethodCall('setFormatter', [new Reference($handler['formatter'])]); 766 | } 767 | 768 | if (!\in_array($handlerId, $this->nestedHandlers) && is_subclass_of($handlerClass, ResettableInterface::class)) { 769 | $definition->addTag('kernel.reset', ['method' => 'reset']); 770 | } 771 | 772 | $container->setDefinition($handlerId, $definition); 773 | 774 | return $handlerId; 775 | } 776 | 777 | private function markNestedHandler(string $nestedHandlerId): void 778 | { 779 | if (\in_array($nestedHandlerId, $this->nestedHandlers, true)) { 780 | return; 781 | } 782 | 783 | $this->nestedHandlers[] = $nestedHandlerId; 784 | } 785 | 786 | private function getHandlerId(string $name): string 787 | { 788 | return \sprintf('monolog.handler.%s', $name); 789 | } 790 | 791 | private function getHandlerClassByType(string $handlerType): string 792 | { 793 | return match ($handlerType) { 794 | 'stream' => \Monolog\Handler\StreamHandler::class, 795 | 'console' => \Symfony\Bridge\Monolog\Handler\ConsoleHandler::class, 796 | 'group' => \Monolog\Handler\GroupHandler::class, 797 | 'buffer' => \Monolog\Handler\BufferHandler::class, 798 | 'deduplication' => \Monolog\Handler\DeduplicationHandler::class, 799 | 'rotating_file' => \Monolog\Handler\RotatingFileHandler::class, 800 | 'syslog' => \Monolog\Handler\SyslogHandler::class, 801 | 'syslogudp' => \Monolog\Handler\SyslogUdpHandler::class, 802 | 'null' => \Monolog\Handler\NullHandler::class, 803 | 'test' => \Monolog\Handler\TestHandler::class, 804 | 'gelf' => \Monolog\Handler\GelfHandler::class, 805 | 'rollbar' => \Monolog\Handler\RollbarHandler::class, 806 | 'flowdock' => \Monolog\Handler\FlowdockHandler::class, 807 | 'browser_console' => \Monolog\Handler\BrowserConsoleHandler::class, 808 | 'firephp' => \Symfony\Bridge\Monolog\Handler\FirePHPHandler::class, 809 | 'chromephp' => \Symfony\Bridge\Monolog\Handler\ChromePhpHandler::class, 810 | 'native_mailer' => \Monolog\Handler\NativeMailerHandler::class, 811 | 'symfony_mailer' => \Symfony\Bridge\Monolog\Handler\MailerHandler::class, 812 | 'socket' => \Monolog\Handler\SocketHandler::class, 813 | 'pushover' => \Monolog\Handler\PushoverHandler::class, 814 | 'newrelic' => \Monolog\Handler\NewRelicHandler::class, 815 | 'slack' => \Monolog\Handler\SlackHandler::class, 816 | 'slackwebhook' => \Monolog\Handler\SlackWebhookHandler::class, 817 | 'cube' => \Monolog\Handler\CubeHandler::class, 818 | 'amqp' => \Monolog\Handler\AmqpHandler::class, 819 | 'error_log' => \Monolog\Handler\ErrorLogHandler::class, 820 | 'loggly' => \Monolog\Handler\LogglyHandler::class, 821 | 'logentries' => \Monolog\Handler\LogEntriesHandler::class, 822 | 'whatfailuregroup' => \Monolog\Handler\WhatFailureGroupHandler::class, 823 | 'fingers_crossed' => \Monolog\Handler\FingersCrossedHandler::class, 824 | 'filter' => \Monolog\Handler\FilterHandler::class, 825 | 'mongodb' => \Monolog\Handler\MongoDBHandler::class, 826 | 'telegram' => \Monolog\Handler\TelegramBotHandler::class, 827 | 'server_log' => \Symfony\Bridge\Monolog\Handler\ServerLogHandler::class, 828 | 'redis', 'predis' => \Monolog\Handler\RedisHandler::class, 829 | 'insightops' => \Monolog\Handler\InsightOpsHandler::class, 830 | 'sampling' => \Monolog\Handler\SamplingHandler::class, 831 | 'elastica' => \Monolog\Handler\ElasticaHandler::class, 832 | 'elastic_search' => \Monolog\Handler\ElasticsearchHandler::class, 833 | 'fallbackgroup' => \Monolog\Handler\FallbackGroupHandler::class, 834 | 'noop' => \Monolog\Handler\NoopHandler::class, 835 | default => throw new \InvalidArgumentException(\sprintf('There is no handler class defined for handler "%s".', $handlerType)), 836 | }; 837 | } 838 | 839 | private function buildPsrLogMessageProcessor(ContainerBuilder $container, array $processorOptions): string 840 | { 841 | $processorId = 'monolog.processor.psr_log_message'; 842 | $processorArguments = []; 843 | 844 | unset($processorOptions['enabled']); 845 | 846 | if ($processorOptions) { 847 | $processorArguments = [ 848 | $processorOptions['date_format'] ?? null, 849 | $processorOptions['remove_used_context_fields'] ?? false, 850 | ]; 851 | $processorId .= '.'.ContainerBuilder::hash($processorArguments); 852 | } 853 | 854 | if (!$container->hasDefinition($processorId)) { 855 | $processor = new Definition(PsrLogMessageProcessor::class); 856 | $processor->setPublic(false); 857 | $processor->setArguments($processorArguments); 858 | $container->setDefinition($processorId, $processor); 859 | } 860 | 861 | return $processorId; 862 | } 863 | } 864 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\MonologBundle\DependencyInjection; 13 | 14 | use Composer\InstalledVersions; 15 | use Monolog\Level; 16 | use Monolog\Logger; 17 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 18 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 19 | use Symfony\Component\Config\Definition\ConfigurationInterface; 20 | use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; 21 | 22 | /** 23 | * This class contains the configuration information for the bundle. 24 | * 25 | * This information is solely responsible for how the different configuration 26 | * sections are normalized, and merged. 27 | * 28 | * Possible handler types and related configurations (brackets indicate optional params): 29 | * 30 | * - service: 31 | * - id 32 | * 33 | * - stream: 34 | * - path: string 35 | * - [level]: level name or int value, defaults to DEBUG 36 | * - [bubble]: bool, defaults to true 37 | * - [file_permission]: int|null, defaults to null (0644) 38 | * - [use_locking]: bool, defaults to false 39 | * 40 | * - console: 41 | * - [verbosity_levels]: level => verbosity configuration 42 | * - [level]: level name or int value, defaults to DEBUG 43 | * - [bubble]: bool, defaults to true 44 | * - [console_formatter_options]: array 45 | * - [interactive_only]: bool, defaults to false 46 | * 47 | * - firephp: 48 | * - [level]: level name or int value, defaults to DEBUG 49 | * - [bubble]: bool, defaults to true 50 | * 51 | * - browser_console: 52 | * - [level]: level name or int value, defaults to DEBUG 53 | * - [bubble]: bool, defaults to true 54 | * 55 | * - gelf: 56 | * - publisher: (one of the following configurations) 57 | * # Option 1: Service-based configuration 58 | * - id: string, service id of a publisher implementation 59 | * 60 | * # Option 2: Direct connection configuration 61 | * - hostname: string, server hostname 62 | * - [port]: int, server port (default: 12201) 63 | * - [chunk_size]: int, UDP packet size (default: 1420) 64 | * - [encoder]: string, encoding format ('json' or 'compressed_json') 65 | * - [level]: level name or int value, defaults to DEBUG 66 | * - [bubble]: bool, defaults to true 67 | * 68 | * - chromephp: 69 | * - [level]: level name or int value, defaults to DEBUG 70 | * - [bubble]: bool, defaults to true 71 | * 72 | * - rotating_file: 73 | * - path: string 74 | * - [max_files]: files to keep, defaults to zero (infinite) 75 | * - [level]: level name or int value, defaults to DEBUG 76 | * - [bubble]: bool, defaults to true 77 | * - [file_permission]: int|null, defaults to null (0o644) 78 | * - [use_locking]: bool, defaults to false 79 | * - [filename_format]: string, defaults to '{filename}-{date}' 80 | * - [date_format]: string, defaults to 'Y-m-d' 81 | * 82 | * - mongodb: 83 | * - mongodb: 84 | * - id: optional if uri is given 85 | * - uri: MongoDB connection string, optional if id is given 86 | * - [username]: Username for database authentication 87 | * - [password]: Password for database authentication 88 | * - [database]: Database to which logs are written (not used for auth), defaults to "monolog" 89 | * - [collection]: Collection to which logs are written, defaults to "logs" 90 | * - [level]: level name or int value, defaults to DEBUG 91 | * - [bubble]: bool, defaults to true 92 | * 93 | * - elastic_search: 94 | * - elasticsearch: 95 | * - id: optional if host is given 96 | * - host: elastic search host name, with scheme (e.g. "https://127.0.0.1:9200") 97 | * - [user]: elastic search user name 98 | * - [password]: elastic search user password 99 | * - [index]: index name, defaults to monolog 100 | * - [document_type]: document_type, defaults to logs 101 | * - [level]: level name or int value, defaults to DEBUG 102 | * - [bubble]: bool, defaults to true 103 | * 104 | * - elastica: 105 | * - elasticsearch: 106 | * - id: optional if host is given 107 | * - host: elastic search host name. Do not prepend with http(s):// 108 | * - [port]: defaults to 9200 109 | * - [transport]: transport protocol (http by default) 110 | * - [user]: elastic search user name 111 | * - [password]: elastic search user password 112 | * - [index]: index name, defaults to monolog 113 | * - [document_type]: document_type, defaults to logs 114 | * - [level]: level name or int value, defaults to DEBUG 115 | * - [bubble]: bool, defaults to true 116 | * 117 | * - redis: 118 | * - redis: 119 | * - id: optional if host is given 120 | * - host: 127.0.0.1 121 | * - password: null 122 | * - port: 6379 123 | * - database: 0 124 | * - key_name: monolog_redis 125 | * 126 | * - predis: 127 | * - redis: 128 | * - id: optional if host is given 129 | * - host: tcp://10.0.0.1:6379 130 | * - key_name: monolog_redis 131 | * 132 | * - fingers_crossed: 133 | * - handler: the wrapped handler's name 134 | * - [action_level|activation_strategy]: minimum level or service id to activate the handler, defaults to WARNING 135 | * - [excluded_http_codes]: if set, the strategy will be changed to one that excludes specific HTTP codes (requires Symfony Monolog bridge 4.1+) 136 | * - [buffer_size]: defaults to 0 (unlimited) 137 | * - [stop_buffering]: bool to disable buffering once the handler has been activated, defaults to true 138 | * - [passthru_level]: level name or int value for messages to always flush, disabled by default 139 | * - [bubble]: bool, defaults to true 140 | * 141 | * - filter: 142 | * - handler: the wrapped handler's name 143 | * - [accepted_levels]: list of levels to accept 144 | * - [min_level]: minimum level to accept (only used if accepted_levels not specified) 145 | * - [max_level]: maximum level to accept (only used if accepted_levels not specified) 146 | * - [bubble]: bool, defaults to true 147 | * 148 | * - buffer: 149 | * - handler: the wrapped handler's name 150 | * - [buffer_size]: defaults to 0 (unlimited) 151 | * - [level]: level name or int value, defaults to DEBUG 152 | * - [bubble]: bool, defaults to true 153 | * - [flush_on_overflow]: bool, defaults to false 154 | * 155 | * - deduplication: 156 | * - handler: the wrapped handler's name 157 | * - [store]: The file/path where the deduplication log should be kept, defaults to %kernel.cache_dir%/monolog_dedup_* 158 | * - [deduplication_level]: The minimum logging level for log records to be looked at for deduplication purposes, defaults to ERROR 159 | * - [time]: The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through, defaults to 60 160 | * - [bubble]: bool, defaults to true 161 | * 162 | * - group: 163 | * - members: the wrapped handlers by name 164 | * - [bubble]: bool, defaults to true 165 | * 166 | * - whatfailuregroup: 167 | * - members: the wrapped handlers by name 168 | * - [bubble]: bool, defaults to true 169 | * 170 | * - fallbackgroup 171 | * - members: the wrapped handlers by name 172 | * - [bubble]: bool, defaults to true 173 | * 174 | * - syslog: 175 | * - ident: string 176 | * - [facility]: defaults to 'user', use any of the LOG_* facility constant but without LOG_ prefix, e.g. user for LOG_USER 177 | * - [logopts]: defaults to LOG_PID 178 | * - [level]: level name or int value, defaults to DEBUG 179 | * - [bubble]: bool, defaults to true 180 | * 181 | * - syslogudp: 182 | * - host: syslogd host name 183 | * - [port]: defaults to 514 184 | * - [facility]: defaults to 'user', use any of the LOG_* facility constant but without LOG_ prefix, e.g. user for LOG_USER 185 | * - [logopts]: defaults to LOG_PID 186 | * - [level]: level name or int value, defaults to DEBUG 187 | * - [bubble]: bool, defaults to true 188 | * - [ident]: string, defaults to 189 | * 190 | * - native_mailer: 191 | * - from_email: string 192 | * - to_email: string 193 | * - subject: string 194 | * - [level]: level name or int value, defaults to DEBUG 195 | * - [bubble]: bool, defaults to true 196 | * - [headers]: optional array containing additional headers: ['Foo: Bar', '...'] 197 | * 198 | * - symfony_mailer: 199 | * - from_email: optional if email_prototype is given 200 | * - to_email: optional if email_prototype is given 201 | * - subject: optional if email_prototype is given 202 | * - [email_prototype]: service id of a message, defaults to a default message with the three fields above 203 | * - [mailer]: mailer service id, defaults to mailer.mailer 204 | * - [level]: level name or int value, defaults to DEBUG 205 | * - [bubble]: bool, defaults to true 206 | * 207 | * - socket: 208 | * - connection_string: string 209 | * - [timeout]: float 210 | * - [connection_timeout]: float 211 | * - [persistent]: bool 212 | * - [level]: level name or int value, defaults to DEBUG 213 | * - [bubble]: bool, defaults to true 214 | * 215 | * - pushover: 216 | * - token: pushover api token 217 | * - user: user id or array of ids 218 | * - [title]: optional title for messages, defaults to the server hostname 219 | * - [level]: level name or int value, defaults to DEBUG 220 | * - [bubble]: bool, defaults to true 221 | * - [timeout]: float 222 | * - [connection_timeout]: float 223 | * 224 | * - newrelic: 225 | * - [level]: level name or int value, defaults to DEBUG 226 | * - [bubble]: bool, defaults to true 227 | * - [app_name]: new relic app name, default null 228 | * 229 | * - slack: 230 | * - token: slack api token 231 | * - channel: channel name (with starting #) 232 | * - [bot_name]: defaults to Monolog 233 | * - [icon_emoji]: defaults to null 234 | * - [use_attachment]: bool, defaults to true 235 | * - [use_short_attachment]: bool, defaults to false 236 | * - [include_extra]: bool, defaults to false 237 | * - [level]: level name or int value, defaults to DEBUG 238 | * - [bubble]: bool, defaults to true 239 | * - [timeout]: float 240 | * - [connection_timeout]: float 241 | * - [exclude_fields]: list of excluded fields, defaults to empty array 242 | * 243 | * - slackwebhook: 244 | * - webhook_url: slack webhook URL 245 | * - channel: channel name (with starting #) 246 | * - [bot_name]: defaults to Monolog 247 | * - [icon_emoji]: defaults to null 248 | * - [use_attachment]: bool, defaults to true 249 | * - [use_short_attachment]: bool, defaults to false 250 | * - [include_extra]: bool, defaults to false 251 | * - [level]: level name or int value, defaults to DEBUG 252 | * - [bubble]: bool, defaults to true 253 | * - [exclude_fields]: list of excluded fields, defaults to empty array 254 | * 255 | * - cube: 256 | * - url: http/udp url to the cube server 257 | * - [level]: level name or int value, defaults to DEBUG 258 | * - [bubble]: bool, defaults to true 259 | * 260 | * - amqp: 261 | * - exchange: service id of an AMQPExchange 262 | * - [exchange_name]: string, defaults to log 263 | * - [level]: level name or int value, defaults to DEBUG 264 | * - [bubble]: bool, defaults to true 265 | * 266 | * - error_log: 267 | * - [message_type]: int 0 or 4, defaults to 0 268 | * - [level]: level name or int value, defaults to DEBUG 269 | * - [bubble]: bool, defaults to true 270 | * 271 | * - null: 272 | * - [level]: level name or int value, defaults to DEBUG 273 | * - [bubble]: bool, defaults to true 274 | * 275 | * - test: 276 | * - [level]: level name or int value, defaults to DEBUG 277 | * - [bubble]: bool, defaults to true 278 | * 279 | * - debug: 280 | * - [level]: level name or int value, defaults to DEBUG 281 | * - [bubble]: bool, defaults to true 282 | * 283 | * - loggly: 284 | * - token: loggly api token 285 | * - [level]: level name or int value, defaults to DEBUG 286 | * - [bubble]: bool, defaults to true 287 | * - [tags]: tag names 288 | * 289 | * - logentries: 290 | * - token: logentries api token 291 | * - [use_ssl]: whether or not SSL encryption should be used, defaults to true 292 | * - [level]: level name or int value, defaults to DEBUG 293 | * - [bubble]: bool, defaults to true 294 | * - [timeout]: float 295 | * - [connection_timeout]: float 296 | * 297 | * - insightops: 298 | * - token: Log token supplied by InsightOps 299 | * - region: Region where InsightOps account is hosted. Could be 'us' or 'eu'. Defaults to 'us' 300 | * - [use_ssl]: whether or not SSL encryption should be used, defaults to true 301 | * - [level]: level name or int value, defaults to DEBUG 302 | * - [bubble]: bool, defaults to true 303 | * 304 | * - flowdock: 305 | * - token: flowdock api token 306 | * - source: human readable identifier of the application 307 | * - from_email: email address of the message sender 308 | * - [level]: level name or int value, defaults to DEBUG 309 | * - [bubble]: bool, defaults to true 310 | * 311 | * - rollbar: 312 | * - id: RollbarNotifier service (mandatory if token is not provided) 313 | * - token: rollbar api token (skip if you provide a RollbarNotifier service id) 314 | * - [config]: config values from https://github.com/rollbar/rollbar-php#configuration-reference 315 | * - [level]: level name or int value, defaults to DEBUG 316 | * - [bubble]: bool, defaults to true 317 | * 318 | * - server_log: 319 | * - host: server log host. ex: 127.0.0.1:9911 320 | * - [level]: level name or int value, defaults to DEBUG 321 | * - [bubble]: bool, defaults to true 322 | * 323 | * - telegram: 324 | * - token: Telegram bot access token provided by BotFather 325 | * - channel: Telegram channel name 326 | * - [level]: level name or int value, defaults to DEBUG 327 | * - [bubble]: bool, defaults to true 328 | * - [parse_mode]: optional the kind of formatting that is used for the message 329 | * - [disable_webpage_preview]: bool, defaults to false, disables link previews for links in the message 330 | * - [disable_notification]: bool, defaults to false, sends the message silently. Users will receive a notification with no sound 331 | * - [split_long_messages]: bool, defaults to false, split messages longer than 4096 bytes into multiple messages 332 | * - [delay_between_messages]: bool, defaults to false, adds a 1sec delay/sleep between sending split messages 333 | * - [topic]: optional the unique identifier for the target message thread (topic) of the forum; for forum supergroups only 334 | * 335 | * - sampling: 336 | * - handler: the wrapped handler's name 337 | * - factor: the sampling factor (e.g. 10 means every ~10th record is sampled) 338 | * 339 | * All handlers can also be marked with `nested: true` to make sure they are never added explicitly to the stack 340 | * 341 | * @author Jordi Boggiano 342 | * @author Christophe Coevoet 343 | */ 344 | final class Configuration implements ConfigurationInterface 345 | { 346 | /** 347 | * Generates the configuration tree builder. 348 | */ 349 | public function getConfigTreeBuilder(): TreeBuilder 350 | { 351 | $treeBuilder = new TreeBuilder('monolog'); 352 | $rootNode = $treeBuilder->getRootNode(); 353 | 354 | $handlers = $rootNode 355 | ->fixXmlConfig('channel') 356 | ->fixXmlConfig('handler') 357 | ->children() 358 | ->scalarNode('use_microseconds')->defaultTrue()->end() 359 | ->arrayNode('channels') 360 | ->canBeUnset() 361 | ->prototype('scalar')->end() 362 | ->end() 363 | ->arrayNode('handlers'); 364 | 365 | $handlers 366 | ->canBeUnset() 367 | ->useAttributeAsKey('name') 368 | ->validate() 369 | ->ifTrue(function ($v) { return isset($v['debug']); }) 370 | ->thenInvalid('The "debug" name cannot be used as it is reserved for the handler of the profiler') 371 | ->end() 372 | ->example([ 373 | 'syslog' => [ 374 | 'type' => 'stream', 375 | 'path' => '/var/log/symfony.log', 376 | 'level' => 'ERROR', 377 | 'bubble' => 'false', 378 | 'formatter' => 'my_formatter', 379 | ], 380 | 'main' => [ 381 | 'type' => 'fingers_crossed', 382 | 'action_level' => 'WARNING', 383 | 'buffer_size' => 30, 384 | 'handler' => 'custom', 385 | ], 386 | 'custom' => [ 387 | 'type' => 'service', 388 | 'id' => 'my_handler', 389 | ], 390 | ]); 391 | 392 | $handlerNode = $handlers 393 | ->prototype('array') 394 | ->fixXmlConfig('member') 395 | ->fixXmlConfig('excluded_http_code') 396 | ->fixXmlConfig('tag') 397 | ->fixXmlConfig('accepted_level') 398 | ->fixXmlConfig('header') 399 | ->canBeUnset(); 400 | 401 | $handlerNode 402 | ->children() 403 | ->scalarNode('type') 404 | ->isRequired() 405 | ->beforeNormalization() 406 | ->ifString()->then(function ($v) { return strtolower($v); }) 407 | ->ifNull()->then(function ($v) { return 'null'; }) 408 | ->end() 409 | ->end() 410 | ->scalarNode('id')->end() // service & rollbar 411 | ->booleanNode('enabled')->defaultTrue()->end() 412 | ->scalarNode('priority')->defaultValue(0)->end() 413 | ->scalarNode('level')->defaultValue('DEBUG')->end() 414 | ->booleanNode('bubble')->defaultTrue()->end() 415 | ->booleanNode('interactive_only')->defaultFalse()->end() 416 | ->scalarNode('app_name')->defaultNull()->end() 417 | ->booleanNode('include_stacktraces')->defaultFalse()->end() 418 | ->arrayNode('process_psr_3_messages') 419 | ->addDefaultsIfNotSet() 420 | ->beforeNormalization() 421 | ->ifTrue(static function ($v) { return !\is_array($v); }) 422 | ->then(static function ($v) { return ['enabled' => $v]; }) 423 | ->end() 424 | ->children() 425 | ->booleanNode('enabled')->defaultNull()->end() 426 | ->scalarNode('date_format')->end() 427 | ->booleanNode('remove_used_context_fields')->end() 428 | ->end() 429 | ->end() 430 | ->scalarNode('path')->defaultValue('%kernel.logs_dir%/%kernel.environment%.log')->end() // stream and rotating 431 | ->scalarNode('file_permission') // stream and rotating 432 | ->defaultNull() 433 | ->beforeNormalization() 434 | ->ifString() 435 | ->then(function ($v) { 436 | if (str_starts_with($v, '0')) { 437 | return octdec($v); 438 | } 439 | 440 | return (int) $v; 441 | }) 442 | ->end() 443 | ->end() 444 | ->booleanNode('use_locking')->defaultFalse()->end() // stream and rotating 445 | ->scalarNode('filename_format')->defaultValue('{filename}-{date}')->end() // rotating 446 | ->scalarNode('date_format')->defaultValue('Y-m-d')->end() // rotating 447 | ->scalarNode('ident')->defaultFalse()->end() // syslog and syslogudp 448 | ->scalarNode('logopts')->defaultValue(\LOG_PID)->end() // syslog 449 | ->scalarNode('facility')->defaultValue('user')->end() // syslog 450 | ->scalarNode('max_files')->defaultValue(0)->end() // rotating 451 | ->scalarNode('action_level')->defaultValue('WARNING')->end() // fingers_crossed 452 | ->scalarNode('activation_strategy')->defaultNull()->end() // fingers_crossed 453 | ->booleanNode('stop_buffering')->defaultTrue()->end()// fingers_crossed 454 | ->scalarNode('passthru_level')->defaultNull()->end() // fingers_crossed 455 | ->arrayNode('excluded_http_codes') // fingers_crossed 456 | ->info('Only for "fingers_crossed" handler type') 457 | ->example([403, 404, [400 => ['^/foo', '^/bar']]]) 458 | ->canBeUnset() 459 | ->beforeNormalization() 460 | ->always(function ($values) { 461 | if (false === $values) { 462 | return false; 463 | } 464 | 465 | return array_map(function ($value) { 466 | /* 467 | * Allows YAML: 468 | * excluded_http_codes: [403, 404, { 400: ['^/foo', '^/bar'] }] 469 | * 470 | * and XML: 471 | * 472 | * ^/foo 473 | * ^/bar 474 | * 475 | * 476 | */ 477 | 478 | if (\is_array($value)) { 479 | return isset($value['code']) ? $value : ['code' => key($value), 'urls' => current($value)]; 480 | } 481 | 482 | return ['code' => $value, 'urls' => []]; 483 | }, $values); 484 | }) 485 | ->end() 486 | ->prototype('array') 487 | ->children() 488 | ->scalarNode('code')->end() 489 | ->arrayNode('urls') 490 | ->prototype('scalar')->end() 491 | ->end() 492 | ->end() 493 | ->end() 494 | ->end() 495 | ->arrayNode('accepted_levels') // filter 496 | ->canBeUnset() 497 | ->prototype('scalar')->end() 498 | ->end() 499 | ->scalarNode('min_level')->defaultValue('DEBUG')->end() // filter 500 | ->scalarNode('max_level')->defaultValue('EMERGENCY')->end() // filter 501 | ->scalarNode('buffer_size')->defaultValue(0)->end() // fingers_crossed and buffer 502 | ->booleanNode('flush_on_overflow')->defaultFalse()->end() // buffer 503 | ->scalarNode('handler')->end() // fingers_crossed, buffer, filter, deduplication, sampling 504 | ->scalarNode('url')->end() // cube 505 | ->scalarNode('exchange')->end() // amqp 506 | ->scalarNode('exchange_name')->defaultValue('log')->end() // amqp 507 | ->scalarNode('channel')->defaultNull()->end() // slack & slackwebhook & telegram 508 | ->scalarNode('bot_name')->defaultValue('Monolog')->end() // slack & slackwebhook 509 | ->scalarNode('use_attachment')->defaultTrue()->end() // slack & slackwebhook 510 | ->scalarNode('use_short_attachment')->defaultFalse()->end() // slack & slackwebhook 511 | ->scalarNode('include_extra')->defaultFalse()->end() // slack & slackwebhook 512 | ->scalarNode('icon_emoji')->defaultNull()->end() // slack & slackwebhook 513 | ->scalarNode('webhook_url')->end() // slackwebhook 514 | ->arrayNode('exclude_fields') 515 | ->canBeUnset() 516 | ->prototype('scalar')->end() 517 | ->end() // slack & slackwebhook 518 | ->scalarNode('token')->end() // pushover & loggly & logentries & flowdock & rollbar & slack & insightops & telegram 519 | ->scalarNode('region')->end() // insightops 520 | ->scalarNode('source')->end() // flowdock 521 | ->booleanNode('use_ssl')->defaultTrue()->end() // logentries & insightops 522 | ->variableNode('user') // pushover 523 | ->validate() 524 | ->ifTrue(function ($v) { 525 | return !\is_string($v) && !\is_array($v); 526 | }) 527 | ->thenInvalid('User must be a string or an array.') 528 | ->end() 529 | ->end() 530 | ->scalarNode('title')->defaultNull()->end() // pushover 531 | ->scalarNode('host')->defaultNull()->end() // syslogudp 532 | ->scalarNode('port')->defaultValue(514)->end() // syslogudp 533 | ->arrayNode('config') 534 | ->canBeUnset() 535 | ->prototype('scalar')->end() 536 | ->end() // rollbar 537 | ->arrayNode('members') // group, whatfailuregroup, fallbackgroup 538 | ->canBeUnset() 539 | ->performNoDeepMerging() 540 | ->prototype('scalar')->end() 541 | ->end() 542 | ->scalarNode('connection_string')->end() // socket_handler 543 | ->scalarNode('timeout')->end() // socket_handler, logentries, pushover & slack 544 | ->scalarNode('time')->defaultValue(60)->end() // deduplication 545 | ->scalarNode('deduplication_level')->defaultValue(Level::Error->value)->end() // deduplication 546 | ->scalarNode('store')->defaultNull()->end() // deduplication 547 | ->scalarNode('connection_timeout')->end() // socket_handler, logentries, pushover & slack 548 | ->booleanNode('persistent')->end() // socket_handler 549 | ->scalarNode('message_type')->defaultValue(0)->end() // error_log 550 | ->scalarNode('parse_mode')->defaultNull()->end() // telegram 551 | ->booleanNode('disable_webpage_preview')->defaultNull()->end() // telegram 552 | ->booleanNode('disable_notification')->defaultNull()->end() // telegram 553 | ->booleanNode('split_long_messages')->defaultFalse()->end() // telegram 554 | ->booleanNode('delay_between_messages')->defaultFalse()->end() // telegram 555 | ->integerNode('topic')->defaultNull()->end() // telegram 556 | ->integerNode('factor')->defaultValue(1)->min(1)->end() // sampling 557 | ->arrayNode('tags') // loggly 558 | ->beforeNormalization() 559 | ->ifString() 560 | ->then(function ($v) { return explode(',', $v); }) 561 | ->end() 562 | ->beforeNormalization() 563 | ->ifArray() 564 | ->then(function ($v) { return array_filter(array_map('trim', $v)); }) 565 | ->end() 566 | ->prototype('scalar')->end() 567 | ->end() 568 | // console 569 | ->variableNode('console_formatter_options') 570 | ->defaultValue([]) 571 | ->validate() 572 | ->ifTrue(static function ($v) { return !\is_array($v); }) 573 | ->thenInvalid('The console_formatter_options must be an array.') 574 | ->end() 575 | ->end() 576 | ->scalarNode('formatter')->end() 577 | ->booleanNode('nested')->defaultFalse()->end() 578 | ->end(); 579 | 580 | $this->addGelfSection($handlerNode); 581 | $this->addMongoDBSection($handlerNode); 582 | $this->addElasticsearchSection($handlerNode); 583 | $this->addRedisSection($handlerNode); 584 | $this->addPredisSection($handlerNode); 585 | $this->addMailerSection($handlerNode); 586 | $this->addVerbosityLevelSection($handlerNode); 587 | $this->addChannelsSection($handlerNode); 588 | 589 | $handlerNode 590 | ->validate() 591 | ->ifTrue(function ($v) { return 'service' === $v['type'] && !empty($v['formatter']); }) 592 | ->thenInvalid('Service handlers can not have a formatter configured in the bundle, you must reconfigure the service itself instead') 593 | ->end() 594 | ->validate() 595 | ->ifTrue(function ($v) { return \in_array($v['type'], ['fingers_crossed', 'buffer', 'filter', 'deduplication', 'sampling'], true) && empty($v['handler']); }) 596 | ->thenInvalid('The handler has to be specified to use a FingersCrossedHandler, BufferHandler, FilterHandler, DeduplicationHandler or SamplingHandler') 597 | ->end() 598 | ->validate() 599 | ->ifTrue(function ($v) { return 'fingers_crossed' === $v['type'] && !empty($v['excluded_http_codes']) && !empty($v['activation_strategy']); }) 600 | ->thenInvalid('You can not use excluded_http_codes together with a custom activation_strategy in a FingersCrossedHandler') 601 | ->end() 602 | ->validate() 603 | ->ifTrue(function ($v) { return 'fingers_crossed' !== $v['type'] && !empty($v['excluded_http_codes']); }) 604 | ->thenInvalid('You can only use excluded_http_codes with a FingersCrossedHandler definition') 605 | ->end() 606 | ->validate() 607 | ->ifTrue(function ($v) { return 'filter' === $v['type'] && 'DEBUG' !== $v['min_level'] && !empty($v['accepted_levels']); }) 608 | ->thenInvalid('You can not use min_level together with accepted_levels in a FilterHandler') 609 | ->end() 610 | ->validate() 611 | ->ifTrue(function ($v) { return 'filter' === $v['type'] && 'EMERGENCY' !== $v['max_level'] && !empty($v['accepted_levels']); }) 612 | ->thenInvalid('You can not use max_level together with accepted_levels in a FilterHandler') 613 | ->end() 614 | ->validate() 615 | ->ifTrue(function ($v) { return 'rollbar' === $v['type'] && !empty($v['id']) && !empty($v['token']); }) 616 | ->thenInvalid('You can not use both an id and a token in a RollbarHandler') 617 | ->end() 618 | ->validate() 619 | ->ifTrue(function ($v) { return 'rollbar' === $v['type'] && empty($v['id']) && empty($v['token']); }) 620 | ->thenInvalid('The id or the token has to be specified to use a RollbarHandler') 621 | ->end() 622 | ->validate() 623 | ->ifTrue(function ($v) { return 'telegram' === $v['type'] && (empty($v['token']) || empty($v['channel'])); }) 624 | ->thenInvalid('The token and channel have to be specified to use a TelegramBotHandler') 625 | ->end() 626 | ->validate() 627 | ->ifTrue(function ($v) { return 'service' === $v['type'] && !isset($v['id']); }) 628 | ->thenInvalid('The id has to be specified to use a service as handler') 629 | ->end() 630 | ->validate() 631 | ->ifTrue(function ($v) { return 'syslogudp' === $v['type'] && !isset($v['host']); }) 632 | ->thenInvalid('The host has to be specified to use a syslogudp as handler') 633 | ->end() 634 | ->validate() 635 | ->ifTrue(function ($v) { return 'socket' === $v['type'] && !isset($v['connection_string']); }) 636 | ->thenInvalid('The connection_string has to be specified to use a SocketHandler') 637 | ->end() 638 | ->validate() 639 | ->ifTrue(function ($v) { return 'pushover' === $v['type'] && (empty($v['token']) || empty($v['user'])); }) 640 | ->thenInvalid('The token and user have to be specified to use a PushoverHandler') 641 | ->end() 642 | ->validate() 643 | ->ifTrue(function ($v) { return 'slack' === $v['type'] && (empty($v['token']) || empty($v['channel'])); }) 644 | ->thenInvalid('The token and channel have to be specified to use a SlackHandler') 645 | ->end() 646 | ->validate() 647 | ->ifTrue(function ($v) { return 'slackwebhook' === $v['type'] && (empty($v['webhook_url'])); }) 648 | ->thenInvalid('The webhook_url have to be specified to use a SlackWebhookHandler') 649 | ->end() 650 | ->validate() 651 | ->ifTrue(function ($v) { return 'cube' === $v['type'] && empty($v['url']); }) 652 | ->thenInvalid('The url has to be specified to use a CubeHandler') 653 | ->end() 654 | ->validate() 655 | ->ifTrue(function ($v) { return 'amqp' === $v['type'] && empty($v['exchange']); }) 656 | ->thenInvalid('The exchange has to be specified to use a AmqpHandler') 657 | ->end() 658 | ->validate() 659 | ->ifTrue(function ($v) { return 'loggly' === $v['type'] && empty($v['token']); }) 660 | ->thenInvalid('The token has to be specified to use a LogglyHandler') 661 | ->end() 662 | ->validate() 663 | ->ifTrue(function ($v) { return 'loggly' === $v['type'] && !empty($v['tags']); }) 664 | ->then(function ($v) { 665 | $invalidTags = preg_grep('/^[a-z0-9][a-z0-9\.\-_]*$/i', $v['tags'], \PREG_GREP_INVERT); 666 | if (!empty($invalidTags)) { 667 | throw new InvalidConfigurationException(\sprintf('The following Loggly tags are invalid: "%s".', implode('", "', $invalidTags))); 668 | } 669 | 670 | return $v; 671 | }) 672 | ->end() 673 | ->validate() 674 | ->ifTrue(function ($v) { return 'logentries' === $v['type'] && empty($v['token']); }) 675 | ->thenInvalid('The token has to be specified to use a LogEntriesHandler') 676 | ->end() 677 | ->validate() 678 | ->ifTrue(function ($v) { return 'insightops' === $v['type'] && empty($v['token']); }) 679 | ->thenInvalid('The token has to be specified to use a InsightOpsHandler') 680 | ->end() 681 | ->validate() 682 | ->ifTrue(function ($v) { return 'flowdock' === $v['type'] && empty($v['token']); }) 683 | ->thenInvalid('The token has to be specified to use a FlowdockHandler') 684 | ->end() 685 | ->validate() 686 | ->ifTrue(function ($v) { return 'flowdock' === $v['type'] && empty($v['from_email']); }) 687 | ->thenInvalid('The from_email has to be specified to use a FlowdockHandler') 688 | ->end() 689 | ->validate() 690 | ->ifTrue(function ($v) { return 'flowdock' === $v['type'] && empty($v['source']); }) 691 | ->thenInvalid('The source has to be specified to use a FlowdockHandler') 692 | ->end() 693 | ->validate() 694 | ->ifTrue(function ($v) { return 'server_log' === $v['type'] && empty($v['host']); }) 695 | ->thenInvalid('The host has to be specified to use a ServerLogHandler') 696 | ->end() 697 | ->validate() 698 | ->ifTrue(function ($v) { return $v['interactive_only'] && version_compare(InstalledVersions::getVersion('symfony/monolog-bridge'), '7.4.0', '<'); }) 699 | ->thenInvalid('The interactive_only flag requires symfony/monolog-bridge 7.4 or higher') 700 | ->end() 701 | ; 702 | 703 | return $treeBuilder; 704 | } 705 | 706 | private function addGelfSection(ArrayNodeDefinition $handlerNode): void 707 | { 708 | $handlerNode 709 | ->children() 710 | ->arrayNode('publisher') 711 | ->canBeUnset() 712 | ->beforeNormalization() 713 | ->ifString() 714 | ->then(function ($v) { return ['id' => $v]; }) 715 | ->end() 716 | ->children() 717 | ->scalarNode('id')->end() 718 | ->scalarNode('hostname')->end() 719 | ->scalarNode('port')->defaultValue(12201)->end() 720 | ->scalarNode('chunk_size')->defaultValue(1420)->end() 721 | ->enumNode('encoder')->values(['json', 'compressed_json'])->end() 722 | ->end() 723 | ->validate() 724 | ->ifTrue(function ($v) { 725 | return !isset($v['id']) && !isset($v['hostname']); 726 | }) 727 | ->thenInvalid('What must be set is either the hostname or the id.') 728 | ->end() 729 | ->end() 730 | ->end() 731 | ->validate() 732 | ->ifTrue(function ($v) { return 'gelf' === $v['type'] && !isset($v['publisher']); }) 733 | ->thenInvalid('The publisher has to be specified to use a GelfHandler') 734 | ->end() 735 | ; 736 | } 737 | 738 | private function addMongoDBSection(ArrayNodeDefinition $handlerNode) 739 | { 740 | $handlerNode 741 | ->children() 742 | ->arrayNode('mongodb') 743 | ->canBeUnset() 744 | ->beforeNormalization() 745 | ->ifString() 746 | ->then(function ($v) { return ['id' => $v]; }) 747 | ->end() 748 | ->children() 749 | ->scalarNode('id') 750 | ->info('ID of a MongoDB\Client service') 751 | ->example('doctrine_mongodb.odm.logs_connection') 752 | ->end() 753 | ->scalarNode('uri')->end() 754 | ->scalarNode('username')->end() 755 | ->scalarNode('password')->end() 756 | ->scalarNode('database')->defaultValue('monolog')->end() 757 | ->scalarNode('collection')->defaultValue('logs')->end() 758 | ->end() 759 | ->validate() 760 | ->ifTrue(function ($v) { 761 | return !isset($v['id']) && !isset($v['uri']); 762 | }) 763 | ->thenInvalid('The "mongodb" handler configuration requires either a service "id" or a connection "uri".') 764 | ->end() 765 | ->end() 766 | ->end() 767 | ->validate() 768 | ->ifTrue(function ($v) { return 'mongodb' === $v['type'] && !isset($v['mongodb']); }) 769 | ->thenInvalid('The "mongodb" configuration has to be specified to use a "mongodb" handler type.') 770 | ->end() 771 | ; 772 | } 773 | 774 | private function addElasticsearchSection(ArrayNodeDefinition $handlerNode): void 775 | { 776 | $handlerNode 777 | ->children() 778 | ->arrayNode('elasticsearch') 779 | ->canBeUnset() 780 | ->beforeNormalization() 781 | ->ifString() 782 | ->then(function ($v) { return ['id' => $v]; }) 783 | ->end() 784 | ->children() 785 | ->scalarNode('id')->end() 786 | ->arrayNode('hosts')->prototype('scalar')->end()->end() 787 | ->scalarNode('host')->end() 788 | ->scalarNode('port')->defaultValue(9200)->end() 789 | ->scalarNode('transport')->defaultValue('Http')->end() 790 | ->scalarNode('user')->defaultNull()->end() 791 | ->scalarNode('password')->defaultNull()->end() 792 | ->end() 793 | ->validate() 794 | ->ifTrue(function ($v) { 795 | return !isset($v['id']) && !isset($v['host']) && !isset($v['hosts']); 796 | }) 797 | ->thenInvalid('What must be set is either the host or the id.') 798 | ->end() 799 | ->end() 800 | ->scalarNode('index')->defaultValue('monolog')->end() // elastic_search & elastica 801 | ->scalarNode('document_type')->defaultValue('logs')->end() // elastic_search & elastica 802 | ->scalarNode('ignore_error')->defaultValue(false)->end() // elastic_search & elastica 803 | ->end() 804 | ; 805 | } 806 | 807 | private function addRedisSection(ArrayNodeDefinition $handlerNode): void 808 | { 809 | $handlerNode 810 | ->children() 811 | ->arrayNode('redis') 812 | ->canBeUnset() 813 | ->beforeNormalization() 814 | ->ifString() 815 | ->then(function ($v) { return ['id' => $v]; }) 816 | ->end() 817 | ->children() 818 | ->scalarNode('id')->end() 819 | ->scalarNode('host')->end() 820 | ->scalarNode('password')->defaultNull()->end() 821 | ->scalarNode('port')->defaultValue(6379)->end() 822 | ->scalarNode('database')->defaultValue(0)->end() 823 | ->scalarNode('key_name')->defaultValue('monolog_redis')->end() 824 | ->end() 825 | ->validate() 826 | ->ifTrue(function ($v) { 827 | return !isset($v['id']) && !isset($v['host']); 828 | }) 829 | ->thenInvalid('What must be set is either the host or the service id of the Redis client.') 830 | ->end() 831 | ->end() 832 | ->end() 833 | ->validate() 834 | ->ifTrue(function ($v) { return 'redis' === $v['type'] && empty($v['redis']); }) 835 | ->thenInvalid('The host has to be specified to use a RedisLogHandler') 836 | ->end() 837 | ; 838 | } 839 | 840 | private function addPredisSection(ArrayNodeDefinition $handlerNode): void 841 | { 842 | $handlerNode 843 | ->children() 844 | ->arrayNode('predis') 845 | ->canBeUnset() 846 | ->beforeNormalization() 847 | ->ifString() 848 | ->then(function ($v) { return ['id' => $v]; }) 849 | ->end() 850 | ->children() 851 | ->scalarNode('id')->end() 852 | ->scalarNode('host')->end() 853 | ->end() 854 | ->validate() 855 | ->ifTrue(function ($v) { 856 | return !isset($v['id']) && !isset($v['host']); 857 | }) 858 | ->thenInvalid('What must be set is either the host or the service id of the Predis client.') 859 | ->end() 860 | ->end() 861 | ->end() 862 | ->validate() 863 | ->ifTrue(function ($v) { return 'predis' === $v['type'] && empty($v['redis']); }) 864 | ->thenInvalid('The host has to be specified to use a RedisLogHandler') 865 | ->end() 866 | ; 867 | } 868 | 869 | private function addMailerSection(ArrayNodeDefinition $handlerNode): void 870 | { 871 | $handlerNode 872 | ->children() 873 | ->scalarNode('from_email')->end() // native_mailer, symfony_mailer and flowdock 874 | ->arrayNode('to_email') // native_mailer and symfony_mailer 875 | ->prototype('scalar')->end() 876 | ->beforeNormalization() 877 | ->ifString() 878 | ->then(function ($v) { return [$v]; }) 879 | ->end() 880 | ->end() 881 | ->scalarNode('subject')->end() // native_mailer and symfony_mailer 882 | ->scalarNode('content_type')->defaultNull()->end() // symfony_mailer 883 | ->arrayNode('headers') // native_mailer 884 | ->canBeUnset() 885 | ->scalarPrototype()->end() 886 | ->end() 887 | ->scalarNode('mailer')->defaultNull()->end() // symfony_mailer 888 | ->arrayNode('email_prototype') // symfony_mailer 889 | ->canBeUnset() 890 | ->beforeNormalization() 891 | ->ifString() 892 | ->then(function ($v) { return ['id' => $v]; }) 893 | ->end() 894 | ->children() 895 | ->scalarNode('id')->isRequired()->end() 896 | ->scalarNode('method')->defaultNull()->end() 897 | ->end() 898 | ->end() 899 | ->end() 900 | ->validate() 901 | ->ifTrue(function ($v) { return 'native_mailer' === $v['type'] && (empty($v['from_email']) || empty($v['to_email']) || empty($v['subject'])); }) 902 | ->thenInvalid('The sender, recipient and subject have to be specified to use a NativeMailerHandler') 903 | ->end() 904 | ->validate() 905 | ->ifTrue(function ($v) { return 'symfony_mailer' === $v['type'] && empty($v['email_prototype']) && (empty($v['from_email']) || empty($v['to_email']) || empty($v['subject'])); }) 906 | ->thenInvalid('The sender, recipient and subject or an email prototype have to be specified to use the Symfony MailerHandler') 907 | ->end() 908 | ; 909 | } 910 | 911 | private function addVerbosityLevelSection(ArrayNodeDefinition $handlerNode): void 912 | { 913 | $handlerNode 914 | ->children() 915 | ->arrayNode('verbosity_levels') // console 916 | ->beforeNormalization() 917 | ->ifArray() 918 | ->then(function ($v) { 919 | $map = []; 920 | $verbosities = ['VERBOSITY_QUIET', 'VERBOSITY_NORMAL', 'VERBOSITY_VERBOSE', 'VERBOSITY_VERY_VERBOSE', 'VERBOSITY_DEBUG']; 921 | // allow numeric indexed array with ascendning verbosity and lowercase names of the constants 922 | foreach ($v as $verbosity => $level) { 923 | if (\is_int($verbosity) && isset($verbosities[$verbosity])) { 924 | $map[$verbosities[$verbosity]] = strtoupper($level); 925 | } else { 926 | $map[strtoupper($verbosity)] = strtoupper($level); 927 | } 928 | } 929 | 930 | return $map; 931 | }) 932 | ->end() 933 | ->children() 934 | ->scalarNode('VERBOSITY_QUIET')->defaultValue('ERROR')->end() 935 | ->scalarNode('VERBOSITY_NORMAL')->defaultValue('WARNING')->end() 936 | ->scalarNode('VERBOSITY_VERBOSE')->defaultValue('NOTICE')->end() 937 | ->scalarNode('VERBOSITY_VERY_VERBOSE')->defaultValue('INFO')->end() 938 | ->scalarNode('VERBOSITY_DEBUG')->defaultValue('DEBUG')->end() 939 | ->end() 940 | ->validate() 941 | ->always(function ($v) { 942 | $map = []; 943 | foreach ($v as $verbosity => $level) { 944 | $verbosityConstant = \Symfony\Component\Console\Output\OutputInterface::class.'::'.$verbosity; 945 | 946 | if (!\defined($verbosityConstant)) { 947 | throw new InvalidConfigurationException(\sprintf('The configured verbosity "%s" is invalid as it is not defined in Symfony\Component\Console\Output\OutputInterface.', $verbosity)); 948 | } 949 | 950 | try { 951 | $level = Logger::toMonologLevel($level)->value; 952 | } catch (\Psr\Log\InvalidArgumentException $e) { 953 | 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)); 954 | } 955 | 956 | $map[\constant($verbosityConstant)] = $level; 957 | } 958 | 959 | return $map; 960 | }) 961 | ->end() 962 | ->end() 963 | ->end() 964 | ; 965 | } 966 | 967 | private function addChannelsSection(ArrayNodeDefinition $handlerNode): void 968 | { 969 | $handlerNode 970 | ->children() 971 | ->arrayNode('channels') 972 | ->fixXmlConfig('channel', 'elements') 973 | ->canBeUnset() 974 | ->beforeNormalization() 975 | ->ifString() 976 | ->then(function ($v) { return ['elements' => [$v]]; }) 977 | ->end() 978 | ->beforeNormalization() 979 | ->ifTrue(function ($v) { return \is_array($v) && is_numeric(key($v)); }) 980 | ->then(function ($v) { return ['elements' => $v]; }) 981 | ->end() 982 | ->validate() 983 | ->ifTrue(function ($v) { return empty($v); }) 984 | ->thenUnset() 985 | ->end() 986 | ->validate() 987 | ->always(function ($v) { 988 | $isExclusive = null; 989 | if (isset($v['type'])) { 990 | $isExclusive = 'exclusive' === $v['type']; 991 | } 992 | 993 | $elements = []; 994 | foreach ($v['elements'] as $element) { 995 | if (str_starts_with($element, '!')) { 996 | if (false === $isExclusive) { 997 | throw new InvalidConfigurationException('Cannot combine exclusive/inclusive definitions in channels list.'); 998 | } 999 | $elements[] = substr($element, 1); 1000 | $isExclusive = true; 1001 | } else { 1002 | if (true === $isExclusive) { 1003 | throw new InvalidConfigurationException('Cannot combine exclusive/inclusive definitions in channels list.'); 1004 | } 1005 | $elements[] = $element; 1006 | $isExclusive = false; 1007 | } 1008 | } 1009 | 1010 | if (!\count($elements)) { 1011 | return null; 1012 | } 1013 | 1014 | // de-duplicating $elements here in case the handlers are redefined, see https://github.com/symfony/monolog-bundle/issues/433 1015 | return ['type' => $isExclusive ? 'exclusive' : 'inclusive', 'elements' => array_unique($elements)]; 1016 | }) 1017 | ->end() 1018 | ->children() 1019 | ->scalarNode('type') 1020 | ->validate() 1021 | ->ifNotInArray(['inclusive', 'exclusive']) 1022 | ->thenInvalid('The type of channels has to be inclusive or exclusive') 1023 | ->end() 1024 | ->end() 1025 | ->arrayNode('elements') 1026 | ->prototype('scalar')->end() 1027 | ->end() 1028 | ->end() 1029 | ->end() 1030 | ->end() 1031 | ; 1032 | } 1033 | } 1034 | --------------------------------------------------------------------------------