├── composer.json ├── registration.php ├── etc ├── module.xml ├── adminhtml │ ├── routes.xml │ ├── menu.xml │ └── system.xml ├── config.xml └── acl.xml ├── Model ├── Config │ └── Source │ │ ├── Direction.php │ │ ├── SortFields.php │ │ └── ListPerPage.php ├── Validate.php └── FileViewer.php ├── view └── adminhtml │ ├── layout │ ├── logviewer_logfile_index.xml │ └── logviewer_logfile_view.xml │ ├── web │ └── css │ │ └── styles.css │ └── templates │ └── logfile │ ├── grid.phtml │ └── view.phtml ├── Controller └── Adminhtml │ └── Logfile │ ├── Index.php │ ├── LiveUpdate.php │ ├── Download.php │ ├── View.php │ ├── LoadPrevious.php │ └── Delete.php ├── README.md └── Block └── LogFile.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mageprince/module-log-viewer", 3 | "description": "Magento 2 Log Viewer Extension", 4 | "type": "magento2-module", 5 | "version": "1.0.7", 6 | "license": "GPL-3.0-or-later", 7 | "authors": [ 8 | { 9 | "name": "Mageprince", 10 | "email": "support@mageprince.com" 11 | } 12 | ], 13 | "autoload": { 14 | "files": [ 15 | "registration.php" 16 | ], 17 | "psr-4": { 18 | "Mageprince\\LogViewer\\": "" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /etc/adminhtml/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Model/Config/Source/Direction.php: -------------------------------------------------------------------------------- 1 | 'asc', 'label' => 'Ascending'], 37 | ['value' => 'desc', 'label' => 'Descending'] 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Model/Config/Source/SortFields.php: -------------------------------------------------------------------------------- 1 | 'name', 'label' => __('File Name')], 37 | ['value' => 'mod_time', 'label' => __('Last Modified')] 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /view/adminhtml/layout/logviewer_logfile_index.xml: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /view/adminhtml/layout/logviewer_logfile_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Model/Config/Source/ListPerPage.php: -------------------------------------------------------------------------------- 1 | 5, 'label' => '5'], 37 | ['value' => 10, 'label' => '10'], 38 | ['value' => 25, 'label' => '25'], 39 | ['value' => 50, 'label' => '50'], 40 | ['value' => 100, 'label' => '100'] 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 26 | 27 | 1 28 | 500 29 | 10 30 | mod_time 31 | desc 32 | 1 33 | 1 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /etc/adminhtml/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 32 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /etc/acl.xml: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Model/Validate.php: -------------------------------------------------------------------------------- 1 | file = $file; 31 | $this->fileDriver = $fileDriver; 32 | } 33 | 34 | /** 35 | * Check file is valid 36 | * 37 | * @param string $fileName 38 | * @return bool 39 | * @throws FileSystemException 40 | */ 41 | public function validateFile(string $fileName) 42 | { 43 | $logDir = $this->fileDriver->getRealPath(BP . '/var/log') . DIRECTORY_SEPARATOR; 44 | $realPath = $this->fileDriver->getRealPath($logDir . $fileName); 45 | 46 | if ($realPath === false || strpos($realPath, $logDir) !== 0) { 47 | return false; 48 | } 49 | 50 | if (!$this->fileDriver->isFile($realPath)) { 51 | return false; 52 | } 53 | 54 | $allowedExtensions = ['log', 'zip', 'tar', 'gz']; 55 | $pathInfo = $this->file->getPathInfo($realPath); 56 | $extension = strtolower($pathInfo['extension'] ?? ''); 57 | if (!in_array($extension, $allowedExtensions, true)) { 58 | return false; 59 | } 60 | 61 | return true; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Logfile/Index.php: -------------------------------------------------------------------------------- 1 | resultPageFactory = $resultPageFactory; 50 | } 51 | 52 | /** 53 | * Index action 54 | * 55 | * @return \Magento\Framework\View\Result\Page 56 | */ 57 | public function execute() 58 | { 59 | $resultPage = $this->resultPageFactory->create(); 60 | $resultPage->setActiveMenu('Magento_Backend::stores'); 61 | $resultPage->getConfig()->getTitle()->prepend(__('Log Viewer')); 62 | return $resultPage; 63 | } 64 | 65 | /** 66 | * @inheritdoc 67 | */ 68 | protected function _isAllowed() 69 | { 70 | return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magento 2 Log Viewer 2 | 3 | Mageprince Log Viewer is a powerful admin utility that allows you to manage, monitor, and debug log files directly from the Magento Admin Panel — without needing to access the server or filesystem. 4 | 5 | # ✅ Compatibility 6 | 7 | Magento Open Source: 2.3.x - 2.4.x
8 | 9 | # ✨ Key Features 10 | 11 | - View Magento log files (var/log/) directly in the admin panel 12 | - Live log view with auto-refresh 13 | - Display latest log lines with “Load Previous” functionality 14 | - Option to toggle line wrapping for better readability 15 | - Search log files by filename 16 | - Sort logs by filename, or last updated time 17 | - Download or delete log files from admin 18 | - Pagination support for large log directories 19 | - Admin configuration for: 20 | - Enable/disable the extension 21 | - Set number of log lines to show 22 | - Set how many log files to list per page 23 | - Define default sort column and direction 24 | - Restrict allowed file types 25 | - Allow or restrict file deletion 26 | - Allow or restrict file download 27 | 28 | # 🚀 Installation Instructions 29 | 30 | ### 1. Install via composer (Recommended) 31 | 32 | Run the following Magento CLI commands: 33 | 34 | ``` 35 | composer require mageprince/module-log-viewer 36 | php bin/magento setup:upgrade 37 | php bin/magento setup:di:compile 38 | php bin/magento setup:static-content:deploy 39 | ``` 40 | 41 | ### 2. Manual Installation 42 | 43 | Copy the content of the repo to the Magento 2 `app/code/Mageprince/LogViewer` 44 | 45 | Run the following Magento CLI commands: 46 | ``` 47 | php bin/magento setup:upgrade 48 | php bin/magento setup:di:compile 49 | php bin/magento setup:static-content:deploy 50 | ``` 51 | 52 | # 🤝 Contribution 53 | 54 | Want to contribute to this extension? The quickest way is to open a pull request on GitHub. 55 | 56 | # 🛠 Support 57 | 58 | If you encounter any problems or bugs, please open an issue on GitHub. 59 | 60 | # 📸 Screenshots 61 | 62 | image 63 | 1-log-list 64 | 3-admin-config 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Logfile/LiveUpdate.php: -------------------------------------------------------------------------------- 1 | fileViewer = $fileViewer; 46 | parent::__construct($context); 47 | } 48 | 49 | /** 50 | * Live file update action 51 | * 52 | * @return Json 53 | */ 54 | public function execute() 55 | { 56 | if (!$this->getRequest()->isXmlHttpRequest()) { 57 | /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ 58 | $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); 59 | $resultRedirect->setUrl($this->getUrl('logviewer/logfile/index')); 60 | return $resultRedirect; 61 | } 62 | 63 | $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); 64 | $file = $this->getRequest()->getParam('file'); 65 | $lastSize = (int)$this->getRequest()->getParam('last_size', 0); 66 | $logPath = BP . '/var/log/' . $file; 67 | 68 | if (!$this->fileViewer->isReadable($logPath)) { 69 | return $resultJson->setData([ 70 | 'success' => false, 71 | 'message' => __('File not found or not readable') 72 | ]); 73 | } 74 | 75 | $currentSize = $this->fileViewer->getFileSize($logPath); 76 | $newContent = ''; 77 | if ($currentSize > $lastSize) { 78 | $newContent = $this->fileViewer->readFromOffset($logPath, $lastSize); 79 | } 80 | 81 | return $resultJson->setData([ 82 | 'success' => true, 83 | 'new_content' => $newContent, 84 | 'current_size' => $currentSize 85 | ]); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Logfile/Download.php: -------------------------------------------------------------------------------- 1 | fileFactory = $fileFactory; 58 | $this->validate = $validate; 59 | parent::__construct($context); 60 | } 61 | 62 | /** 63 | * Delete action 64 | * 65 | * @return \Magento\Framework\Controller\ResultInterface 66 | */ 67 | public function execute() 68 | { 69 | try { 70 | $fileName = $this->getRequest()->getParam('file'); 71 | $isValid = $this->validate->validateFile($fileName); 72 | if (!$isValid) { 73 | $this->messageManager->addErrorMessage(__('Invalid file')); 74 | return $this->_redirect('logviewer/logfile/index'); 75 | } 76 | 77 | $filePath = 'var/log/'. $fileName; 78 | return $this->fileFactory->create( 79 | $fileName, 80 | [ 81 | 'type' => 'filename', 82 | 'value' => $filePath 83 | ] 84 | ); 85 | } catch (\Exception $e) { 86 | $this->messageManager->addErrorMessage(__('File not found')); 87 | } 88 | return $this->_redirect('logviewer/logfile/index'); 89 | } 90 | 91 | /** 92 | * @inheritdoc 93 | */ 94 | protected function _isAllowed() 95 | { 96 | return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Logfile/View.php: -------------------------------------------------------------------------------- 1 | resultPageFactory = $resultPageFactory; 60 | $this->validate = $validate; 61 | parent::__construct($context); 62 | } 63 | 64 | /** 65 | * View file action 66 | * 67 | * @return ResponseInterface|ResultInterface 68 | */ 69 | public function execute() 70 | { 71 | $resultPage = $this->resultPageFactory->create(); 72 | $resultPage->setActiveMenu('Magento_Backend::stores'); 73 | try { 74 | $fileName = $this->getRequest()->getParam('file'); 75 | $isValid = $this->validate->validateFile($fileName); 76 | if (!$isValid) { 77 | $this->messageManager->addErrorMessage(__('Invalid file')); 78 | return $this->_redirect('logviewer/logfile/index'); 79 | } 80 | $resultPage->getConfig()->getTitle()->prepend(__('Log Viewer (%1)', $fileName)); 81 | return $resultPage; 82 | } catch (\Exception $e) { 83 | $this->messageManager->addErrorMessage(__('File not found')); 84 | } 85 | $this->_redirect('logviewer/logfile/index'); 86 | } 87 | 88 | /** 89 | * @inheritdoc 90 | */ 91 | protected function _isAllowed() 92 | { 93 | return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Logfile/LoadPrevious.php: -------------------------------------------------------------------------------- 1 | jsonFactory = $jsonFactory; 59 | $this->fileViewer = $fileViewer; 60 | parent::__construct($context); 61 | } 62 | 63 | /** 64 | * Load logs action 65 | * 66 | * @return Json 67 | */ 68 | public function execute() 69 | { 70 | if (!$this->getRequest()->isXmlHttpRequest()) { 71 | /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ 72 | $resultRedirect = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT); 73 | $resultRedirect->setUrl($this->getUrl('logviewer/logfile/index')); 74 | return $resultRedirect; 75 | } 76 | 77 | $result = $this->jsonFactory->create(); 78 | 79 | $file = $this->getRequest()->getParam('file'); 80 | $offset = (int) $this->getRequest()->getParam('offset'); 81 | $lines = (int) $this->getRequest()->getParam('lines'); 82 | $filePath = BP . '/var/log/' . $file; 83 | 84 | $data = $this->fileViewer->tailFile($filePath, $lines, $offset); 85 | $hasMore = $this->fileViewer->hasMoreDataToLoad($filePath, $data, $lines, $offset); 86 | 87 | return $result->setData([ 88 | 'success' => true, 89 | 'data' => $data, 90 | 'has_more' => $hasMore 91 | ]); 92 | } 93 | 94 | /** 95 | * @inheritdoc 96 | */ 97 | protected function _isAllowed() 98 | { 99 | return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Logfile/Delete.php: -------------------------------------------------------------------------------- 1 | fileFactory = $fileFactory; 75 | $this->logFile = $logFile; 76 | $this->driver = $driver; 77 | $this->validate = $validate; 78 | parent::__construct($context); 79 | } 80 | 81 | /** 82 | * Delete action 83 | * 84 | * @return void 85 | */ 86 | public function execute() 87 | { 88 | try { 89 | $fileName = $this->getRequest()->getParam('file'); 90 | $isValid = $this->validate->validateFile($fileName); 91 | if (!$isValid) { 92 | $this->messageManager->addErrorMessage(__('Invalid file')); 93 | return $this->_redirect('logviewer/logfile/index'); 94 | } 95 | 96 | $file = BP . '/var/log/' . $fileName; 97 | $fp = $this->driver->fileOpen($file, "r+"); 98 | ftruncate($fp, 0); 99 | $this->driver->fileClose($fp); 100 | $this->messageManager->addSuccessMessage(__('File content of %1 has been deleted', $fileName)); 101 | return $this->_redirect('logviewer/logfile/view', ['file' => $fileName]); 102 | } catch (\Exception $e) { 103 | $this->messageManager->addErrorMessage(__('File not found')); 104 | } 105 | return $this->_redirect('logviewer/logfile/index'); 106 | } 107 | 108 | /** 109 | * @inheritdoc 110 | */ 111 | protected function _isAllowed() 112 | { 113 | return $this->_authorization->isAllowed(self::ADMIN_RESOURCE); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /view/adminhtml/web/css/styles.css: -------------------------------------------------------------------------------- 1 | .logviewer-grid .action-buttons a{ 2 | border-radius: 4px; 3 | } 4 | .logviewer-grid .data-grid-th a{ 5 | color: #ffffff; 6 | } 7 | .logviewer-grid .pagination { 8 | margin-top: 20px; 9 | } 10 | .logviewer-grid .pagination a { 11 | padding: 6px 12px; 12 | margin-right: 5px; 13 | border: 1px solid #ccc; 14 | color: #514943; 15 | text-decoration: none; 16 | } 17 | .logviewer-grid .pagination a.active { 18 | background-color: #514943; 19 | color: #fff; 20 | border-color: #514943; 21 | } 22 | .logviewer-grid input[type="text"] { 23 | padding: 5px; 24 | width: 250px; 25 | } 26 | .logviewer-grid .search-filename { 27 | margin-bottom: 10px; 28 | } 29 | .logviewer-grid .sort-indicator::after { 30 | content: ''; 31 | display: inline-block; 32 | margin-left: 4px; 33 | border: 4px solid transparent; 34 | vertical-align: middle; 35 | } 36 | 37 | .logviewer-grid .sort-indicator.asc::after { 38 | border-bottom-color: #fff; 39 | } 40 | 41 | .logviewer-grid .sort-indicator.desc::after { 42 | border-top-color: #fff; 43 | } 44 | .log-container { 45 | position: relative; 46 | background-color: #f9f9f9; 47 | border: 1px solid #ccc; 48 | padding: 20px; 49 | border-radius: 8px; 50 | } 51 | 52 | .log-container #log-output { 53 | width: 100%; 54 | height: auto; 55 | min-height: 600px; 56 | resize: vertical; 57 | background-color: #1e1e1e; 58 | color: #dcdcdc; 59 | line-height: 1.5; 60 | border: none; 61 | padding: 15px; 62 | border-radius: 6px; 63 | overflow-y: auto; 64 | white-space: pre; 65 | } 66 | 67 | .log-container a, 68 | .log-container button { 69 | margin: 0 10px 15px 0; 70 | padding: 10px 15px; 71 | border-radius: 4px; 72 | } 73 | 74 | .log-container a.action-secondary.delete { 75 | background-color: #e53935; 76 | color: #fff; 77 | border: 1px solid #d32f2f; 78 | } 79 | .live-log-status { 80 | background: #e8f5e8; 81 | padding: 8px 12px; 82 | border-radius: 4px; 83 | margin-bottom: 10px; 84 | border-left: 3px solid #4caf50; 85 | display: flex; 86 | align-items: center; 87 | font-size: 16px; 88 | } 89 | .live-log-status.active { 90 | background: #e8f5e8; 91 | color: #388e3c; 92 | border-left: 3px solid #4caf50; 93 | font-weight: 600; 94 | } 95 | .live-log-status .status-indicator { 96 | display: inline-block; 97 | width: 10px; 98 | height: 10px; 99 | background: #4caf50; 100 | border-radius: 50%; 101 | margin-right: 8px; 102 | animation: logviewer-pulse 2s infinite; 103 | } 104 | @keyframes logviewer-pulse { 105 | 0% { opacity: 1; } 106 | 50% { opacity: 0.5; } 107 | 100% { opacity: 1; } 108 | } 109 | .log-container .live-log-btn.active { 110 | background-color: #4caf50; 111 | color: #fff; 112 | border: 2px solid #388e3c; 113 | box-shadow: 0 2px 8px rgba(76,175,80,0.18); 114 | } 115 | .log-container .live-log-btn.active:hover { 116 | background-color: #388e3c; 117 | color: #fff; 118 | } 119 | .log-container .log-actions .icon-live-log-play svg, 120 | .log-container .log-actions .icon-live-log-pause svg { 121 | vertical-align: top; 122 | } 123 | 124 | .log-container .live-log-btn-text { 125 | vertical-align: top; 126 | } 127 | .log-container .wrap-lines-btn { 128 | background: #fff; 129 | margin-bottom: 0 !important; 130 | line-height: 10px; 131 | vertical-align: top; 132 | } 133 | .log-container .wrap-lines-btn:hover, 134 | .log-container .wrap-lines-btn:focus { 135 | background: #e0e0e0; 136 | color: #222; 137 | border-color: #514943; 138 | box-shadow: 0 2px 8px rgba(81,73,67,0.08); 139 | } 140 | .log-container .wrap-lines-btn.active { 141 | background: #4caf50; 142 | color: #fff; 143 | border: 1.5px solid #388e3c; 144 | box-shadow: 0 2px 8px rgba(76,175,80,0.18); 145 | } 146 | .log-container .wrap-lines-btn.active:hover, 147 | .log-container .wrap-lines-btn.active:focus { 148 | background: #388e3c; 149 | color: #fff; 150 | border-color: #388e3c; 151 | } 152 | -------------------------------------------------------------------------------- /Model/FileViewer.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 32 | $this->logger = $logger; 33 | } 34 | 35 | /** 36 | * Tail log file 37 | * 38 | * @param string $filePath 39 | * @param int $lines 40 | * @param int $offset 41 | * @return string 42 | */ 43 | public function tailFile($filePath, $lines, $offset = 0) 44 | { 45 | $output = []; 46 | 47 | try { 48 | if ($this->isReadable($filePath)) { 49 | $fp = $this->driver->fileOpen($filePath, 'rb'); 50 | if ($fp === false) { 51 | return ''; 52 | } 53 | 54 | $this->driver->fileSeek($fp, 0, SEEK_END); 55 | $position = $this->driver->fileTell($fp); 56 | $chunk = ''; 57 | $lineCount = 0; 58 | $buffer = 4096; 59 | $needed = $offset + $lines; 60 | 61 | while ($position > 0 && $lineCount <= $needed) { 62 | $readSize = ($position - $buffer > 0) ? $buffer : $position; 63 | $position -= $readSize; 64 | $this->driver->fileSeek($fp, $position); 65 | 66 | $chunk = $this->driver->fileRead($fp, $readSize) . $chunk; 67 | $lineCount = substr_count($chunk, "\n"); 68 | } 69 | 70 | $this->driver->fileClose($fp); 71 | 72 | $linesArray = explode("\n", $chunk); 73 | $slice = array_slice($linesArray, -$needed, $lines); 74 | $output = $slice; 75 | } 76 | } catch (\Exception $e) { 77 | $this->logger->error($e->getMessage()); 78 | } 79 | 80 | return implode("\n", $output); 81 | } 82 | 83 | /** 84 | * Check if file has more data to load 85 | * 86 | * @param string $filePath 87 | * @param string $data 88 | * @param int $lines 89 | * @param int $offset 90 | * @return bool 91 | */ 92 | public function hasMoreDataToLoad($filePath, $data, $lines, $offset) 93 | { 94 | $file = $this->driver->fileOpen($filePath, 'rb'); 95 | $this->driver->fileSeek($file, 0, SEEK_END); 96 | $fileSize = $this->driver->fileTell($file); 97 | 98 | $this->driver->fileClose($file); 99 | $avgLineLength = max(strlen($data) / max($lines, 1), 1); 100 | $estimatedTotal = (int)($fileSize / $avgLineLength); 101 | 102 | return ($offset + $lines) < $estimatedTotal; 103 | } 104 | 105 | /** 106 | * Read file content from a given offset to the end 107 | * 108 | * @param string $filePath 109 | * @param int $offset 110 | * @return string 111 | */ 112 | public function readFromOffset($filePath, $offset) 113 | { 114 | $content = ''; 115 | try { 116 | if ($this->isReadable($filePath)) { 117 | $fp = $this->driver->fileOpen($filePath, 'rb'); 118 | if ($fp === false) { 119 | return ''; 120 | } 121 | $this->driver->fileSeek($fp, $offset); 122 | $content = $this->driver->fileRead($fp, $this->getFileSize($filePath) - $offset); 123 | $this->driver->fileClose($fp); 124 | } 125 | } catch (\Exception $e) { 126 | $this->logger->error($e->getMessage()); 127 | } 128 | return $content; 129 | } 130 | 131 | /** 132 | * Check is file is readable 133 | * 134 | * @param string $fileName 135 | * @return bool 136 | * @throws FileSystemException 137 | */ 138 | public function isReadable($fileName) 139 | { 140 | return $this->driver->isReadable($fileName); 141 | } 142 | 143 | /** 144 | * Retrieve file size 145 | * 146 | * @param string $filePath 147 | * @return mixed 148 | * @throws FileSystemException 149 | */ 150 | public function getFileSize($filePath) 151 | { 152 | return $this->driver->stat($filePath)['size']; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | mageprince 31 | Mageprince_LogViewer::log_viewer_settings 32 | 33 | 34 | 35 | 36 | Magento\Config\Model\Config\Source\Yesno 37 | 38 | 39 | 40 | Number of lines to display from the bottom of the log file (e.g. 500) 41 | validate-number validate-greater-than-zero 42 | 43 | 1 44 | 45 | 46 | 47 | 48 | Mageprince\LogViewer\Model\Config\Source\ListPerPage 49 | Number of logs display on log list page 50 | 51 | 1 52 | 53 | 54 | 55 | 56 | Mageprince\LogViewer\Model\Config\Source\SortFields 57 | 58 | 1 59 | 60 | 61 | 62 | 63 | Mageprince\LogViewer\Model\Config\Source\Direction 64 | 65 | 1 66 | 67 | 68 | 69 | 70 | Comma-separated (e.g. .log,.txt). Leave it blank to allow all files 71 | 72 | 1 73 | 74 | 75 | 76 | 77 | Magento\Config\Model\Config\Source\Yesno 78 | Allow file cleanup on log view page 79 | 80 | 1 81 | 82 | 83 | 84 | 85 | Magento\Config\Model\Config\Source\Yesno 86 | 87 | 1 88 | 89 | 90 | 91 |
92 |
93 |
94 | -------------------------------------------------------------------------------- /view/adminhtml/templates/logfile/grid.phtml: -------------------------------------------------------------------------------- 1 | getLogFiles(); 31 | $items = $logs['items']; 32 | $page = $logs['page']; 33 | $totalPages = $logs['totalPages']; 34 | $search = $logs['search']; 35 | ?> 36 |
37 |
38 |
39 | 42 | 43 |
44 |
45 | 46 | 47 | getDefaultSortColumn(); 52 | ?> 53 | 63 | 73 | 83 | 84 | 85 | 0): ?> 86 | 87 | 88 | 89 | 90 | 91 | 92 | 96 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |
54 | 59 | escapeHtml(__('File Name')) ?> 60 | 61 | 62 | 64 | 69 | escapeHtml(__('Size')) ?> 70 | 71 | 72 | 74 | 79 | escapeHtml(__('Last Updated')) ?> 80 | 81 | 82 | escapeHtml(__('Actions')) ?>
escapeHtml($fileName) ?>escapeHtml($log['size_readable']) ?> 93 | 94 | escapeHtml($time) ?> 95 | 97 | 109 |
escapeHtml(__("We couldn't find any records."))?>
120 | showPagination($logs)): ?> 121 | 133 | 134 |
135 | -------------------------------------------------------------------------------- /Block/LogFile.php: -------------------------------------------------------------------------------- 1 | fileViewer = $fileViewer; 47 | parent::__construct($context, $data); 48 | } 49 | 50 | /** 51 | * Retrieve download log file url 52 | * 53 | * @param string $fileName 54 | * @return string 55 | */ 56 | public function getDownloadLogFileUrl($fileName) 57 | { 58 | return $this->getUrl('logviewer/logfile/download', ['file' => $fileName]); 59 | } 60 | 61 | /** 62 | * Retrieve view log file url 63 | * 64 | * @param string $fileName 65 | * @return string 66 | */ 67 | public function getViewLogFileUrl($fileName) 68 | { 69 | return $this->getUrl('logviewer/logfile/view', ['file' => $fileName]); 70 | } 71 | 72 | /** 73 | * Retrieve delete log file url 74 | * 75 | * @param string $fileName 76 | * @return string 77 | */ 78 | public function getDeleteLogFile($fileName) 79 | { 80 | return $this->getUrl('logviewer/logfile/delete', ['file' => $fileName]); 81 | } 82 | 83 | /** 84 | * Retrieve load previous log url 85 | * 86 | * @return string 87 | */ 88 | public function getLoadPreviousLogUrl() 89 | { 90 | return $this->getUrl('logviewer/logfile/loadprevious') . '?isAjax=true'; 91 | } 92 | 93 | /** 94 | * Retrieve live log update url 95 | * 96 | * @return string 97 | */ 98 | public function getLiveLogUrl() 99 | { 100 | return $this->getUrl('logviewer/logfile/liveupdate'); 101 | } 102 | 103 | /** 104 | * Retrieve file name 105 | * 106 | * @return string 107 | */ 108 | public function getFileName() 109 | { 110 | return $this->getRequest()->getParam('file'); 111 | } 112 | 113 | /** 114 | * Retrieve file size 115 | * 116 | * @param string $bytes 117 | * @param string $precision 118 | * @return string 119 | */ 120 | protected function filesizeToReadableString($bytes, $precision = 2) 121 | { 122 | $units = ['B', 'KB', 'MB', 'GB', 'TB']; 123 | $bytes = max($bytes, 0); 124 | $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); 125 | $pow = min($pow, count($units) - 1); 126 | $bytes /= pow(1024, $pow); 127 | return round($bytes, $precision) . ' ' . $units[$pow]; 128 | } 129 | 130 | /** 131 | * Retrieve log files 132 | * 133 | * @return array 134 | */ 135 | public function getLogFiles() 136 | { 137 | $page = (int) $this->getRequest()->getParam('page', 1); 138 | $limit = $this->getItemsPerPageCount(); 139 | $search = trim((string) $this->getRequest()->getParam('q', '')); 140 | $defaultSortColumn = $this->getDefaultSortColumn(); 141 | $sort = $this->getRequest()->getParam('sort', $defaultSortColumn); 142 | $defaultSortDirection = $this->getDefaultSortDirection(); 143 | $direction = strtolower($this->getRequest()->getParam('dir', $defaultSortDirection)); 144 | 145 | $path = BP . '/var/log/'; 146 | $files = []; 147 | $allowedFileExtensions = $this->getAllowedFileExtensions(); 148 | 149 | $iterator = new \RecursiveIteratorIterator( 150 | new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS) 151 | ); 152 | foreach ($iterator as $fileInfo) { 153 | if ($fileInfo->isFile()) { 154 | $filePath = $fileInfo->getPathname(); 155 | $relativePath = ltrim(str_replace($path, '', $filePath), '/'); 156 | $fileName = $fileInfo->getFilename(); 157 | 158 | if ($search && stripos($relativePath, $search) === false) { 159 | continue; 160 | } 161 | 162 | if ($allowedFileExtensions) { 163 | $extension = strtolower(strrchr($fileName, '.')); 164 | if (!in_array($extension, $allowedFileExtensions, true)) { 165 | continue; 166 | } 167 | } 168 | 169 | $files[] = [ 170 | 'name' => $relativePath, 171 | 'size' => $fileInfo->getSize(), 172 | 'size_readable' => $this->filesizeToReadableString($fileInfo->getSize()), 173 | 'mod_time' => $fileInfo->getMTime(), 174 | 'mod_time_full' => date("F d Y H:i:s.", $fileInfo->getMTime()), 175 | ]; 176 | } 177 | } 178 | 179 | usort($files, function ($a, $b) use ($sort, $direction) { 180 | $result = $a[$sort] <=> $b[$sort]; 181 | return $direction === 'desc' ? -$result : $result; 182 | }); 183 | 184 | $total = count($files); 185 | $totalPages = ceil($total / $limit); 186 | $start = ($page - 1) * $limit; 187 | 188 | return [ 189 | 'items' => array_slice($files, $start, $limit), 190 | 'page' => $page, 191 | 'total' => $total, 192 | 'totalPages' => $totalPages, 193 | 'search' => $search, 194 | 'sort' => $sort, 195 | 'dir' => $direction, 196 | ]; 197 | } 198 | 199 | /** 200 | * Retrieve file content 201 | * 202 | * @param string $filePath 203 | * @param int $lines 204 | * @return string 205 | */ 206 | public function tailFile($filePath, $lines) 207 | { 208 | return $this->fileViewer->tailFile($filePath, $lines); 209 | } 210 | 211 | /** 212 | * Check if show pagination 213 | * 214 | * @param array $logs 215 | * @return bool 216 | */ 217 | public function showPagination($logs) 218 | { 219 | $isShow = false; 220 | $limit = $this->getItemsPerPageCount(); 221 | if ($logs['total'] > $limit) { 222 | $isShow = true; 223 | } 224 | return $isShow; 225 | } 226 | 227 | /** 228 | * Retrieve item per page count 229 | * 230 | * @return int 231 | */ 232 | public function getItemsPerPageCount() 233 | { 234 | return (int)$this->_scopeConfig->getValue('log_viewer/general/items_per_page'); 235 | } 236 | 237 | /** 238 | * Retrieve lines per page count 239 | * 240 | * @return int 241 | */ 242 | public function getLinesToShowPerPageCount() 243 | { 244 | return (int)$this->_scopeConfig->getValue('log_viewer/general/lines_to_show'); 245 | } 246 | 247 | /** 248 | * Retrieve default sort column 249 | * 250 | * @return string 251 | */ 252 | public function getDefaultSortColumn() 253 | { 254 | return $this->_scopeConfig->getValue('log_viewer/general/default_sort_column'); 255 | } 256 | 257 | /** 258 | * Retrieve default sort direction 259 | * 260 | * @return string 261 | */ 262 | public function getDefaultSortDirection() 263 | { 264 | return $this->_scopeConfig->getValue('log_viewer/general/default_sort_dir'); 265 | } 266 | 267 | /** 268 | * Retrieve allowed log file extensions 269 | * 270 | * @return array 271 | */ 272 | public function getAllowedFileExtensions() 273 | { 274 | $extensions = []; 275 | $allowedExtensions = $this->_scopeConfig->getValue('log_viewer/general/allowed_extensions'); 276 | if ($allowedExtensions) { 277 | $extensions = explode(',', $allowedExtensions); 278 | } 279 | return $extensions; 280 | } 281 | 282 | /** 283 | * Check is log can download 284 | * 285 | * @return bool 286 | */ 287 | public function isDownloadAllowed() 288 | { 289 | return $this->_scopeConfig->isSetFlag('log_viewer/general/allow_download'); 290 | } 291 | 292 | /** 293 | * Check is log can delete 294 | * 295 | * @return bool 296 | */ 297 | public function isDeleteAllowed() 298 | { 299 | return $this->_scopeConfig->isSetFlag('log_viewer/general/allow_delete'); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /view/adminhtml/templates/logfile/view.phtml: -------------------------------------------------------------------------------- 1 | getFileName(); 29 | $displayLines = $block->getLinesToShowPerPageCount(); 30 | $logPath = BP . '/var/log/' . $logFile; 31 | $previousLogUrl = $block->getLoadPreviousLogUrl(); 32 | ?> 33 | 39 |
40 |
41 | 54 | 64 | 65 | escapeHtml(__('Go Back')) ?> 66 | 67 |
68 |
69 | 75 | 76 | isDeleteAllowed()): ?> 77 | 82 | 83 | 84 | 85 |
86 | 87 |
88 | 203 | --------------------------------------------------------------------------------