├── .gitignore ├── Api ├── Data │ ├── RedisReportInterface.php │ └── RedisReportSearchResultsInterface.php └── RedisReportRepositoryInterface.php ├── Controller └── Adminhtml │ └── Index │ └── Index.php ├── Cron ├── Clean.php └── Log.php ├── LICENSE ├── Model ├── RedisInfo.php ├── RedisReport.php ├── RedisReportRepository.php └── ResourceModel │ ├── RedisReport.php │ └── RedisReport │ └── Collection.php ├── README.md ├── Scope └── Config.php ├── composer.json ├── etc ├── acl.xml ├── adminhtml │ ├── menu.xml │ ├── routes.xml │ └── system.xml ├── config.xml ├── crontab.xml ├── db_schema.xml ├── di.xml └── module.xml ├── registration.php └── view └── adminhtml ├── layout └── redis_report_index_index.xml ├── templates ├── info │ ├── activity.phtml │ ├── connections.phtml │ ├── databases.phtml │ └── memory.phtml └── wrapper.phtml └── web ├── css └── module.css └── js └── chart.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /Api/Data/RedisReportInterface.php: -------------------------------------------------------------------------------- 1 | resultPageFactory = $resultPageFactory; 28 | } 29 | 30 | public function execute(): Page 31 | { 32 | $resultPage = $this->resultPageFactory->create(); 33 | $resultPage->getConfig()->getTitle()->prepend(__('Redis Report')); 34 | 35 | return $resultPage; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Cron/Clean.php: -------------------------------------------------------------------------------- 1 | redisReportRepository = $redisReportRepository; 34 | $this->moduleConfig = $moduleConfig; 35 | $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; 36 | $this->dateTimeFactory = $dateTimeFactory; 37 | } 38 | 39 | /** 40 | * Delete old Redis report data. 41 | * 42 | * @return void 43 | * @throws CouldNotDeleteException 44 | */ 45 | public function execute(): void 46 | { 47 | if (!($days = $this->moduleConfig->getRedisReportLogTruncateDays())) { 48 | return; 49 | } 50 | 51 | $lastException = null; 52 | 53 | /** @var DateTime $cutoff */ 54 | $cutoff = $this->dateTimeFactory->create(date(DateTime::DATETIME_PHP_FORMAT, strtotime("-$days day"))); 55 | 56 | /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ 57 | $searchCriteriaBuilder = $this->searchCriteriaBuilderFactory->create(); 58 | $searchCriteria = $searchCriteriaBuilder->addFilter( 59 | RedisReportInterface::CREATED_AT, 60 | $cutoff, 61 | 'lt' 62 | )->create(); 63 | 64 | /** @var RedisReportInterface[] $redisReportsToDelete */ 65 | $redisReportsToDelete = $this->redisReportRepository->getList($searchCriteria)->getItems(); 66 | 67 | foreach ($redisReportsToDelete as $redisReport) { 68 | try { 69 | $this->redisReportRepository->delete($redisReport); 70 | } catch (CouldNotDeleteException $e) { 71 | $lastException = $e; // store exception as not to stop execution 72 | } 73 | } 74 | 75 | if ($lastException instanceof Exception) { 76 | throw $lastException; // re-throw exception to make noise 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Cron/Log.php: -------------------------------------------------------------------------------- 1 | redisReportRepository = $redisReportRepository; 31 | $this->redisInfo = $redisInfo; 32 | $this->redisReportFactory = $redisReportFactory; 33 | $this->moduleConfig = $moduleConfig; 34 | } 35 | 36 | /** 37 | * Store Redis report data. 38 | * 39 | * @return void 40 | * @throws CouldNotSaveException 41 | */ 42 | public function execute(): void 43 | { 44 | if (!$this->moduleConfig->isRedisReportLoggingCronEnabled()) { 45 | return; 46 | } 47 | 48 | /** @var RedisReportInterface $redisReportModel */ 49 | $redisReportModel = $this->redisReportFactory->create(); 50 | $allRedisData = $this->redisInfo->get(); 51 | 52 | if (array_key_exists('chart-data', $allRedisData)) { 53 | $redisReportModel->setChartData($allRedisData['chart-data']); 54 | unset($allRedisData['chart-data']); 55 | } 56 | 57 | $redisReportModel->setReportData($allRedisData); 58 | $this->redisReportRepository->save($redisReportModel); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 element119 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Model/RedisInfo.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 35 | $this->deploymentConfig = $deploymentConfig; 36 | $this->resourceConnection = $resourceConnection; 37 | $this->redis = $this->cache->getFrontend()->getBackend(); 38 | } 39 | 40 | public function get(): array 41 | { 42 | if (!($redisInfo = $this->getAllRedisInfo())) { 43 | return []; 44 | } 45 | 46 | return [ 47 | 'version' => $redisInfo['redis_version'], 48 | 'databases' => $this->getDatabaseKeyspaceInfo($redisInfo), 49 | 'memory' => [ 50 | 'Used Memory' => $redisInfo['used_memory_human'], 51 | 'Used Memory Peak' => $redisInfo['used_memory_peak_human'], 52 | 'Used Memory Peak Percentage' => $redisInfo['used_memory_peak_perc'], 53 | 'Used Memory Dataset Percentage' => $redisInfo['used_memory_dataset_perc'], 54 | 'Percentage of Keys Used by Magento' => $this->getMagentoKeyUsagePercentage($redisInfo) . '%', 55 | 'Total System Memory' => $redisInfo['total_system_memory_human'], 56 | 'Max. Memory' => $redisInfo['maxmemory_human'], 57 | 'Max. Memory Policy' => $redisInfo['maxmemory_policy'], 58 | ], 59 | 'connections' => [ 60 | 'Cluster Enabled' => $redisInfo['cluster_enabled'], 61 | 'Max. Clients' => $redisInfo['maxclients'], 62 | 'Connected Clients' => $redisInfo['connected_clients'], 63 | 'Cluster Connections' => $redisInfo['cluster_connections'], 64 | 'Rejected Connections' => $redisInfo['rejected_connections'], 65 | ], 66 | 'activity' => [ 67 | 'Uptime in Days' => $redisInfo['uptime_in_days'], 68 | 'Uptime in Seconds' => $redisInfo['uptime_in_seconds'], 69 | 'Keyspace Hits' => $redisInfo['keyspace_hits'], 70 | 'Keyspace Misses' => $redisInfo['keyspace_misses'], 71 | 'Hit Rate' => $this->redis->getHitMissPercentage() . '%', 72 | 'Total Error Replies' => $redisInfo['total_error_replies'], 73 | 'Evicted Keys' => $redisInfo['evicted_keys'], 74 | 'Expired Keys' => $redisInfo['expired_keys'], 75 | 'Used CPU Time (Sys)' => round((float)$redisInfo['used_cpu_sys'], 2) . ' seconds', 76 | 'Used CPU Time (User)' => round((float)$redisInfo['used_cpu_user'], 2) . ' seconds', 77 | 'RDB Last Save Time' => date('r', (int)$redisInfo['rdb_last_save_time']), 78 | 'RDB Changes Since Last Save' => $redisInfo['rdb_changes_since_last_save'], 79 | 'Total Reads Processed' => $redisInfo['total_reads_processed'], 80 | 'Total Writes Processed' => $redisInfo['total_writes_processed'], 81 | ], 82 | 'chart-data' => [ 83 | 'memory' => [ 84 | 'Used Memory' => $redisInfo['used_memory'], 85 | 'Used Memory Peak' => $redisInfo['used_memory_peak'], 86 | 'Used Memory Peak Percentage' => $redisInfo['used_memory_peak_perc'], 87 | 'Used Memory Dataset Percentage' => $redisInfo['used_memory_dataset_perc'], 88 | 'Percentage of Keys Used by Magento' => $this->getMagentoKeyUsagePercentage($redisInfo), 89 | ], 90 | 'activity' => [ 91 | 'Keyspace Hits' => $redisInfo['keyspace_hits'], 92 | 'Keyspace Misses' => $redisInfo['keyspace_misses'], 93 | 'Hit Rate' => $this->redis->getHitMissPercentage(), 94 | 'Total Error Replies' => $redisInfo['total_error_replies'], 95 | 'Evicted Keys' => $redisInfo['evicted_keys'], 96 | 'Expired Keys' => $redisInfo['expired_keys'], 97 | 'Used CPU Time (Sys)' => round((float)$redisInfo['used_cpu_sys'], 2), 98 | 'Used CPU Time (User)' => round((float)$redisInfo['used_cpu_user'], 2), 99 | 'Total Reads Processed' => $redisInfo['total_reads_processed'], 100 | 'Total Writes Processed' => $redisInfo['total_writes_processed'], 101 | ], 102 | ], 103 | ]; 104 | } 105 | 106 | public function getDatabaseKeyspaceInfo(array $redisInfo): array 107 | { 108 | $redisDatabaseInfo = []; 109 | $dbKeys = preg_grep('/^db(\d*)/m', array_keys($redisInfo)); // db0, db1 ... db(n-1), db(n) 110 | 111 | foreach ($dbKeys as $dbKey) { 112 | // keyspace info e.g. key count, expiring key count, and average ttl 113 | $dbInfo = explode(',', $redisInfo[$dbKey]); 114 | 115 | foreach ($dbInfo as $info) { 116 | $data = explode('=', $info); 117 | 118 | array_key_exists($dbKey, $redisDatabaseInfo) 119 | ? $redisDatabaseInfo[$dbKey] += [$data[0] => (int)$data[1]] // append 120 | : $redisDatabaseInfo[$dbKey] = [$data[0] => (int)$data[1]]; // initialise 121 | } 122 | 123 | $avgTtlKey = array_key_last($redisDatabaseInfo[$dbKey]); 124 | $redisDatabaseInfo[$dbKey][$avgTtlKey] = ($redisDatabaseInfo[$dbKey][$avgTtlKey] / 1000) . ' seconds'; 125 | } 126 | 127 | return $redisDatabaseInfo; 128 | } 129 | 130 | public function getMagentoKeyUsagePercentage(array $redisInfo): float 131 | { 132 | $cacheConfig = $this->deploymentConfig->get('cache'); 133 | 134 | if (!isset($cacheConfig['frontend']['default']['id_prefix'])) { 135 | return -1.0; 136 | } 137 | 138 | $allTags = $this->redis->getTags() + $this->redis->getIds(); 139 | $redisKeyPrefix = $cacheConfig['frontend']['default']['id_prefix']; 140 | $tagsWithPrefix = preg_grep('/^' . preg_quote($redisKeyPrefix) . '/m', $allTags); 141 | 142 | return round((count($tagsWithPrefix) / count($allTags)) * 100, 2); 143 | } 144 | 145 | /** 146 | * Return historic Redis data, keyed by creation timestamp. 147 | * 148 | * @return array 149 | * @throws Zend_Db_Statement_Exception 150 | */ 151 | public function getHistoricRedisReportData(): array 152 | { 153 | $reportData = []; 154 | $connection = $this->resourceConnection->getConnection(); 155 | $redisReportSelect = $connection->select()->from( 156 | $this->resourceConnection->getTableName(RedisReportRepositoryInterface::MAIN_TABLE), 157 | [ 158 | RedisReportInterface::REPORT_DATA, 159 | RedisReportInterface::CHART_DATA, 160 | RedisReportInterface::CREATED_AT, 161 | ] 162 | ); 163 | 164 | foreach ($connection->query($redisReportSelect)->fetchAll() as $dbData) { 165 | $reportData[$dbData[RedisReportInterface::CREATED_AT]] = json_decode( 166 | $dbData[RedisReportInterface::REPORT_DATA], true 167 | ); 168 | 169 | if ($dbData[RedisReportInterface::CHART_DATA]) { 170 | $chartData = json_decode($dbData[RedisReportInterface::CHART_DATA], true); 171 | $chartData[RedisReportInterface::CREATED_AT] = $dbData[RedisReportInterface::CREATED_AT]; 172 | 173 | $reportData[$dbData[RedisReportInterface::CREATED_AT]][RedisReportInterface::CHART_DATA] = $chartData; 174 | } 175 | } 176 | 177 | return $reportData; 178 | } 179 | 180 | /** 181 | * Get all Redis information. 182 | * 183 | * This function exists solely to ease extensibility. 184 | * 185 | * @link https://redis.io/commands/info/ 186 | * 187 | * @return array 188 | */ 189 | public function getAllRedisInfo(): array 190 | { 191 | return $this->redis instanceof Cm_Cache_Backend_Redis && method_exists($this->redis, 'getInfo') 192 | ? $this->redis->getInfo() 193 | : []; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Model/RedisReport.php: -------------------------------------------------------------------------------- 1 | _init(RedisReportResourceModel::class); 22 | } 23 | 24 | /** 25 | * @inheritDoc 26 | */ 27 | public function getEntityId(): int 28 | { 29 | return (int)$this->getData(self::ENTITY_ID); 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | public function setEntityId($entityId): self 36 | { 37 | return $this->setData(self::ENTITY_ID, $entityId); 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | public function getReportData(): array 44 | { 45 | return $this->getData(json_decode(self::REPORT_DATA)); 46 | } 47 | 48 | /** 49 | * @inheritDoc 50 | */ 51 | public function setReportData(array $reportData): RedisReportInterface 52 | { 53 | return $this->setData(self::REPORT_DATA, json_encode($reportData)); 54 | } 55 | 56 | /** 57 | * @inheritDoc 58 | */ 59 | public function getChartData(): array 60 | { 61 | return $this->getData(json_decode(self::CHART_DATA)); 62 | } 63 | 64 | /** 65 | * @inheritDoc 66 | */ 67 | public function setChartData(array $chartData): RedisReportInterface 68 | { 69 | return $this->setData(self::CHART_DATA, json_encode($chartData)); 70 | } 71 | 72 | /** 73 | * @inheritDoc 74 | */ 75 | public function getCreatedAt(): string 76 | { 77 | return $this->getData(self::CREATED_AT); 78 | } 79 | 80 | /** 81 | * @inheritDoc 82 | */ 83 | public function setCreatedAt(string $createdAt): RedisReportInterface 84 | { 85 | return $this->setData(self::CREATED_AT, $createdAt); 86 | } 87 | 88 | /** 89 | * @inheritDoc 90 | */ 91 | public function getUpdatedAt(): string 92 | { 93 | return $this->getData(self::UPDATED_AT); 94 | } 95 | 96 | /** 97 | * @inheritDoc 98 | */ 99 | public function setUpdatedAt(string $updatedAt): RedisReportInterface 100 | { 101 | return $this->setData(self::UPDATED_AT, $updatedAt); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Model/RedisReportRepository.php: -------------------------------------------------------------------------------- 1 | redisReportCollectionFactory = $redisReportCollectionFactory; 40 | $this->redisReportFactory = $redisReportFactory; 41 | $this->resource = $resource; 42 | $this->searchResultsFactory = $searchResultsFactory; 43 | $this->collectionProcessor = $collectionProcessor; 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | public function save(RedisReportInterface $redisReport): RedisReportInterface 50 | { 51 | try { 52 | $this->resource->save($redisReport); 53 | } catch (Exception $e) { 54 | throw new CouldNotSaveException(__('Could not save the report: %1', $e->getMessage())); 55 | } 56 | 57 | return $redisReport; 58 | } 59 | 60 | /** 61 | * @inheritDoc 62 | */ 63 | public function getById($redisReportId): RedisReportInterface 64 | { 65 | $redisReport = $this->redisReportFactory->create(); 66 | $this->resource->load($redisReport, $redisReportId); 67 | 68 | if (!$redisReport->getId()) { 69 | throw new NoSuchEntityException(__('Redis report with entity ID "%1" does not exist.', $redisReportId)); 70 | } 71 | 72 | return $redisReport; 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function getList(SearchCriteriaInterface $searchCriteria): SearchResults 79 | { 80 | $collection = $this->redisReportCollectionFactory->create(); 81 | 82 | $this->collectionProcessor->process($searchCriteria, $collection); 83 | 84 | $searchResults = $this->searchResultsFactory->create(); 85 | $searchResults->setSearchCriteria($searchCriteria); 86 | 87 | $items = []; 88 | 89 | foreach ($collection as $redisReport) { 90 | $items[] = $redisReport; 91 | } 92 | 93 | $searchResults->setItems($items); 94 | $searchResults->setTotalCount($collection->getSize()); 95 | 96 | return $searchResults; 97 | } 98 | 99 | /** 100 | * @inheritDoc 101 | */ 102 | public function delete(RedisReportInterface $redisReport): bool 103 | { 104 | try { 105 | /** @var RedisReportInterface $redisReportModel */ 106 | $redisReportModel = $this->redisReportFactory->create(); 107 | 108 | $this->resource->load($redisReportModel, $redisReport->getEntityId()); 109 | $this->resource->delete($redisReportModel); 110 | } catch (Exception $e) { 111 | throw new CouldNotDeleteException(__('Could not delete the Redis report with ID: %1', $e->getMessage())); 112 | } 113 | 114 | return true; 115 | } 116 | 117 | /** 118 | * @inheritDoc 119 | */ 120 | public function deleteById($redisReportId): bool 121 | { 122 | return $this->delete($this->getById($redisReportId)); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Model/ResourceModel/RedisReport.php: -------------------------------------------------------------------------------- 1 | _init(RedisReportRepositoryInterface::MAIN_TABLE, RedisReportInterface::ENTITY_ID); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Model/ResourceModel/RedisReport/Collection.php: -------------------------------------------------------------------------------- 1 | _init(RedisReport::class, RedisReportResourceModel::class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |