From 051b98125a87fee0d0a2d80b09d6fb300e8eea72 Mon Sep 17 00:00:00 2001 From: Luke Rodgers Date: Thu, 27 Jan 2022 11:07:56 +0000 Subject: [PATCH] Define third party loggers (#2) * Define third party loggers bundled in the core * Run additional test --- .travis.yml | 6 +- README.md | 40 +++- src/Console/CommandList.php | 57 ++++++ src/Console/ListCustomLoggersCommand.php | 193 ++++++++++++++++++ .../ListCustomLoggersCommandTest.php | 78 +++++++ src/etc/di.xml | 80 +++++++- src/etc/module.xml | 12 ++ src/registration.php | 6 + 8 files changed, 461 insertions(+), 11 deletions(-) create mode 100644 src/Console/CommandList.php create mode 100644 src/Console/ListCustomLoggersCommand.php create mode 100644 src/Test/Integration/ListCustomLoggersCommandTest.php diff --git a/.travis.yml b/.travis.yml index d62167f..6cf3598 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,11 @@ script: - mysql -uroot -e 'SET @@global.sql_mode = NO_ENGINE_SUBSTITUTION; DROP DATABASE IF EXISTS magento_integration_tests; CREATE DATABASE magento_integration_tests;' - cp dev/tests/integration/etc/install-config-mysql.travis-no-rabbitmq.php.dist dev/tests/integration/etc/install-config-mysql.php - php $TRAVIS_BUILD_DIR/dev/prepare_phpunit_config.php $TRAVIS_BUILD_DIR/vendor/ampersand/travis-vanilla-magento/instances/ampmodule - - vendor/bin/phpunit -c $(pwd)/dev/tests/integration/phpunit.xml.dist --testsuite Integration --debug + - composer install -o + - IS_CI_PIPELINE=1 vendor/bin/phpunit -c $(pwd)/dev/tests/integration/phpunit.xml.dist --testsuite Integration --debug + # Output the custom loggers command filtering out modules we've accounted for + - if [[ $TEST_GROUP = magento_23 ]]; then php bin/magento ampersand:log-correlation-id:list-custom-loggers --filter "Klarna\Core\Logger\Logger" --filter "Dotdigitalgroup\Email\Logger\Logger" --filter "Amazon\Core\Logger\Logger" --filter "Amazon\Core\Logger\IpnLogger" ; fi + - if [[ $TEST_GROUP = magento_latest ]]; then php bin/magento ampersand:log-correlation-id:list-custom-loggers --filter "Yotpo\Yotpo\Model\Logger" --filter "Klarna\Core\Logger\Logger" --filter "Dotdigitalgroup\Email\Logger\Logger" --filter "Amazon\Core\Logger\Logger" --filter "Amazon\Core\Logger\IpnLogger"; fi after_failure: - test -d ./vendor/ampersand/travis-vanilla-magento/instances/ampmodule/var/report/ && for r in ./vendor/ampersand/travis-vanilla-magento/instances/ampmodule/var/report/*; do cat $r; done - test -f ./vendor/ampersand/travis-vanilla-magento/instances/ampmodule/var/log/system.log && grep -v "Broken reference" ./vendor/ampersand/travis-vanilla-magento/instances/ampmodule/var/log/system.log diff --git a/README.md b/README.md index 780f935..9d5235c 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,9 @@ $ grep -ri 61e04d741bf78 ./var/log If the request was long-running, or had an error it may also be flagged in new relic with the custom parameter `amp_correlation_id` -# Configuration and Customisation +## Configuration and Customisation -## Change the key name from `amp_correlation_id` +### Change the key name from `amp_correlation_id` You can change the monolog/new relic key from `amp_correlation_id` using `app/etc/ampersand_magento2_log_correlation/di.xml` @@ -92,7 +92,7 @@ You can change the monolog/new relic key from `amp_correlation_id` using `app/et ``` -## Use existing correlation id from request header +### Use existing correlation id from request header If you want to use an upstream correlation/trace ID you can define one `app/etc/ampersand_magento2_log_correlation/di.xml` @@ -116,3 +116,37 @@ $ 2>&1 curl -H 'X-Your-Header-Here: abc123' https://your-magento-site.example.c $ 2>&1 curl https://your-magento-site.example.com/ -vvv | grep "X-Log-Correlation-Id" < X-Log-Correlation-Id: cid-61e4194d1bea5 ``` + +### Custom Loggers + +By default this module hooks into all vanilla magento loggers. + +However third party modules may define additional loggers to write to custom files. If you want the correlation ID added to those logs as well you will need to create a module that depends on both `Ampersand_LogCorrelation_Id` and the module with the custom logger, you will then have to add the log handler in `di.xml` like so + +```xml + + + + + Ampersand\LogCorrelationId\Processor\MonologCorrelationId + addCorrelationId + + + + +``` + +This module provides a command to try to help you keep track of the custom loggers in your system + +```shell +$ php bin/magento ampersand:log-correlation-id:list-custom-loggers +Scanning for classes which extend Monolog\Logger +- You need to run 'composer dump-autoload --optimize' for this command to work +- Use this on your local environment to configure your di.xml +- See vendor/ampersand/magento2-log-correlation-id/README.md +-------------------------------------------------------------------------------- +Dotdigitalgroup\Email\Logger\Logger +StripeIntegration\Payments\Logger\WebhooksLogger +Yotpo\Yotpo\Model\Logger +-------------------------------------------------------------------------------- +DONE``` diff --git a/src/Console/CommandList.php b/src/Console/CommandList.php new file mode 100644 index 0000000..aa6c4c1 --- /dev/null +++ b/src/Console/CommandList.php @@ -0,0 +1,57 @@ +objectManager = $objectManager; + } + + /** + * Gets list of command classes + * + * @return string[] + */ + private function getCommandsClasses(): array + { + return [ + ListCustomLoggersCommand::class + ]; + } + + /** + * @inheritdoc + */ + public function getCommands(): array + { + $commands = []; + foreach ($this->getCommandsClasses() as $class) { + if (class_exists($class)) { + $commands[] = $this->objectManager->get($class); + } else { + throw new \RuntimeException('Class ' . $class . ' does not exist'); + } + } + + return $commands; + } +} diff --git a/src/Console/ListCustomLoggersCommand.php b/src/Console/ListCustomLoggersCommand.php new file mode 100644 index 0000000..9b3b2fe --- /dev/null +++ b/src/Console/ListCustomLoggersCommand.php @@ -0,0 +1,193 @@ +dir = $dir; + $this->list = $list; + parent::__construct(); + } + + /** + * Configure the command + * + * @return void + */ + protected function configure() + { + $options = [ + new InputOption( + self::INPUT_KEY_FILTER, + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Filter a logger from the output results' + ), + ]; + + $this->setName('ampersand:log-correlation-id:list-custom-loggers'); + $this->setDescription('List custom monolog loggers defined in magento modules'); + $this->setDefinition($options); + + parent::configure(); + } + + /** + * List all module classes that extend the monolog logger + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $returnCode = Cli::RETURN_SUCCESS; + try { + $output->writeln(str_pad('', 80, '-')); + $output->writeln("Scanning for classes which extend " . MonologLogger::class); + $output->writeln(str_pad('', 80, '-')); + $output->writeln("- You need to run 'composer dump-autoload --optimize' for this command to work"); + $output->writeln("- Use this on your local environment to configure your di.xml"); + $output->writeln("- See vendor/ampersand/magento2-log-correlation-id/README.md"); + $output->writeln(str_pad('', 80, '-')); + + $extendsMonologLogger = []; + + $classMap = $this->getOptimisedAutoloadClassMap(); + + foreach ($this->list->getNames() as $moduleName) { + $moduleDirectory = $this->dir->getDir($moduleName) . DIRECTORY_SEPARATOR; + + // Get all the files from the classmap pertaining to this module + $filesFromClassMap = array_filter( + $classMap, + function ($filepath) use ($moduleDirectory) { + return substr($filepath, 0, strlen($moduleDirectory)) === $moduleDirectory; + } + ); + + // From this list of files get the list of them that extend the Monolog Logger class + $moduleClassesThatExtendMonologLogger = array_filter( + array_keys($filesFromClassMap), + function ($class) use ($output) { + try { + $result = is_subclass_of($class, MonologLogger::class); + } catch (\Throwable $throwable) { + $output->writeln("" . $throwable->getMessage() . ""); + $result = false; + } + return $result; + } + ); + + if (empty($moduleClassesThatExtendMonologLogger)) { + continue; + } + // phpcs:ignore Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge + $extendsMonologLogger = array_merge($extendsMonologLogger, $moduleClassesThatExtendMonologLogger); + } + + $filters = $input->getOption(self::INPUT_KEY_FILTER); + $extendsMonologLogger = array_diff($extendsMonologLogger, $input->getOption(self::INPUT_KEY_FILTER)); + + if (!empty($filters) && !empty($extendsMonologLogger)) { + /* + * We have supplied filters but still have results output, so we have not filtered everything + * Likely a new logger has been added that needs accounted for in di.xml + */ + $returnCode = Cli::RETURN_FAILURE; + } + + sort($extendsMonologLogger); + foreach ($extendsMonologLogger as $className) { + $output->writeln($className); + } + + $output->writeln(str_pad('', 80, '-')); + $output->writeln('DONE'); + } catch (\Throwable $throwable) { + $output->writeln("" . $throwable->getMessage() . ""); + return Cli::RETURN_FAILURE; + } + return $returnCode; + } + + /** + * This is the same autoloading mechanism as in vendor/magento/magento2-base/app/autoload.php + * + * However we cannot use the wrapper as we need direct access ot the composer autoloader data + * + * @phpcs:disable Magento2.Security.IncludeFile.FoundIncludeFile + * @phpcs:disable Magento2.Functions.DiscouragedFunction.DiscouragedWithAlternative + * @phpcs:disable Magento2.Exceptions.DirectThrow.FoundDirectThrow + * + * @return array + * @throws \Exception + */ + protected function getOptimisedAutoloadClassMap(): array + { + // Get the vendor path + // @phpstan-ignore-next-line + $vendorDir = include VENDOR_PATH; + // @phpstan-ignore-next-line + $vendorAutoload = BP . "/{$vendorDir}/autoload.php"; + if (!is_readable($vendorAutoload)) { + throw new \Exception("Could not find vendor/autoload.php at " . $vendorAutoload); + } + + // Get the optimised autoload classmap + $classMap = (include $vendorAutoload)->getClassMap(); + + // Filter out files without a concrete class + foreach (array_filter($classMap) as $className => $filePath) { + try { + if (strpos($filePath, '/TestFramework/') !== false) { + unset($classMap[$className]); // Filter out any test framework results + continue; + } + if (strpos($className, '\\TestFramework\\') !== false) { + unset($classMap[$className]); // Filter out any test framework results + continue; + } + $realPath = realpath($filePath); + if (!$realPath) { + unset($classMap[$className]); // Could not work out realpath + continue; + } + } catch (\Throwable $throwable) { + unset($classMap[$className]); // Could not work out realpath + continue; + } + $classMap[$className] = $realPath; + } + + return array_filter($classMap); + } +} diff --git a/src/Test/Integration/ListCustomLoggersCommandTest.php b/src/Test/Integration/ListCustomLoggersCommandTest.php new file mode 100644 index 0000000..fb3c80b --- /dev/null +++ b/src/Test/Integration/ListCustomLoggersCommandTest.php @@ -0,0 +1,78 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + public function testListCustomLoggersCommandInCi() + { + if (!getenv('IS_CI_PIPELINE')) { + $this->markTestSkipped('Skipping this test as it is not in the modules CI pipeline'); + } + + $tester = new CommandTester($this->objectManager->create(ListCustomLoggersCommand::class)); + $tester->execute([]); + + /* + * Our modules CI pipeline ensures we have composer dump-autoload -o so we should see the bundled loggers + */ + $this->assertStringContainsString( + 'Amazon\Core\Logger\Logger', + $tester->getDisplay() + ); + $this->assertStringContainsString( + 'Dotdigitalgroup\Email\Logger\Logger', + $tester->getDisplay() + ); + $this->assertStringContainsString( + 'Klarna\Core\Logger\Logger', + $tester->getDisplay() + ); + } + + public function testListCustomLoggersCommand() + { + $command = $this->getMockBuilder(ListCustomLoggersCommand::class) + ->setConstructorArgs( + [ + $this->objectManager->get(ModuleDir::class), + $this->objectManager->get(ModuleList::class) + ] + ) + ->setMethods(['getOptimisedAutoloadClassMap']) + ->getMock(); + + // Spoof in the framework logger which is guaranteed to be here, pretend its in the catalog module + $classMap = [ + 'Magento\Framework\Logger\Monolog' => BP . '/vendor/magento/module-catalog/', + ]; + + $command->expects($this->any()) + ->method('getOptimisedAutoloadClassMap') + ->willReturn($classMap); + + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertStringContainsString( + 'Magento\Framework\Logger\Monolog', + $tester->getDisplay() + ); + } +} diff --git a/src/etc/di.xml b/src/etc/di.xml index 2cce411..cdc30fc 100644 --- a/src/etc/di.xml +++ b/src/etc/di.xml @@ -1,14 +1,11 @@ - - + + - - - Ampersand\LogCorrelationId\Processor\MonologCorrelationId - addCorrelationId - + + Ampersand\LogCorrelationId\Console\ListCustomLoggersCommand @@ -26,4 +23,73 @@ + + + + + + + Ampersand\LogCorrelationId\Processor\MonologCorrelationId + addCorrelationId + + + + + + + + + + + Ampersand\LogCorrelationId\Processor\MonologCorrelationId + addCorrelationId + + + + + + + + + + Ampersand\LogCorrelationId\Processor\MonologCorrelationId + addCorrelationId + + + + + + + + + + Ampersand\LogCorrelationId\Processor\MonologCorrelationId + addCorrelationId + + + + + + + + + + Ampersand\LogCorrelationId\Processor\MonologCorrelationId + addCorrelationId + + + + + + + + + + Ampersand\LogCorrelationId\Processor\MonologCorrelationId + addCorrelationId + + + + + diff --git a/src/etc/module.xml b/src/etc/module.xml index 7047ad4..a0e4284 100755 --- a/src/etc/module.xml +++ b/src/etc/module.xml @@ -4,6 +4,18 @@ + + + + + diff --git a/src/registration.php b/src/registration.php index ce44905..b71e4f2 100755 --- a/src/registration.php +++ b/src/registration.php @@ -3,3 +3,9 @@ use Magento\Framework\Component\ComponentRegistrar; ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Ampersand_LogCorrelationId', __DIR__); + +if (PHP_SAPI === 'cli') { + \Magento\Framework\Console\CommandLocator::register( + \Ampersand\LogCorrelationId\Console\CommandList::class + ); +}