diff --git a/README.md b/README.md index e8670ea..d49ad8b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ Magento 2 specific tasks for GrumPHP< The easiest way to install this package is through composer: ```bash -composer config repositories.grumphp-magento2 vcs https://github.com/roma-glushko/grumphp-magento2 composer require --dev roma-glushko/grumphp-magento2 ``` @@ -20,7 +19,7 @@ parameters: ## Usage -### MagentoModuleRegistration +### 🛠 MagentoModuleRegistration It's a common practice to commit config.php file in Magento 2. Especially, the file is useful for managing modules. The common issue is when during development people forget to register newly added modules to the config.php which can lead to outcomes that hard to troubleshoot. This task helps to watch for such cases and let to know when registration is missing. @@ -79,3 +78,39 @@ Key of the array is a package name. The value is a list of module names that the *Default: `./app/code/*/*/registration.php`* A glob() pattern that helps to find custom non-composer magento modules. + +### 🛠 MagentoLogNotification + +It's useful to be notified when you have recently added records in Magento logs. This tasks checks log files located +under `log_patterns` and informs if there are logs that have been added inside of time frame defined in `record_stale_threshold`. +The `exclude_severities` helps to reduce noisy records. + +```yaml +parameters: + tasks: + magento2-log-notification: + log_patterns: + - "./var/*/*.log" + record_stale_threshold: 1 # in days + exclude_severities: + - "INFO" + - "DEBUG" +``` + +**log_patterns** + +*Default: `./var/*/*.log`* + +Paths where log files should be watched + +**record_stale_threshold** + +*Default: `1`* + +Stale threshold (in days) that helps to ignore old records in logs. + +**exclude_severities** + +*Default: `INFO, DEBUG`* + +This config excludes records with specified severity levels. \ No newline at end of file diff --git a/composer.json b/composer.json index 4f8027e..50e3cf2 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "require": { "php": ">=7.0", "phpro/grumphp": "~0.15", - "composer/composer": "^1.6" + "composer/composer": "^1.6", + "roma-glushko/monolog-parser": "^1.0" }, "minimum-stability": "stable" } diff --git a/composer.lock b/composer.lock index b3b6660..2e269c2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a188f89af300ecd34b33a33bf91af2a5", + "content-hash": "dd436fbfa47c727833bbc9bb4a89a1d3", "packages": [ { "name": "composer/ca-bundle", @@ -502,21 +502,21 @@ }, { "name": "monolog/monolog", - "version": "1.25.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf" + "reference": "c861fcba2ca29404dc9e617eedd9eff4616986b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/70e65a5470a42cfec1a7da00d30edb6e617e8dcf", - "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c861fcba2ca29404dc9e617eedd9eff4616986b8", + "reference": "c861fcba2ca29404dc9e617eedd9eff4616986b8", "shasum": "" }, "require": { - "php": ">=5.3.0", - "psr/log": "~1.0" + "php": "^7.2", + "psr/log": "^1.0.1" }, "provide": { "psr/log-implementation": "1.0.0" @@ -524,33 +524,36 @@ "require-dev": { "aws/aws-sdk-php": "^2.4.9 || ^3.0", "doctrine/couchdb": "~1.0@dev", - "graylog2/gelf-php": "~1.0", - "jakub-onderka/php-parallel-lint": "0.9", + "elasticsearch/elasticsearch": "^6.0", + "graylog2/gelf-php": "^1.4.2", + "jakub-onderka/php-parallel-lint": "^0.9", "php-amqplib/php-amqplib": "~2.4", "php-console/php-console": "^3.1.3", - "phpunit/phpunit": "~4.5", - "phpunit/phpunit-mock-objects": "2.3.0", + "phpspec/prophecy": "^1.6.1", + "phpunit/phpunit": "^8.3", + "predis/predis": "^1.1", + "rollbar/rollbar": "^1.3", "ruflin/elastica": ">=0.90 <3.0", - "sentry/sentry": "^0.13", "swiftmailer/swiftmailer": "^5.3|^6.0" }, "suggest": { "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-mongo": "Allow sending log messages to a MongoDB server", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", "php-console/php-console": "Allow sending log messages to Google Chrome", "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server", - "sentry/sentry": "Allow sending log messages to a Sentry server" + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -576,7 +579,7 @@ "logging", "psr-3" ], - "time": "2019-09-06T13:49:17+00:00" + "time": "2019-12-20T14:22:59+00:00" }, { "name": "phpro/grumphp", @@ -734,16 +737,16 @@ }, { "name": "psr/log", - "version": "1.1.0", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", - "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", "shasum": "" }, "require": { @@ -752,7 +755,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -777,7 +780,46 @@ "psr", "psr-3" ], - "time": "2018-11-20T15:27:04+00:00" + "time": "2020-03-23T09:12:05+00:00" + }, + { + "name": "roma-glushko/monolog-parser", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/roma-glushko/monolog-parser.git", + "reference": "7dd600173865f49faeb37ba07b00fc51d5c342b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roma-glushko/monolog-parser/zipball/7dd600173865f49faeb37ba07b00fc51d5c342b4", + "reference": "7dd600173865f49faeb37ba07b00fc51d5c342b4", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ], + "psr-0": { + "MonologParser": "src/MonologParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Glushko", + "email": "roman.glushko.m@gmail.com" + } + ], + "description": "A parser for monolog log records", + "time": "2020-03-29T09:55:27+00:00" }, { "name": "seld/jsonlint", diff --git a/src/Extension/Loader.php b/src/Extension/Loader.php index 5b71ae6..e5c3b72 100644 --- a/src/Extension/Loader.php +++ b/src/Extension/Loader.php @@ -3,6 +3,7 @@ namespace Glushko\GrumphpMagento2\Extension; use Glushko\GrumphpMagento2\Task\MagentoModuleRegistrationTask; +use Glushko\GrumphpMagento2\Task\MagentoLogNotificationTask; use GrumPHP\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -24,5 +25,11 @@ public function load(ContainerBuilder $container) ->addArgument(new Reference('process_builder')) ->addArgument(new Reference('formatter.raw_process')) ->addTag('grumphp.task', ['config' => 'magento2-module-registration']); + + $container->register('magento2.log-watcher', MagentoLogNotificationTask::class) + ->addArgument(new Reference('config')) + ->addArgument(new Reference('process_builder')) + ->addArgument(new Reference('formatter.raw_process')) + ->addTag('grumphp.task', ['config' => 'magento2-log-notification']); } } diff --git a/src/Task/MagentoLogNotificationTask.php b/src/Task/MagentoLogNotificationTask.php new file mode 100644 index 0000000..726c4b6 --- /dev/null +++ b/src/Task/MagentoLogNotificationTask.php @@ -0,0 +1,131 @@ +setDefaults([ + 'log_patterns' => ['./var/*/*.log'], + 'record_stale_threshold' => '1', + 'exclude_severities' => ['INFO', 'DEBUG'], + ]); + + $resolver->addAllowedTypes('log_patterns', ['array']); + $resolver->addAllowedTypes('exclude_severities', ['array']); + $resolver->addAllowedTypes('record_stale_threshold', ['integer']); + + return $resolver; + } + + /** + * {@inheritdoc} + */ + public function canRunInContext(ContextInterface $context): bool + { + return ($context instanceof GitPreCommitContext || $context instanceof RunContext); + } + + /** + * Notify about recently added records in Magento logs + * + * @param ContextInterface $context + * + * @return TaskResultInterface + * + * @throws Exception + */ + public function run(ContextInterface $context): TaskResultInterface + { + $config = $this->getConfiguration(); + $logPatterns = $config['log_patterns']; + $excludedSeverities = $config['exclude_severities']; + $recordStaleThreshold = $config['record_stale_threshold']; + + $logFiles = []; + + foreach ($logPatterns as $logPattern) { + $logFiles[] = glob($logPattern, GLOB_NOSORT); + } + + $logFiles = array_merge(...$logFiles); + + $dateNow = new DateTime(); + $logReport = []; + + foreach ($logFiles as $logPath) { + $logReader = new LogReader($logPath); + $logCount = count($logReader); + + // go from the bottom to the top of the log file + // and count how many records are inside of report interval (like not older then 1 day) + for ($i = $logCount - 1; $i >= 0; $i--) { + $lastLine = $logReader[$i]; + + // calculate log relevance in hours + $recordFreshness = $dateNow->diff($lastLine['date'])->days; + + // check log relevance + if ($recordFreshness > $recordStaleThreshold) { + break; + } + + // check record severity + if (in_array($lastLine['level'], $excludedSeverities, true)) { + continue; + } + + $logReport[$logPath] = array_key_exists($logPath, $logReport) ? $logReport[$logPath] + 1 : 1; + } + } + + if (0 === count($logReport)) { + return TaskResult::createPassed($this, $context); + } + + $message = '✘ Magento Logs have recently added records:' . PHP_EOL; + + foreach ($logReport as $logPath => $recentLogCount) { + $message .= sprintf( + '• %s - %s %s', + $logPath, + $recentLogCount, + $recentLogCount > 1 ? 'records' : 'record' + ) . PHP_EOL; + } + + return TaskResult::createFailed( + $this, + $context, + $message + ); + } +}