diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 459ed51f..f3f9639b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,6 +14,7 @@ jobs: php: [ '8.1', '8.2', '8.3', '8.4' ] monolog: [ '2.*' ] symfony: [ false ] + extensions: [ '' ] include: - php: '8.1' deps: lowest @@ -26,6 +27,8 @@ jobs: - php: '8.4' deps: highest monolog: '3.*' + - php: '8.4' + extensions: mongodb env: SYMFONY_REQUIRE: ${{ matrix.symfony }} @@ -40,6 +43,7 @@ jobs: php-version: ${{ matrix.php }} ini-values: zend.exception_ignore_args=false tools: flex + extensions: ${{ matrix.extensions }} - name: Configure composer if: "${{ matrix.deps == 'highest' }}" @@ -49,6 +53,10 @@ jobs: if: "${{ matrix.monolog != '' }}" run: composer require --no-update monolog/monolog:${{ matrix.monolog }} + - name: Require mongodb/mongodb if ext-mongodb is available + if: "${{ contains(matrix.extensions, 'mongodb') }}" + run: composer require --no-update mongodb/mongodb + - name: Composer install uses: ramsey/composer-install@v3 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eebab41..8f54b078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * Add `hosts` configuration for `elastica` handler * Add `enabled` option to `handlers` configuration * Add `priority` field to `processor` tag +* Add `mongodb` handler and deprecate `mongo` ## 3.10.0 (2023-11-06) diff --git a/config/schema/monolog-1.0.xsd b/config/schema/monolog-1.0.xsd index 1e73118d..bc9c6e55 100644 --- a/config/schema/monolog-1.0.xsd +++ b/config/schema/monolog-1.0.xsd @@ -21,6 +21,7 @@ + @@ -163,6 +164,15 @@ + + + + + + + + + diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 2585f411..6fa53ed4 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -90,6 +90,17 @@ * - [level]: level name or int value, defaults to DEBUG * - [bubble]: bool, defaults to true * + * - mongodb: + * - mongodb: + * - id: optional if uri is given + * - uri: MongoDB connection string, optional if id is given + * - [username]: Username for database authentication + * - [password]: Password for database authentication + * - [database]: Database to which logs are written (not used for auth), defaults to "monolog" + * - [collection]: Collection to which logs are written, defaults to "logs" + * - [level]: level name or int value, defaults to DEBUG + * - [bubble]: bool, defaults to true + * * - elastic_search: * - elasticsearch: * - id: optional if host is given @@ -648,6 +659,7 @@ public function getConfigTreeBuilder(): TreeBuilder $this->addGelfSection($handlerNode); $this->addMongoSection($handlerNode); + $this->addMongoDBSection($handlerNode); $this->addElasticsearchSection($handlerNode); $this->addRedisSection($handlerNode); $this->addPredisSection($handlerNode); @@ -879,7 +891,7 @@ private function addMongoSection(ArrayNodeDefinition $handlerNode) ->ifTrue(function ($v) { return !isset($v['id']) && !isset($v['host']); }) - ->thenInvalid('What must be set is either the host or the id.') + ->thenInvalid('The "mongo" handler configuration requires either a service "id" or a connection "host".') ->end() ->validate() ->ifTrue(function ($v) { @@ -891,7 +903,43 @@ private function addMongoSection(ArrayNodeDefinition $handlerNode) ->end() ->validate() ->ifTrue(function ($v) { return 'mongo' === $v['type'] && !isset($v['mongo']); }) - ->thenInvalid('The mongo configuration has to be specified to use a MongoHandler') + ->thenInvalid('The "mongo" configuration has to be specified to use a "mongo" handler type.') + ->end() + ; + } + + private function addMongoDBSection(ArrayNodeDefinition $handlerNode) + { + $handlerNode + ->children() + ->arrayNode('mongodb') + ->canBeUnset() + ->beforeNormalization() + ->ifString() + ->then(function ($v) { return ['id' => $v]; }) + ->end() + ->children() + ->scalarNode('id') + ->info('ID of a MongoDB\Client service') + ->example('doctrine_mongodb.odm.logs_connection') + ->end() + ->scalarNode('uri')->end() + ->scalarNode('username')->end() + ->scalarNode('password')->end() + ->scalarNode('database')->defaultValue('monolog')->end() + ->scalarNode('collection')->defaultValue('logs')->end() + ->end() + ->validate() + ->ifTrue(function ($v) { + return !isset($v['id']) && !isset($v['uri']); + }) + ->thenInvalid('The "mongodb" handler configuration requires either a service "id" or a connection "uri".') + ->end() + ->end() + ->end() + ->validate() + ->ifTrue(function ($v) { return 'mongodb' === $v['type'] && !isset($v['mongodb']); }) + ->thenInvalid('The "mongodb" configuration has to be specified to use a "mongodb" handler type.') ->end() ; } diff --git a/src/DependencyInjection/MonologExtension.php b/src/DependencyInjection/MonologExtension.php index f6dfb574..235f3804 100644 --- a/src/DependencyInjection/MonologExtension.php +++ b/src/DependencyInjection/MonologExtension.php @@ -265,6 +265,12 @@ private function buildHandler(ContainerBuilder $container, $name, array $handler break; case 'mongo': + trigger_deprecation('symfony/monolog-bundle', '3.11', 'The "mongo" handler type is deprecated in MonologBundle since version 3.11.0, use the "mongodb" type instead.'); + + if (!class_exists('MongoDB\Client')) { + throw new \RuntimeException('The "mongo" handler requires the mongodb/mongodb package to be installed.'); + } + if (isset($handler['mongo']['id'])) { $client = new Reference($handler['mongo']['id']); } else { @@ -278,9 +284,8 @@ private function buildHandler(ContainerBuilder $container, $name, array $handler $client = new Definition('MongoDB\Client', [ $server, + ['appname' => 'monolog-bundle'], ]); - - $client->setPublic(false); } $definition->setArguments([ @@ -292,6 +297,44 @@ private function buildHandler(ContainerBuilder $container, $name, array $handler ]); break; + case 'mongodb': + if (!class_exists('MongoDB\Client')) { + throw new \RuntimeException('The "mongodb" handler requires the mongodb/mongodb package to be installed.'); + } + + if (isset($handler['mongodb']['id'])) { + $client = new Reference($handler['mongodb']['id']); + } else { + $uriOptions = ['appname' => 'monolog-bundle']; + + if (isset($handler['mongodb']['username'])) { + $uriOptions['username'] = $handler['mongodb']['username']; + } + + if (isset($handler['mongodb']['password'])) { + $uriOptions['password'] = $handler['mongodb']['password']; + } + + $client = new Definition('MongoDB\Client', [ + $handler['mongodb']['uri'], + $uriOptions, + ]); + } + + $definition->setArguments([ + $client, + $handler['mongodb']['database'], + $handler['mongodb']['collection'], + $handler['level'], + $handler['bubble'], + ]); + + if (empty($handler['formatter'])) { + $formatter = new Definition('Monolog\Formatter\MongoDBFormatter'); + $definition->addMethodCall('setFormatter', [$formatter]); + } + break; + case 'elasticsearch': trigger_deprecation('symfony/monolog-bundle', '3.8', 'The "elasticsearch" handler type is deprecated in MonologBundle since version 3.8.0, use the "elastica" type instead, or switch to the official Elastic client using the "elastic_search" type.'); // no break @@ -1021,6 +1064,7 @@ private function getHandlerClassByType($handlerType) 'fingers_crossed' => 'Monolog\Handler\FingersCrossedHandler', 'filter' => 'Monolog\Handler\FilterHandler', 'mongo' => 'Monolog\Handler\MongoDBHandler', + 'mongodb' => 'Monolog\Handler\MongoDBHandler', 'elasticsearch' => 'Monolog\Handler\ElasticSearchHandler', 'telegram' => 'Monolog\Handler\TelegramBotHandler', 'server_log' => 'Symfony\Bridge\Monolog\Handler\ServerLogHandler', diff --git a/tests/DependencyInjection/FixtureMonologExtensionTestCase.php b/tests/DependencyInjection/FixtureMonologExtensionTestCase.php index 3bb2264a..30529e4f 100644 --- a/tests/DependencyInjection/FixtureMonologExtensionTestCase.php +++ b/tests/DependencyInjection/FixtureMonologExtensionTestCase.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\MonologBundle\Tests\DependencyInjection; use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; +use Monolog\Handler\MongoDBHandler; use Monolog\Handler\NoopHandler; use Monolog\Handler\NullHandler; use Monolog\Processor\PsrLogMessageProcessor; @@ -332,6 +333,23 @@ public function testEnabledHandleOption() $this->assertFalse($container->hasDefinition('monolog.handler.disabled')); } + public function testMongoDB() + { + if (!class_exists('MongoDB\Client')) { + $this->markTestSkipped('mongodb/mongodb is not installed.'); + } + + $container = $this->getContainer('mongodb'); + + $this->assertTrue($container->hasDefinition('monolog.handler.mongodb')); + $handler = $container->getDefinition('monolog.handler.mongodb'); + $this->assertDICDefinitionClass($handler, MongoDBHandler::class); + $client = $handler->getArgument(0); + $this->assertDICDefinitionClass($client, 'MongoDB\Client'); + $this->assertDICConstructorArguments($client, ['mongodb://localhost:27018', ['appname' => 'monolog-bundle', 'username' => 'username', 'password' => 'password']]); + $this->assertDICConstructorArguments($handler, [$client, 'db', 'coll', 'DEBUG', true]); + } + protected function getContainer($fixture) { $container = new ContainerBuilder(); diff --git a/tests/DependencyInjection/Fixtures/xml/mongodb.xml b/tests/DependencyInjection/Fixtures/xml/mongodb.xml new file mode 100644 index 00000000..cce38b62 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/xml/mongodb.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/tests/DependencyInjection/Fixtures/yml/mongodb.yml b/tests/DependencyInjection/Fixtures/yml/mongodb.yml new file mode 100644 index 00000000..4311f8ee --- /dev/null +++ b/tests/DependencyInjection/Fixtures/yml/mongodb.yml @@ -0,0 +1,10 @@ +monolog: + handlers: + mongodb: + type: mongodb + mongodb: + uri: "mongodb://localhost:27018" + username: username + password: password + database: db + collection: coll diff --git a/tests/DependencyInjection/MonologExtensionTest.php b/tests/DependencyInjection/MonologExtensionTest.php index 4a5ad2c1..aec3ccfb 100644 --- a/tests/DependencyInjection/MonologExtensionTest.php +++ b/tests/DependencyInjection/MonologExtensionTest.php @@ -16,6 +16,7 @@ use Monolog\Handler\ElasticaHandler; use Monolog\Handler\ElasticsearchHandler; use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; +use Monolog\Handler\MongoDBHandler; use Monolog\Handler\RollbarHandler; use Monolog\Logger; use Monolog\Processor\UidProcessor; @@ -932,6 +933,163 @@ public function testElasticsearchAndElasticaHandlers() $this->assertSame(['hosts' => ['es:9200'], 'transport' => 'Http'], $elasticaClient->getArgument(0)); } + /** @group legacy */ + public function testMongo() + { + if (!class_exists('MongoDB\Client')) { + $this->markTestSkipped('mongodb/mongodb is not installed.'); + } + + $this->expectDeprecation('Since symfony/monolog-bundle 3.11: The "mongo" handler type is deprecated in MonologBundle since version 3.11.0, use the "mongodb" type instead.'); + + $container = new ContainerBuilder(); + $container->setDefinition('mongodb.client', new Definition('MongoDB\Client')); + + $config = [[ + 'handlers' => [ + 'mongo_with_id' => [ + 'type' => 'mongo', + 'mongo' => ['id' => 'mongodb.client'], + ], + 'mongo_with_string_id' => [ + 'type' => 'mongo', + 'mongo' => 'mongodb.client', + ], + 'mongo_with_host' => [ + 'type' => 'mongo', + 'mongo' => [ + 'host' => 'localhost', + 'port' => '27018', + 'user' => 'username', + 'pass' => 'password', + 'database' => 'db', + 'collection' => 'coll', + ], + ], + 'mongo_with_host_and_default_args' => [ + 'type' => 'mongo', + 'mongo' => [ + 'host' => 'localhost', + ], + ], + ], + ]]; + + $extension = new MonologExtension(); + $extension->load($config, $container); + + $this->assertTrue($container->hasDefinition('monolog.handler.mongo_with_id')); + $this->assertTrue($container->hasDefinition('monolog.handler.mongo_with_string_id')); + $this->assertTrue($container->hasDefinition('monolog.handler.mongo_with_host')); + $this->assertTrue($container->hasDefinition('monolog.handler.mongo_with_host_and_default_args')); + + // MongoDB handler should receive the mongodb.client as first argument + $handler = $container->getDefinition('monolog.handler.mongo_with_id'); + $this->assertDICDefinitionClass($handler, MongoDBHandler::class); + $this->assertDICConstructorArguments($handler, [new Reference('mongodb.client'), 'monolog', 'logs', 'DEBUG', true]); + + // MongoDB handler should receive the mongodb.client as first argument + $handler = $container->getDefinition('monolog.handler.mongo_with_string_id'); + $this->assertDICDefinitionClass($handler, MongoDBHandler::class); + $this->assertDICConstructorArguments($handler, [new Reference('mongodb.client'), 'monolog', 'logs', 'DEBUG', true]); + + // MongoDB handler with host and arguments + $handler = $container->getDefinition('monolog.handler.mongo_with_host'); + $this->assertDICDefinitionClass($handler, MongoDBHandler::class); + $client = $handler->getArgument(0); + $this->assertDICDefinitionClass($client, 'MongoDB\Client'); + $this->assertDICConstructorArguments($client, ['mongodb://username:password@localhost:27018', ['appname' => 'monolog-bundle']]); + $this->assertDICConstructorArguments($handler, [$client, 'db', 'coll', 'DEBUG', true]); + + // MongoDB handler with host and default arguments + $handler = $container->getDefinition('monolog.handler.mongo_with_host_and_default_args'); + $this->assertDICDefinitionClass($handler, MongoDBHandler::class); + $client = $handler->getArgument(0); + $this->assertDICDefinitionClass($client, 'MongoDB\Client'); + $this->assertDICConstructorArguments($client, ['mongodb://localhost:27017', ['appname' => 'monolog-bundle']]); + $this->assertDICConstructorArguments($handler, [$client, 'monolog', 'logs', 'DEBUG', true]); + } + + public function testMongoDB() + { + if (!class_exists('MongoDB\Client')) { + $this->markTestSkipped('mongodb/mongodb is not installed.'); + } + + $container = new ContainerBuilder(); + $container->setDefinition('mongodb.client', new Definition('MongoDB\Client')); + + $config = [[ + 'handlers' => [ + 'mongodb_with_id' => [ + 'type' => 'mongodb', + 'mongodb' => ['id' => 'mongodb.client'], + ], + 'mongodb_with_string_id' => [ + 'type' => 'mongodb', + 'mongodb' => 'mongodb.client', + ], + 'mongodb_with_uri' => [ + 'type' => 'mongodb', + 'mongodb' => [ + 'uri' => 'mongodb://localhost:27018', + 'username' => 'username', + 'password' => 'password', + 'database' => 'db', + 'collection' => 'coll', + ], + ], + 'mongodb_with_uri_and_default_args' => [ + 'type' => 'mongodb', + 'mongodb' => [ + 'uri' => 'mongodb://localhost:27018', + ], + ], + ], + ]]; + + $extension = new MonologExtension(); + $extension->load($config, $container); + + $this->assertTrue($container->hasDefinition('monolog.handler.mongodb_with_id')); + $this->assertTrue($container->hasDefinition('monolog.handler.mongodb_with_string_id')); + $this->assertTrue($container->hasDefinition('monolog.handler.mongodb_with_uri')); + $this->assertTrue($container->hasDefinition('monolog.handler.mongodb_with_uri_and_default_args')); + + // A MongoDBFormatter will be applied to each handler by default + $formatter = new Definition('Monolog\Formatter\MongoDBFormatter'); + + // MongoDB handler should receive the mongodb.client as first argument + $handler = $container->getDefinition('monolog.handler.mongodb_with_id'); + $this->assertDICDefinitionClass($handler, MongoDBHandler::class); + $this->assertDICConstructorArguments($handler, [new Reference('mongodb.client'), 'monolog', 'logs', 'DEBUG', true]); + $this->assertDICDefinitionMethodCallAt(1, $handler, 'setFormatter', [$formatter]); + + // MongoDB handler should receive the mongodb.client as first argument + $handler = $container->getDefinition('monolog.handler.mongodb_with_string_id'); + $this->assertDICDefinitionClass($handler, MongoDBHandler::class); + $this->assertDICConstructorArguments($handler, [new Reference('mongodb.client'), 'monolog', 'logs', 'DEBUG', true]); + $this->assertDICDefinitionMethodCallAt(1, $handler, 'setFormatter', [$formatter]); + + // MongoDB handler with arguments + $handler = $container->getDefinition('monolog.handler.mongodb_with_uri'); + $this->assertDICDefinitionClass($handler, MongoDBHandler::class); + $client = $handler->getArgument(0); + $this->assertDICDefinitionClass($client, 'MongoDB\Client'); + $this->assertDICConstructorArguments($client, ['mongodb://localhost:27018', ['appname' => 'monolog-bundle', 'username' => 'username', 'password' => 'password']]); + $this->assertDICConstructorArguments($handler, [$client, 'db', 'coll', 'DEBUG', true]); + $this->assertDICDefinitionMethodCallAt(1, $handler, 'setFormatter', [$formatter]); + + // MongoDB handler with host and default arguments + $handler = $container->getDefinition('monolog.handler.mongodb_with_uri_and_default_args'); + $this->assertDICDefinitionClass($handler, MongoDBHandler::class); + $client = $handler->getArgument(0); + $this->assertDICDefinitionClass($client, 'MongoDB\Client'); + $this->assertDICConstructorArguments($client, ['mongodb://localhost:27018', ['appname' => 'monolog-bundle']]); + $this->assertDICConstructorArguments($handler, [$client, 'monolog', 'logs', 'DEBUG', true]); + $this->assertDICDefinitionMethodCallAt(1, $handler, 'setFormatter', [$formatter]); + } + protected function getContainer(array $config = [], array $thirdPartyDefinitions = []): ContainerBuilder { $container = new ContainerBuilder(new EnvPlaceholderParameterBag());