├── 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 |
63 |
64 |
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 |
| 54 | 59 | = $escaper->escapeHtml(__('File Name')) ?> 60 | 61 | 62 | | 63 |64 | 69 | = $escaper->escapeHtml(__('Size')) ?> 70 | 71 | 72 | | 73 |74 | 79 | = $escaper->escapeHtml(__('Last Updated')) ?> 80 | 81 | 82 | | 83 |= $escaper->escapeHtml(__('Actions')) ?> | 84 |
|---|---|---|---|
| = $escaper->escapeHtml($fileName) ?> | 91 |= $escaper->escapeHtml($log['size_readable']) ?> | 92 |93 | 94 | = $escaper->escapeHtml($time) ?> 95 | | 96 |97 | 109 | | 110 |
| = $escaper->escapeHtml(__("We couldn't find any records."))?> | 117 ||||