From c4c59ce35598cc6c80f93a74cad2195c2ac4297d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ronald=20M=C3=A1rf=C3=B6ldi?= Date: Mon, 24 Apr 2023 15:41:41 +0200 Subject: [PATCH] refs AB#7245 Add error logs for console commands and add ability to send the X-Context-ID header on a response (#28) --- composer.json | 1 + .../AnzuSystemsCommonExtension.php | 18 +++++++- src/DependencyInjection/Configuration.php | 1 + .../Listener/ConsoleExceptionListener.php | 42 +++++++++++++++++++ .../Listener/ContextIdOnResponseListener.php | 22 ++++++++++ src/Log/Factory/LogContextFactory.php | 22 ++++++++++ src/Resources/config/services.php | 9 ++++ tests/Controller/ContentIdOnResponseTest.php | 26 ++++++++++++ .../Controller/HealthCheckControllerTest.php | 1 + .../config/packages/anzu_systems_common.yaml | 2 + 10 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 src/Event/Listener/ConsoleExceptionListener.php create mode 100644 src/Event/Listener/ContextIdOnResponseListener.php create mode 100644 tests/Controller/ContentIdOnResponseTest.php diff --git a/composer.json b/composer.json index f94567f..6d7cb3b 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "mongodb/mongodb": "^1.9", "monolog/monolog": "^2.7", "nelmio/api-doc-bundle": "^4.8", + "symfony/console": "^6.0", "symfony/dependency-injection": ">=5.3|^6.0", "symfony/dotenv": ">=5.3|^6.0", "symfony/expression-language": "^5.3|^6.0", diff --git a/src/DependencyInjection/AnzuSystemsCommonExtension.php b/src/DependencyInjection/AnzuSystemsCommonExtension.php index ded006c..c249a12 100644 --- a/src/DependencyInjection/AnzuSystemsCommonExtension.php +++ b/src/DependencyInjection/AnzuSystemsCommonExtension.php @@ -17,6 +17,8 @@ use AnzuSystems\CommonBundle\Domain\PermissionGroup\PermissionGroupFacade; use AnzuSystems\CommonBundle\Domain\PermissionGroup\PermissionGroupManager; use AnzuSystems\CommonBundle\Domain\User\CurrentAnzuUserProvider; +use AnzuSystems\CommonBundle\Event\Listener\ConsoleExceptionListener; +use AnzuSystems\CommonBundle\Event\Listener\ContextIdOnResponseListener; use AnzuSystems\CommonBundle\Event\Listener\ExceptionListener; use AnzuSystems\CommonBundle\Event\Subscriber\AuditLogSubscriber; use AnzuSystems\CommonBundle\Event\Subscriber\CommandLockSubscriber; @@ -55,7 +57,6 @@ use AnzuSystems\CommonBundle\Validator\Validator; use AnzuSystems\SerializerBundle\Metadata\MetadataRegistry; use AnzuSystems\SerializerBundle\Serializer; -use Doctrine\DBAL\Driver\Connection; use Doctrine\ORM\EntityManagerInterface; use Exception; use MongoDB; @@ -71,6 +72,7 @@ use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; +use Symfony\Component\HttpKernel\KernelEvents; final class AnzuSystemsCommonExtension extends Extension implements PrependExtensionInterface { @@ -228,6 +230,12 @@ private function loadSettings(ContainerBuilder $container): void ->getDefinition(CurrentAnzuUserProvider::class) ->replaceArgument('$userEntityClass', $settings['user_entity_class']); + if ($settings['send_context_id_with_response']) { + $container->register(ContextIdOnResponseListener::class) + ->addTag('kernel.event_listener', ['event' => KernelEvents::RESPONSE]) + ; + } + $definition = $this->createControllerDefinition(DebugController::class); $container->setDefinition(DebugController::class, $definition); @@ -319,7 +327,7 @@ private function loadHealthCheck(ContainerBuilder $container): void if ($hasModule(MysqlModule::class)) { $definition = new Definition(MysqlModule::class); - $definition->setArgument('$connection', new Reference(Connection::class)); + $definition->setArgument('$connection', new Reference('database_connection')); $definition->setArgument('$tableName', $healthCheck['mysql_table_name']); $definition->addTag(AnzuSystemsCommonBundle::TAG_HEALTH_CHECK_MODULE); $container->setDefinition(MysqlModule::class, $definition); @@ -392,6 +400,12 @@ private function loadLogs(LoaderInterface $loader, ContainerBuilder $container): ->replaceArgument('$ignoredExceptions', $logs['app']['ignored_exceptions']) ; + $container + ->getDefinition(ConsoleExceptionListener::class) + ->replaceArgument('$logContextFactory', new Reference(LogContextFactory::class)) + ->replaceArgument('$ignoredExceptions', $logs['app']['ignored_exceptions']) + ; + $appLogMongo = $logs['app']['mongo']; $appLogClientDefinition = new Definition(MongoDB\Client::class); $appLogClientDefinition->setArgument('$uri', $appLogMongo['uri']); diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 264545e..9894cd5 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -132,6 +132,7 @@ private function addSettingsSection(): NodeDefinition ->scalarNode('app_entity_namespace')->defaultValue('App\\Entity')->end() ->scalarNode('app_value_object_namespace')->defaultValue('App\\Model\\ValueObject')->end() ->scalarNode('app_enum_namespace')->defaultValue('App\\Model\\Enum')->end() + ->booleanNode('send_context_id_with_response')->defaultFalse()->end() ->arrayNode('unlocked_commands') ->defaultValue(self::DEFAULT_UNLOCKED_COMMANDS) ->validate() diff --git a/src/Event/Listener/ConsoleExceptionListener.php b/src/Event/Listener/ConsoleExceptionListener.php new file mode 100644 index 0000000..2fc723c --- /dev/null +++ b/src/Event/Listener/ConsoleExceptionListener.php @@ -0,0 +1,42 @@ +getError(); + if (in_array($exception::class, $this->ignoredExceptions, true)) { + return; + } + + $context = []; + if ($this->logContextFactory instanceof LogContextFactory) { + $context = $this->logContextFactory->buildFromConsoleErrorEventToArray($event); + } + + $this->appLogger->critical(sprintf( + '[Command] [%s] %s', + (string) $event->getCommand()?->getName(), + $exception->getMessage() + ), $context); + } +} diff --git a/src/Event/Listener/ContextIdOnResponseListener.php b/src/Event/Listener/ContextIdOnResponseListener.php new file mode 100644 index 0000000..8a73a65 --- /dev/null +++ b/src/Event/Listener/ContextIdOnResponseListener.php @@ -0,0 +1,22 @@ +getResponse(); + if ($response->headers->has(AnzuKernel::CONTEXT_IDENTITY_HEADER)) { + return; + } + + $response->headers->set(AnzuKernel::CONTEXT_IDENTITY_HEADER, AnzuApp::getContextId()); + } +} diff --git a/src/Log/Factory/LogContextFactory.php b/src/Log/Factory/LogContextFactory.php index 2d9f9e0..6747341 100644 --- a/src/Log/Factory/LogContextFactory.php +++ b/src/Log/Factory/LogContextFactory.php @@ -11,6 +11,7 @@ use AnzuSystems\SerializerBundle\Exception\SerializerException; use AnzuSystems\SerializerBundle\Serializer; use JsonException; +use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -85,6 +86,27 @@ public function buildFromRequestToArray(Request $request, Response $response = n ); } + /** + * @throws SerializerException + */ + public function buildFromConsoleErrorEventToArray(ConsoleErrorEvent $event): array + { + return $this->serializer->toArray( + $this->buildFromConsoleErrorEvent($event) + ); + } + + public function buildFromConsoleErrorEvent(ConsoleErrorEvent $event): LogContext + { + return $this->buildBaseContext() + ->setContent((string) $event->getError()) + ->setPath((string) $event->getCommand()?->getName()) + ->setParams(['args' => $event->getInput()->getArguments(), 'opts' => $event->getInput()->getOptions()]) + ->setUserId((int) $this->userProvider->getCurrentUser()->getId()) + ->setHttpStatus($event->getExitCode()) + ; + } + public function buildCustomFromRequest(Request $request, LogDto $logDto): LogContext { return $this->buildBaseContext() diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index 8220917..37fd943 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -12,6 +12,7 @@ use AnzuSystems\CommonBundle\Domain\Job\JobManager; use AnzuSystems\CommonBundle\Domain\Job\JobProcessor; use AnzuSystems\CommonBundle\Domain\User\CurrentAnzuUserProvider; +use AnzuSystems\CommonBundle\Event\Listener\ConsoleExceptionListener; use AnzuSystems\CommonBundle\Event\Listener\ExceptionListener; use AnzuSystems\CommonBundle\Event\Listener\LockReleaseListener; use AnzuSystems\CommonBundle\Event\Subscriber\CommandLockSubscriber; @@ -25,6 +26,7 @@ use AnzuSystems\CommonBundle\Validator\Validator; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; +use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -115,6 +117,13 @@ ->tag('kernel.event_listener', ['event' => KernelEvents::EXCEPTION]) ; + $services->set(ConsoleExceptionListener::class) + ->arg('$ignoredExceptions', null) + ->arg('$appLogger', service('monolog.logger')) + ->arg('$logContextFactory', null) + ->tag('kernel.event_listener', ['event' => ConsoleEvents::ERROR]) + ; + $services->set(Validator::class) ->arg('$validator', service(ValidatorInterface::class)) ; diff --git a/tests/Controller/ContentIdOnResponseTest.php b/tests/Controller/ContentIdOnResponseTest.php new file mode 100644 index 0000000..011517e --- /dev/null +++ b/tests/Controller/ContentIdOnResponseTest.php @@ -0,0 +1,26 @@ +get(uri: '/something'); + + $this->assertTrue(self::$client->getResponse()->headers->has(AnzuKernel::CONTEXT_IDENTITY_HEADER)); + $this->assertSame( + expected: AnzuApp::getContextId(), + actual: self::$client->getResponse()->headers->get(AnzuKernel::CONTEXT_IDENTITY_HEADER), + ); + } +} diff --git a/tests/Controller/HealthCheckControllerTest.php b/tests/Controller/HealthCheckControllerTest.php index bfc269e..4015601 100644 --- a/tests/Controller/HealthCheckControllerTest.php +++ b/tests/Controller/HealthCheckControllerTest.php @@ -28,5 +28,6 @@ public function testHealthCheck(): void self::assertContains('redis', $moduleResultsKeys); self::assertContains('dataMount', $moduleResultsKeys); self::assertContains('mongo', $moduleResultsKeys); + self::assertContains('mysql', $moduleResultsKeys); } } diff --git a/tests/config/packages/anzu_systems_common.yaml b/tests/config/packages/anzu_systems_common.yaml index 67c7070..d5e4b3d 100644 --- a/tests/config/packages/anzu_systems_common.yaml +++ b/tests/config/packages/anzu_systems_common.yaml @@ -5,6 +5,7 @@ anzu_systems_common: user_entity_class: AnzuSystems\CommonBundle\Tests\Data\Entity\User app_entity_namespace: AnzuSystems\CommonBundle\Tests\Data\Entity app_value_object_namespace: AnzuSystems\CommonBundle\Tests\Data\Model\ValueObject + send_context_id_with_response: true unlocked_commands: - Symfony\Bundle\FrameworkBundle\Command\AssetsInstallCommand - Symfony\Bundle\FrameworkBundle\Command\CacheWarmupCommand @@ -19,6 +20,7 @@ anzu_systems_common: - AnzuSystems\CommonBundle\HealthCheck\Module\RedisModule - AnzuSystems\CommonBundle\HealthCheck\Module\DataMountModule - AnzuSystems\CommonBundle\HealthCheck\Module\MongoModule + - AnzuSystems\CommonBundle\HealthCheck\Module\MysqlModule errors: enabled: true default_exception_handler: AnzuSystems\CommonBundle\Exception\Handler\DefaultExceptionHandler