diff --git a/src/module-elasticsuite-core/Api/Healthcheck/CheckInterface.php b/src/module-elasticsuite-core/Api/Healthcheck/CheckInterface.php new file mode 100644 index 000000000..b1194c760 --- /dev/null +++ b/src/module-elasticsuite-core/Api/Healthcheck/CheckInterface.php @@ -0,0 +1,51 @@ + + * @copyright 2024 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Api\Healthcheck; + +/** + * Health CheckInterface. + */ +interface CheckInterface +{ + /** + * Retrieve the unique identifier for the health check. + * + * @return string + */ + public function getIdentifier(): string; + + /** + * Retrieve the description of the health check. + * + * @return string + */ + public function getDescription(): string; + + /** + * Retrieve the status of the health check. + * Expected values: 'success', 'warning'. + * + * @return string + */ + public function getStatus(): string; + + /** + * Retrieve the sort order for the health check, which determines + * the display order of checks in the admin panel. + * + * @return int + */ + public function getSortOrder(): int; +} diff --git a/src/module-elasticsuite-core/Block/Adminhtml/Healthcheck/Healthcheck.php b/src/module-elasticsuite-core/Block/Adminhtml/Healthcheck/Healthcheck.php new file mode 100644 index 000000000..d9dae75a0 --- /dev/null +++ b/src/module-elasticsuite-core/Block/Adminhtml/Healthcheck/Healthcheck.php @@ -0,0 +1,61 @@ + + * @copyright 2024 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Block\Adminhtml\Healthcheck; + +use Magento\Backend\Block\Template; +use Magento\Backend\Block\Template\Context; +use Smile\ElasticsuiteCore\Api\Healthcheck\CheckInterface; +use Smile\ElasticsuiteCore\Healthcheck\HealthcheckList; + +/** + * Class Healthcheck. + * + * Block class for displaying Elasticsuite health checks in the Magento Admin panel. + */ +class Healthcheck extends Template +{ + /** + * HealthcheckList instance to manage and retrieve health checks. + * + * @var HealthcheckList + */ + private $healthcheckList; + + /** + * Constructor. + * + * @param Context $context Magento context object for backend blocks. + * @param HealthcheckList $healthcheckList The health check list object providing health check data. + * @param array $data Additional block data. + */ + public function __construct(Context $context, HealthcheckList $healthcheckList, array $data = []) + { + parent::__construct($context, $data); + $this->healthcheckList = $healthcheckList; + } + + /** + * Retrieve all health checks. + * + * Provides an array of health check instances, each implementing the CheckInterface, + * sorted by their specified order. + * + * @return CheckInterface[] + */ + public function getHealthchecks(): array + { + return $this->healthcheckList->getChecks(); + } +} diff --git a/src/module-elasticsuite-core/Controller/Adminhtml/Healthcheck/Index.php b/src/module-elasticsuite-core/Controller/Adminhtml/Healthcheck/Index.php new file mode 100644 index 000000000..9fffa54a9 --- /dev/null +++ b/src/module-elasticsuite-core/Controller/Adminhtml/Healthcheck/Index.php @@ -0,0 +1,42 @@ + + * @copyright 2024 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Controller\Adminhtml\Healthcheck; + +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\View\Result\Page; +use Smile\ElasticsuiteIndices\Controller\Adminhtml\AbstractAction; + +/** + * Class Index. + */ +class Index extends AbstractAction implements HttpGetActionInterface +{ + /** + * @inheritdoc + * + * @return Page + */ + public function execute(): Page + { + $breadMain = __('Healthcheck'); + + /** @var Page $resultPage */ + $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $resultPage->getConfig()->getTitle()->prepend($breadMain); + + return $resultPage; + } +} diff --git a/src/module-elasticsuite-core/Healthcheck/GhostIndicesCheck.php b/src/module-elasticsuite-core/Healthcheck/GhostIndicesCheck.php new file mode 100644 index 000000000..9e9133dee --- /dev/null +++ b/src/module-elasticsuite-core/Healthcheck/GhostIndicesCheck.php @@ -0,0 +1,164 @@ + + * @copyright 2024 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Healthcheck; + +use Exception; +use Magento\Framework\UrlInterface; +use Smile\ElasticsuiteCore\Api\Healthcheck\CheckInterface; +use Smile\ElasticsuiteIndices\Model\IndexStatsProvider; + +/** + * Class GhostIndicesCheck. + * + * Health check to identify any ghost indices in the Elasticsearch cluster. + */ +class GhostIndicesCheck implements CheckInterface +{ + /** + * Route to Elasticsuite -> Indices page. + */ + private const ROUTE_ELASTICSUITE_INDICES = 'smile_elasticsuite_indices'; + + public const GHOST_STATUS = 'ghost'; + + /** + * @var IndexStatsProvider + */ + private $indexStatsProvider; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * Constructor. + * + * @param IndexStatsProvider $indexStatsProvider Index stats provider. + * @param UrlInterface $urlBuilder URL builder. + */ + public function __construct( + IndexStatsProvider $indexStatsProvider, + UrlInterface $urlBuilder + ) { + $this->indexStatsProvider = $indexStatsProvider; + $this->urlBuilder = $urlBuilder; + } + + /** + * Retrieve the unique identifier for this health check. + * + * @return string + */ + public function getIdentifier(): string + { + return 'ghost_indices_check'; + } + + /** + * Retrieve a brief description of this health check. + * + * @return string + * @throws Exception + */ + public function getDescription(): string + { + $ghostCount = $this->getNumberOfGhostIndices(); + + if ($ghostCount > 0) { + // Description when ghost indices are found. + // @codingStandardsIgnoreStart + return __( + 'You have %1 ghost indices. Ghost indices have a footprint on your Elasticsearch cluster health. ' + . 'You should consider removing them.
' + . 'Click here to go to the Elasticsuite Indices page to take appropriate actions.', + $ghostCount, + $this->getElasticsuiteIndicesUrl() + ); + // @codingStandardsIgnoreEnd + } + + // Description when no ghost indices are found. + return __('There are no ghost indexes in your Elasticsearch cluster. No action is required at this time.'); + } + + /** + * Retrieve the status of this health check. + * Returns 'warning' if ghost indices are found, otherwise 'success'. + * + * @return string + * @throws Exception + */ + public function getStatus(): string + { + return $this->hasGhostIndices() ? 'warning' : 'success'; + } + + /** + * Retrieve the sort order for this health check. + * + * @return int + */ + public function getSortOrder(): int + { + return 10; // Adjust as necessary. + } + + /** + * Checks if there are any ghost indices. + * + * @return bool + * @throws Exception + */ + private function hasGhostIndices(): bool + { + return $this->getNumberOfGhostIndices() > 0; + } + + /** + * Get number of ghost Elasticsuite indices. + * + * @return int + * @throws Exception + */ + private function getNumberOfGhostIndices(): int + { + $ghostIndices = 0; + $elasticsuiteIndices = $this->indexStatsProvider->getElasticSuiteIndices(); + + if ($elasticsuiteIndices !== null) { + foreach ($elasticsuiteIndices as $indexName => $indexAlias) { + $indexData = $this->indexStatsProvider->indexStats($indexName, $indexAlias); + + if (array_key_exists('index_status', $indexData) + && $indexData['index_status'] === self::GHOST_STATUS) { + $ghostIndices++; + } + } + } + + return $ghostIndices; + } + + /** + * Retrieve a URL to the Elasticsuite Indices page for more information. + * + * @return string + */ + public function getElasticsuiteIndicesUrl(): string + { + return $this->urlBuilder->getUrl(self::ROUTE_ELASTICSUITE_INDICES); + } +} diff --git a/src/module-elasticsuite-core/Healthcheck/HealthcheckList.php b/src/module-elasticsuite-core/Healthcheck/HealthcheckList.php new file mode 100644 index 000000000..587f99b26 --- /dev/null +++ b/src/module-elasticsuite-core/Healthcheck/HealthcheckList.php @@ -0,0 +1,58 @@ + + * @copyright 2024 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Healthcheck; + +use Smile\ElasticsuiteCore\Api\Healthcheck\CheckInterface; + +/** + * Class HealthcheckList. + * + * Manages a list of health checks for the Elasticsuite module. + */ +class HealthcheckList +{ + /** + * Array of health checks implementing the CheckInterface. + * + * @var CheckInterface[] + */ + private $checks; + + /** + * Constructor. + * + * @param CheckInterface[] $checks Array of health checks to be managed by this list. + */ + public function __construct(array $checks = []) + { + $this->checks = $checks; + } + + /** + * Retrieve all health checks, sorted by their sort order. + * + * Sorts the checks based on the value returned by each check's `getSortOrder` method. + * + * @return CheckInterface[] Array of health checks sorted by order. + */ + public function getChecks(): array + { + usort($this->checks, function (CheckInterface $a, CheckInterface $b) { + return $a->getSortOrder() <=> $b->getSortOrder(); + }); + + return $this->checks; + } +} diff --git a/src/module-elasticsuite-core/Healthcheck/ReplicasConfigCheck.php b/src/module-elasticsuite-core/Healthcheck/ReplicasConfigCheck.php new file mode 100644 index 000000000..434e517d1 --- /dev/null +++ b/src/module-elasticsuite-core/Healthcheck/ReplicasConfigCheck.php @@ -0,0 +1,167 @@ + + * @copyright 2024 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Healthcheck; + +use Magento\Framework\UrlInterface; +use Smile\ElasticsuiteCore\Api\Healthcheck\CheckInterface; +use Smile\ElasticsuiteCore\Helper\IndexSettings as IndexSettingsHelper; +use Smile\ElasticsuiteCore\Client\Client; + +/** + * Class ReplicasConfigCheck. + * + * Health check for replicas misconfiguration in Elasticsuite. + */ +class ReplicasConfigCheck implements CheckInterface +{ + /** + * Route to Stores -> Configuration section. + */ + private const ROUTE_SYSTEM_CONFIG = 'adminhtml/system_config/edit'; + + /** + * Anchor for Stores -> Configuration -> ELASTICSUITE -> Base Settings -> Indices Settings. + */ + private const ANCHOR_ES_INDICES_SETTINGS_PATH = 'smile_elasticsuite_core_base_settings_indices_settings-link'; + + /** + * URL for Elasticsuite Indices Settings Wiki page. + */ + private const ES_INDICES_SETTINGS_WIKI_PAGE = 'https://github.com/Smile-SA/elasticsuite/wiki/ModuleInstall#indices-settings'; + + public const WARNING_STATUS = 'warning'; + + /** + * @var IndexSettingsHelper + */ + protected $helper; + + /** + * @var Client + */ + protected $client; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * Constructor. + * + * @param IndexSettingsHelper $indexSettingHelper Index settings helper. + * @param Client $client Elasticsearch client. + * @param UrlInterface $urlBuilder URL builder. + */ + public function __construct( + IndexSettingsHelper $indexSettingHelper, + Client $client, + UrlInterface $urlBuilder + ) { + $this->helper = $indexSettingHelper; + $this->client = $client; + $this->urlBuilder = $urlBuilder; + } + + /** + * Retrieve the unique identifier for this health check. + * + * @return string + */ + public function getIdentifier(): string + { + return 'replicas_config_check'; + } + + /** + * Retrieve a dynamic description for this health check based on its status. + * + * @return string + */ + public function getDescription(): string + { + $status = $this->getStatus(); + + if ($status === self::WARNING_STATUS) { + // Description when the replicas configuration is incorrect. + // @codingStandardsIgnoreStart + return __( + 'The number of replicas configured for Elasticsuite is incorrect. ' + . 'You can\'t use %1 replicas since there is only %2 nodes in your Elasticsearch cluster.
' + . 'Click here to go to the Elasticsuite Basic Settings page and change your Number of Replicas per Index parameter according to our Wiki page.', + $this->helper->getNumberOfReplicas(), + $this->getNumberOfNodes(), + $this->getElasticsuiteConfigUrl(), + self::ES_INDICES_SETTINGS_WIKI_PAGE + ); + // @codingStandardsIgnoreEnd + } + + // Description when the replicas configuration is optimized. + return __('The number of replicas is properly configured for the Elasticsearch cluster. No action is required at this time.'); + } + + /** + * Retrieve the status of this health check. + * + * @return string + */ + public function getStatus(): string + { + $numberOfReplicas = $this->helper->getNumberOfReplicas(); + $numberOfNodes = $this->getNumberOfNodes(); + + if ($numberOfReplicas > 1 && $numberOfReplicas > ($numberOfNodes - 1)) { + return 'warning'; + } + + return 'success'; + } + + /** + * Retrieve the sort order for this health check. + * + * @return int + */ + public function getSortOrder(): int + { + return 20; // Adjust as necessary. + } + + /** + * Get the number of nodes in the Elasticsearch cluster. + * + * @return int + */ + private function getNumberOfNodes(): int + { + $nodeInfo = $this->client->nodes()->info()['_nodes'] ?? []; + + return $nodeInfo['total'] ?? 0; + } + + /** + * Get URL to the admin Elasticsuite Configuration page. + * + * @return string + */ + private function getElasticsuiteConfigUrl(): string + { + return $this->urlBuilder->getUrl( + self::ROUTE_SYSTEM_CONFIG, + ['section' => 'smile_elasticsuite_core_base_settings', '_fragment' => self::ANCHOR_ES_INDICES_SETTINGS_PATH] + ); + } +} diff --git a/src/module-elasticsuite-core/Healthcheck/ShardsConfigCheck.php b/src/module-elasticsuite-core/Healthcheck/ShardsConfigCheck.php new file mode 100644 index 000000000..66e04fe9f --- /dev/null +++ b/src/module-elasticsuite-core/Healthcheck/ShardsConfigCheck.php @@ -0,0 +1,192 @@ + + * @copyright 2024 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Healthcheck; + +use Exception; +use Magento\Framework\UrlInterface; +use Smile\ElasticsuiteCore\Api\Healthcheck\CheckInterface; +use Smile\ElasticsuiteCore\Helper\IndexSettings as IndexSettingsHelper; +use Smile\ElasticsuiteIndices\Model\IndexStatsProvider; + +/** + * Class ShardsConfigCheck. + * + * Checks for shard misconfigurations in the Elasticsearch cluster. + */ +class ShardsConfigCheck implements CheckInterface +{ + /** + * Route to Stores -> Configuration section. + */ + private const ROUTE_SYSTEM_CONFIG = 'adminhtml/system_config/edit'; + + /** + * Anchor for Stores -> Configuration -> ELASTICSUITE -> Base Settings -> Indices Settings. + */ + private const ANCHOR_ES_INDICES_SETTINGS_PATH = 'smile_elasticsuite_core_base_settings_indices_settings-link'; + + /** + * URL for Elasticsuite Indices Settings Wiki page. + */ + private const ES_INDICES_SETTINGS_WIKI_PAGE = 'https://github.com/Smile-SA/elasticsuite/wiki/ModuleInstall#indices-settings'; + + public const WARNING_STATUS = 'warning'; + + public const UNDEFINED_SIZE = 'undefined'; + + /** + * @var IndexSettingsHelper + */ + private $indexSettingsHelper; + + /** + * @var IndexStatsProvider + */ + private $indexStatsProvider; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * Constructor. + * + * @param IndexSettingsHelper $indexSettingsHelper Index settings helper. + * @param IndexStatsProvider $indexStatsProvider Index stats provider. + * @param UrlInterface $urlBuilder URL builder. + */ + public function __construct( + IndexSettingsHelper $indexSettingsHelper, + IndexStatsProvider $indexStatsProvider, + UrlInterface $urlBuilder + ) { + $this->indexSettingsHelper = $indexSettingsHelper; + $this->indexStatsProvider = $indexStatsProvider; + $this->urlBuilder = $urlBuilder; + } + + /** + * Retrieve the unique identifier for this health check. + * + * @return string + */ + public function getIdentifier(): string + { + return 'shards_config_check'; + } + + /** + * Retrieve a dynamic description for this health check based on its status. + * + * @return string + * @throws Exception + */ + public function getDescription(): string + { + $numberOfShards = $this->indexSettingsHelper->getNumberOfShards(); + $maxIndexSize = $this->getMaxIndexSize(); + $status = $this->getStatus(); + + if ($status === self::WARNING_STATUS) { + // Description when the shard's configuration is incorrect. + // @codingStandardsIgnoreStart + return __( + 'The number of shards configured for Elasticsuite is incorrect. ' + . 'You don\'t need to use %1 shards since your biggest Elasticsuite index is only %2.
' + . 'Click here to go to the Elasticsuite Basic Settings page and change your Number of Shards per Index parameter according to our Wiki page.', + $numberOfShards, + $maxIndexSize['human_size'], + $this->getElasticsuiteConfigUrl(), + self::ES_INDICES_SETTINGS_WIKI_PAGE + ); + // @codingStandardsIgnoreEnd + } + + // Description when the shard's configuration is optimized. + return __('The number of shards is properly configured for the Elasticsearch cluster. No action is required at this time.'); + } + + /** + * Retrieve the status of this health check. + * + * @return string + * @throws Exception + */ + public function getStatus(): string + { + $numberOfShards = $this->indexSettingsHelper->getNumberOfShards(); + $maxIndexSize = $this->getMaxIndexSize(); + + if ($numberOfShards > 1 && $maxIndexSize && $maxIndexSize['size_in_bytes'] < 10737418240) { + return 'warning'; + } + + return 'success'; + } + + /** + * Retrieve the sort order for this health check. + * + * @return int + */ + public function getSortOrder(): int + { + return 30; // Adjust as necessary. + } + + /** + * Get size of the largest Elasticsuite index. + * + * @return array|false + * @throws Exception + */ + private function getMaxIndexSize() + { + $elasticsuiteIndices = $this->indexStatsProvider->getElasticSuiteIndices(); + $indexSizes = []; + + foreach ($elasticsuiteIndices as $indexName => $indexAlias) { + $indexData = $this->indexStatsProvider->indexStats($indexName, $indexAlias); + + if (array_key_exists('size', $indexData) && array_key_exists('size_in_bytes', $indexData) + && $indexData['size_in_bytes'] !== self::UNDEFINED_SIZE) { + $indexSizes[] = ['human_size' => $indexData['size'], 'size_in_bytes' => $indexData['size_in_bytes']]; + } + } + + if (!empty($indexSizes)) { + $indexSizesInBytes = array_column($indexSizes, "size_in_bytes"); + array_multisort($indexSizesInBytes, SORT_DESC, $indexSizes); + + return current($indexSizes); + } + + return false; + } + + /** + * Get URL to the Elasticsuite Configuration page. + * + * @return string + */ + private function getElasticsuiteConfigUrl(): string + { + return $this->urlBuilder->getUrl( + self::ROUTE_SYSTEM_CONFIG, + ['section' => 'smile_elasticsuite_core_base_settings', '_fragment' => self::ANCHOR_ES_INDICES_SETTINGS_PATH] + ); + } +} diff --git a/src/module-elasticsuite-core/Model/System/Message/GenericWarningAboutClusterMisconfig.php b/src/module-elasticsuite-core/Model/System/Message/GenericWarningAboutClusterMisconfig.php new file mode 100644 index 000000000..b3d9bbc7f --- /dev/null +++ b/src/module-elasticsuite-core/Model/System/Message/GenericWarningAboutClusterMisconfig.php @@ -0,0 +1 @@ + + + + diff --git a/src/module-elasticsuite-core/etc/adminhtml/menu.xml b/src/module-elasticsuite-core/etc/adminhtml/menu.xml index 3f66f7af0..86e5ef442 100644 --- a/src/module-elasticsuite-core/etc/adminhtml/menu.xml +++ b/src/module-elasticsuite-core/etc/adminhtml/menu.xml @@ -3,14 +3,7 @@ - + + diff --git a/src/module-elasticsuite-core/view/adminhtml/layout/smile_elasticsuite_healthcheck_index.xml b/src/module-elasticsuite-core/view/adminhtml/layout/smile_elasticsuite_healthcheck_index.xml new file mode 100644 index 000000000..fae89ae7f --- /dev/null +++ b/src/module-elasticsuite-core/view/adminhtml/layout/smile_elasticsuite_healthcheck_index.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/src/module-elasticsuite-core/view/adminhtml/templates/healthcheck/list.phtml b/src/module-elasticsuite-core/view/adminhtml/templates/healthcheck/list.phtml new file mode 100644 index 000000000..7add1ba8a --- /dev/null +++ b/src/module-elasticsuite-core/view/adminhtml/templates/healthcheck/list.phtml @@ -0,0 +1,41 @@ + + * @copyright 2024 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +/** @var \Smile\ElasticsuiteCore\Block\Adminhtml\Healthcheck\Healthcheck $block */ +?> + + + + + + + + + + + getHealthchecks() as $index => $check): ?> + + + + + + + +
getIdentifier() ?>getDescription() ?> + + getStatus() ?> + +
diff --git a/src/module-elasticsuite-core/view/adminhtml/web/css/source/_module.less b/src/module-elasticsuite-core/view/adminhtml/web/css/source/_module.less index a9b9f6a0e..620cca5bd 100644 --- a/src/module-elasticsuite-core/view/adminhtml/web/css/source/_module.less +++ b/src/module-elasticsuite-core/view/adminhtml/web/css/source/_module.less @@ -42,3 +42,14 @@ fieldset.radioset-tooltip { line-height: @line-height__l; } } + +.smile_elasticsuite-healthcheck-index { + .data-grid tbody tr:nth-child(odd) td { + background-color: #ffffff; /* White */ + } + + .data-grid tbody tr:nth-child(even) td { + background-color: #f5f5f5; /* Light gray */ + } +} +