From 9368f5445dda8b63133c6051aa819f641a0ee61d Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Sat, 27 Mar 2021 18:28:09 +0100 Subject: [PATCH 01/12] Display structured logs in admin Tries to parse log lines and to display info in a table. Additional JSON data is parsed and displayed clicking on a row. File reading and line parsing is handled in iterators, to avoid to keep too much data in memory. Search and filter should be trivial to add. Log file is read backward to display log events newest first. A "tail" functionality should be easy to implement. --- src/Model/Log/ParsedLogIterator.php | 81 ++++++++++++++++++++ src/Module/Admin/Logs/View.php | 39 ++++------ src/Object/Log/ParsedLog.php | 114 ++++++++++++++++++++++++++++ src/Util/ReversedFileReader.php | 101 ++++++++++++++++++++++++ view/js/module/admin/logs/view.js | 7 ++ view/templates/admin/logs/view.tpl | 44 ++++++++++- 6 files changed, 361 insertions(+), 25 deletions(-) create mode 100644 src/Model/Log/ParsedLogIterator.php create mode 100644 src/Object/Log/ParsedLog.php create mode 100644 src/Util/ReversedFileReader.php create mode 100644 view/js/module/admin/logs/view.js diff --git a/src/Model/Log/ParsedLogIterator.php b/src/Model/Log/ParsedLogIterator.php new file mode 100644 index 000000000..621381ac5 --- /dev/null +++ b/src/Model/Log/ParsedLogIterator.php @@ -0,0 +1,81 @@ +. + * + */ +namespace Friendica\Model\Log; + +use \Friendica\Util\ReversedFileReader; +use \Friendica\Object\Log\ParsedLog; + + +/** + * An iterator which returns `\Friendica\Objec\Log\ParsedLog` instances + * + * Uses `\Friendica\Util\ReversedFileReader` to fetch log lines + * from newest to oldest + */ +class ParsedLogIterator implements \Iterator +{ + public function __construct(string $filename, int $limit=0) + { + $this->reader = new ReversedFileReader($filename); + $this->_value = null; + $this->_limit = $limit; + } + + public function next() + { + $this->reader->next(); + if ($this->_limit > 0 && $this->reader->key() > $this->_limit) { + $this->_value = null; + return; + } + if ($this->reader->valid()) { + $line = $this->reader->current(); + $this->_value = new ParsedLog($this->reader->key(), $line); + } else { + $this->_value = null; + } + } + + + public function rewind() + { + $this->_value = null; + $this->reader->rewind(); + $this->next(); + } + + public function key() + { + return $this->reader->key(); + } + + public function current() + { + return $this->_value; + } + + public function valid() + { + return ! is_null($this->_value); + } + +} + diff --git a/src/Module/Admin/Logs/View.php b/src/Module/Admin/Logs/View.php index 91e8f2dd8..339a28b6a 100644 --- a/src/Module/Admin/Logs/View.php +++ b/src/Module/Admin/Logs/View.php @@ -21,49 +21,42 @@ namespace Friendica\Module\Admin\Logs; -use Friendica\Core\Renderer; use Friendica\DI; +use Friendica\Core\Renderer; +use Friendica\Core\Theme; use Friendica\Module\BaseAdmin; -use Friendica\Util\Strings; +use Friendica\Model\Log\ParsedLogIterator; class View extends BaseAdmin { + const LIMIT = 500; + public static function content(array $parameters = []) { parent::content($parameters); $t = Renderer::getMarkupTemplate('admin/logs/view.tpl'); + DI::page()->registerFooterScript(Theme::getPathForFile('js/module/admin/logs/view.js')); + $f = DI::config()->get('system', 'logfile'); - $data = ''; + $data = null; + $error = null; + if (!file_exists($f)) { - $data = DI::l10n()->t('Error trying to open %1$s log file.\r\n
Check to see if file %1$s exist and is readable.', $f); + $error = DI::l10n()->t('Error trying to open %1$s log file.\r\n
Check to see if file %1$s exist and is readable.', $f); } else { - $fp = fopen($f, 'r'); - if (!$fp) { - $data = DI::l10n()->t('Couldn\'t open %1$s log file.\r\n
Check to see if file %1$s is readable.', $f); - } else { - $fstat = fstat($fp); - $size = $fstat['size']; - if ($size != 0) { - if ($size > 5000000 || $size < 0) { - $size = 5000000; - } - $seek = fseek($fp, 0 - $size, SEEK_END); - if ($seek === 0) { - $data = Strings::escapeHtml(fread($fp, $size)); - while (!feof($fp)) { - $data .= Strings::escapeHtml(fread($fp, 4096)); - } - } - } - fclose($fp); + try { + $data = new ParsedLogIterator($f, self::LIMIT); + } catch (Exception $e) { + $error = DI::l10n()->t('Couldn\'t open %1$s log file.\r\n
Check to see if file %1$s is readable.', $f); } } return Renderer::replaceMacros($t, [ '$title' => DI::l10n()->t('Administration'), '$page' => DI::l10n()->t('View Logs'), '$data' => $data, + '$error' => $error, '$logname' => DI::config()->get('system', 'logfile') ]); } diff --git a/src/Object/Log/ParsedLog.php b/src/Object/Log/ParsedLog.php new file mode 100644 index 000000000..21bd53876 --- /dev/null +++ b/src/Object/Log/ParsedLog.php @@ -0,0 +1,114 @@ +. + * + */ +namespace Friendica\Object\Log; + +/** + * Parse a log line and offer some utility methods + */ +class ParsedLog +{ + const REGEXP = '/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^ ]*) (\w+) \[(\w*)\]: (.*)/'; + + public $id = 0; + public $date = null; + public $context = null; + public $level = null; + public $message = null; + public $data = null; + public $source = null; + + /** + * @param string $logline Source log line to parse + */ + public function __construct(int $id, string $logline) + { + $this->id = $id; + $this->parse($logline); + $this->stop = false; + } + + private function parse($logline) + { + list($logline, $jsonsource) = explode(' - ', $logline); + $jsondata = null; + if (strpos($logline, '{"') > 0) { + list($logline, $jsondata) = explode('{"', $logline, 2); + $jsondata = '{"' . $jsondata; + } + preg_match(self::REGEXP, $logline, $matches); + $this->date = $matches[1]; + $this->context = $matches[2]; + $this->level = $matches[3]; + $this->message = $matches[4]; + $this->data = $jsondata; + $this->source = $jsonsource; + $this->try_fix_json('data'); + } + + /** + * In log boundary between message and json data is not specified. + * If message contains '{' the parser thinks there starts the json data. + * This method try to parse the found json and if it fails, search for next '{' + * in json data and retry + */ + private function try_fix_json(string $key) + { + if (is_null($this->$key) || $this->$key == "") { + return; + } + try { + $d = json_decode($this->$key, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + // try to find next { in $str and move string before to 'message' + + $pos = strpos($this->$key, '{', 1); + + $this->message .= substr($this->$key, 0, $pos); + $this->$key = substr($this->key, $pos); + $this->try_fix_json($key); + } + } + + /** + * Return decoded `data` as array suitable for template + * + * @return array + */ + public function get_data() { + $data = json_decode($this->data, true); + if ($data) { + foreach($data as $k => $v) { + $v = print_r($v, true); + $data[$k] = $v; + } + } + return $data; + } + + /** + * Return decoded `source` as array suitable for template + * + * @return array + */ + public function get_source() { + return json_decode($this->source, true); + } +} diff --git a/src/Util/ReversedFileReader.php b/src/Util/ReversedFileReader.php new file mode 100644 index 000000000..8a3083f7d --- /dev/null +++ b/src/Util/ReversedFileReader.php @@ -0,0 +1,101 @@ +. + * + */ + +namespace Friendica\Util; + + +/** + * An iterator which returns lines from file in reversed order + * + * original code https://stackoverflow.com/a/10494801 + */ +class ReversedFileReader implements \Iterator +{ + const BUFFER_SIZE = 4096; + const SEPARATOR = "\n"; + + public function __construct($filename) + { + $this->_fh = fopen($filename, 'r'); + if (!$this->_fh) { + // this should use a custom exception. + throw \Exception("Unable to open $filename"); + } + $this->_filesize = filesize($filename); + $this->_pos = -1; + $this->_buffer = null; + $this->_key = -1; + $this->_value = null; + } + + public function _read($size) + { + $this->_pos -= $size; + fseek($this->_fh, $this->_pos); + return fread($this->_fh, $size); + } + + public function _readline() + { + $buffer =& $this->_buffer; + while (true) { + if ($this->_pos == 0) { + return array_pop($buffer); + } + if (count($buffer) > 1) { + return array_pop($buffer); + } + $buffer = explode(self::SEPARATOR, $this->_read(self::BUFFER_SIZE) . $buffer[0]); + } + } + + public function next() + { + ++$this->_key; + $this->_value = $this->_readline(); + } + + public function rewind() + { + if ($this->_filesize > 0) { + $this->_pos = $this->_filesize; + $this->_value = null; + $this->_key = -1; + $this->_buffer = explode(self::SEPARATOR, $this->_read($this->_filesize % self::BUFFER_SIZE ?: self::BUFFER_SIZE)); + $this->next(); + } + } + + public function key() + { + return $this->_key; + } + + public function current() + { + return $this->_value; + } + + public function valid() + { + return ! is_null($this->_value); + } +} diff --git a/view/js/module/admin/logs/view.js b/view/js/module/admin/logs/view.js new file mode 100644 index 000000000..45d08a5ed --- /dev/null +++ b/view/js/module/admin/logs/view.js @@ -0,0 +1,7 @@ +function log_show_details(id) { + document + .querySelectorAll('[data-id="' + id + '"]') + .forEach(elm => { + elm.classList.toggle('hidden') + }); +} diff --git a/view/templates/admin/logs/view.tpl b/view/templates/admin/logs/view.tpl index 9ac5acd9d..166dea0ba 100644 --- a/view/templates/admin/logs/view.tpl +++ b/view/templates/admin/logs/view.tpl @@ -1,6 +1,46 @@

{{$title}} - {{$page}}

- +

{{$logname}}

-
{{$data}}
+ {{if $error }} +
+

{{$error nofilter}}

+
+ {{else}} + + + + + + + + + + + {{foreach $data as $row}} + + + + + + + {{foreach $row->get_data() as $k=>$v}} + + + + + {{/foreach}} + + {{foreach $row->get_source() as $k=>$v}} + + + + + {{/foreach}} + {{/foreach}} + +
DateLevelContextMessage
{{$row->date}}{{$row->level}}{{$row->context}}{{$row->message}}
+ {{/if}}
From b8fc6a8c023dc6808362e38179d7ee9338802ca7 Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Sat, 27 Mar 2021 18:36:55 +0100 Subject: [PATCH 02/12] Advanced log view: Frio version Show log event details in a modal dialog --- view/theme/frio/css/style.css | 14 +++ view/theme/frio/js/module/admin/logs/view.js | 41 ++++++++ view/theme/frio/templates/admin/logs/view.tpl | 93 +++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 view/theme/frio/js/module/admin/logs/view.js create mode 100755 view/theme/frio/templates/admin/logs/view.tpl diff --git a/view/theme/frio/css/style.css b/view/theme/frio/css/style.css index ba15534d5..1bf908e1d 100644 --- a/view/theme/frio/css/style.css +++ b/view/theme/frio/css/style.css @@ -84,6 +84,20 @@ blockquote { overflow: hidden !important; } +/** + * details tag + */ +details { + padding: .5em .5em 0; +} +details details { + padding-left: .5em; +} +details summary { + font-weight: bold; + display: list-item; +} + /** * mobile aside */ diff --git a/view/theme/frio/js/module/admin/logs/view.js b/view/theme/frio/js/module/admin/logs/view.js new file mode 100644 index 000000000..fc19af28a --- /dev/null +++ b/view/theme/frio/js/module/admin/logs/view.js @@ -0,0 +1,41 @@ +$(function(){ + $(".log-event").on("click", function(ev) { + var $modal = $("#logdetail"); + var tr = $modal.find(".main-data tbody tr")[0]; + tr.innerHTML = ev.currentTarget.innerHTML; + + var data = JSON.parse(ev.currentTarget.dataset.source); + $modal.find(".source-data td").each(function(i,elm){ + var k = elm.dataset.value; + elm.innerText = data[k]; + }); + + var elm = $modal.find(".event-data")[0]; + elm.innerHTML = ""; + var data = ev.currentTarget.dataset.data; + if (data !== "") { + elm.innerHTML = "

Data

"; + data = JSON.parse(data); + elm.innerHTML += recursive_details("", data); + } + + $modal.modal({}) + }) + + function recursive_details(s, data, lev=0) { + for(var k in data) { + if (data.hasOwnProperty(k)) { + var v = data[k]; + var open = lev > 1 ? "" : "open"; + s += "
" + k + ""; + if (typeof v === 'object' && v !== null) { + s = recursive_details(s, v, lev+1); + } else { + s += $("
").text(v)[0].outerHTML;
+				}
+				s += "
"; + } + } + return s; + } +}); diff --git a/view/theme/frio/templates/admin/logs/view.tpl b/view/theme/frio/templates/admin/logs/view.tpl new file mode 100755 index 000000000..99a96befd --- /dev/null +++ b/view/theme/frio/templates/admin/logs/view.tpl @@ -0,0 +1,93 @@ +
+

{{$title}} - {{$page}}

+ +

{{$logname}}

+ {{if $error }} +
+

{{$error nofilter}}

+
+ {{else}} + + + + + + + + + + + {{foreach $data as $row}} + + + + + + + {{/foreach}} + +
DateLevelContextMessage
{{$row->date}}level == "ERROR"}}bg-danger + {{elseif $row->level == "WARNING"}}bg-warinig + {{elseif $row->level == "NOTICE"}}bg-info + {{elseif $row->level == "DEBUG"}}text-muted + {{/if}} + ">{{$row->level}}{{$row->context}}{{$row->message}}
+ {{/if}} +
+ + From 5b9aeeeca95165203ed2753ba44ce6d8fbdea321 Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Mon, 24 May 2021 21:47:10 +0200 Subject: [PATCH 03/12] Add search and filter to log view --- src/Model/Log/ParsedLogIterator.php | 115 +++++++++++++++++++++++----- src/Module/Admin/Logs/View.php | 31 +++++++- src/Object/Log/ParsedLog.php | 38 ++++++--- src/Util/ReversedFileReader.php | 53 ++++++++----- view/templates/admin/logs/view.tpl | 92 ++++++++++++++-------- 5 files changed, 248 insertions(+), 81 deletions(-) diff --git a/src/Model/Log/ParsedLogIterator.php b/src/Model/Log/ParsedLogIterator.php index 621381ac5..15714454e 100644 --- a/src/Model/Log/ParsedLogIterator.php +++ b/src/Model/Log/ParsedLogIterator.php @@ -28,36 +28,113 @@ use \Friendica\Object\Log\ParsedLog; * An iterator which returns `\Friendica\Objec\Log\ParsedLog` instances * * Uses `\Friendica\Util\ReversedFileReader` to fetch log lines - * from newest to oldest + * from newest to oldest. */ class ParsedLogIterator implements \Iterator { - public function __construct(string $filename, int $limit=0) + /** @var \Iterator */ + private $reader; + + /** @var ParsedLog current iterator value*/ + private $value; + + /** @var int max number of lines to read */ + private $limit; + + /** @var array filters per column */ + private $filters; + + /** @var string search term */ + private $search; + + + /** + * @param string $filename File to open + * @param int $limit Max num of lines to read + * @param array $filter filters per column + * @param string $search string to search to filter lines + */ + public function __construct(string $filename, int $limit=0, array $filters=[], string $search="") { $this->reader = new ReversedFileReader($filename); - $this->_value = null; - $this->_limit = $limit; + $this->value = null; + $this->limit = $limit; + $this->filters = $filters; + $this->search = $search; + } + + /** + * Check if parsed log line match filters. + * Always match if no filters are set. + * + * @param ParsedLog $parsedlog + * @return bool + */ + private function filter($parsedlog) + { + $match = true; + foreach ($this->filters as $filter => $filtervalue) { + switch($filter) { + case "level": + $match = $match && ($parsedlog->level == strtoupper($filtervalue)); + break; + case "context": + $match = $match && ($parsedlog->context == $filtervalue); + break; + } + } + return $match; + } + + /** + * Check if parsed log line match search. + * Always match if no search query is set. + * + * @param ParsedLog $parsedlog + * @return bool + */ + private function search($parsedlog) + { + if ($this->search != "") { + return strstr($parsedlog->logline, $this->search) !== false; + } + return True; + } + + /** + * Read a line from reader and parse. + * Returns null if limit is reached or the reader is invalid. + * + * @param ParsedLog $parsedlog + * @return ?ParsedLog + */ + private function read() + { + $this->reader->next(); + if ($this->limit > 0 && $this->reader->key() > $this->limit || !$this->reader->valid()) { + return null; + } + + $line = $this->reader->current(); + return new ParsedLog($this->reader->key(), $line); } public function next() { - $this->reader->next(); - if ($this->_limit > 0 && $this->reader->key() > $this->_limit) { - $this->_value = null; - return; - } - if ($this->reader->valid()) { - $line = $this->reader->current(); - $this->_value = new ParsedLog($this->reader->key(), $line); - } else { - $this->_value = null; - } - } + $parsed = $this->read(); + // if read() has not retuned none and + // the line don't match filters or search + // read the next line + while(is_null($parsed) == false && !($this->filter($parsed) && $this->search($parsed))) { + $parsed = $this->read(); + } + $this->value = $parsed; + } public function rewind() { - $this->_value = null; + $this->value = null; $this->reader->rewind(); $this->next(); } @@ -69,12 +146,12 @@ class ParsedLogIterator implements \Iterator public function current() { - return $this->_value; + return $this->value; } public function valid() { - return ! is_null($this->_value); + return ! is_null($this->value); } } diff --git a/src/Module/Admin/Logs/View.php b/src/Module/Admin/Logs/View.php index 339a28b6a..a51250727 100644 --- a/src/Module/Admin/Logs/View.php +++ b/src/Module/Admin/Logs/View.php @@ -26,6 +26,7 @@ use Friendica\Core\Renderer; use Friendica\Core\Theme; use Friendica\Module\BaseAdmin; use Friendica\Model\Log\ParsedLogIterator; +use Psr\Log\LogLevel; class View extends BaseAdmin { @@ -43,11 +44,34 @@ class View extends BaseAdmin $error = null; + $search = $_GET['q'] ?? ''; + $filters_valid_values = [ + 'level' => [ + '', + LogLevel::CRITICAL, + LogLevel::ERROR, + LogLevel::WARNING, + LogLevel::NOTICE, + LogLevel::INFO, + LogLevel::DEBUG, + ], + 'context' => ['', 'index', 'worker'], + ]; + $filters = [ + 'level' => $_GET['level'] ?? '', + 'context' => $_GET['context'] ?? '', + ]; + foreach($filters as $k=>$v) { + if ($v == '' || !in_array($v, $filters_valid_values[$k])) { + unset($filters[$k]); + } + } + if (!file_exists($f)) { $error = DI::l10n()->t('Error trying to open %1$s log file.\r\n
Check to see if file %1$s exist and is readable.', $f); } else { try { - $data = new ParsedLogIterator($f, self::LIMIT); + $data = new ParsedLogIterator($f, self::LIMIT, $filters, $search); } catch (Exception $e) { $error = DI::l10n()->t('Couldn\'t open %1$s log file.\r\n
Check to see if file %1$s is readable.', $f); } @@ -56,8 +80,11 @@ class View extends BaseAdmin '$title' => DI::l10n()->t('Administration'), '$page' => DI::l10n()->t('View Logs'), '$data' => $data, + '$q' => $search, + '$filters' => $filters, + '$filtersvalues' => $filters_valid_values, '$error' => $error, - '$logname' => DI::config()->get('system', 'logfile') + '$logname' => DI::config()->get('system', 'logfile'), ]); } } diff --git a/src/Object/Log/ParsedLog.php b/src/Object/Log/ParsedLog.php index 21bd53876..f48ce400e 100644 --- a/src/Object/Log/ParsedLog.php +++ b/src/Object/Log/ParsedLog.php @@ -27,22 +27,38 @@ class ParsedLog { const REGEXP = '/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^ ]*) (\w+) \[(\w*)\]: (.*)/'; + /** @var int */ public $id = 0; + + /** @var string */ public $date = null; + + /** @var string */ public $context = null; + + /** @var string */ public $level = null; + + /** @var string */ public $message = null; + + /** @var string */ public $data = null; + + /** @var string */ public $source = null; + /** @var string */ + public $logline; + /** + * @param int line id * @param string $logline Source log line to parse */ public function __construct(int $id, string $logline) { $this->id = $id; $this->parse($logline); - $this->stop = false; } private function parse($logline) @@ -60,30 +76,34 @@ class ParsedLog $this->message = $matches[4]; $this->data = $jsondata; $this->source = $jsonsource; - $this->try_fix_json('data'); + $this->try_fix_json(); + + $this->logline = $logline; } /** + * Fix message / data split + * * In log boundary between message and json data is not specified. * If message contains '{' the parser thinks there starts the json data. * This method try to parse the found json and if it fails, search for next '{' * in json data and retry */ - private function try_fix_json(string $key) + private function try_fix_json() { - if (is_null($this->$key) || $this->$key == "") { + if (is_null($this->data) || $this->data == "") { return; } try { - $d = json_decode($this->$key, true, 512, JSON_THROW_ON_ERROR); + $d = json_decode($this->data, true, 512, JSON_THROW_ON_ERROR); } catch (\JsonException $e) { // try to find next { in $str and move string before to 'message' - $pos = strpos($this->$key, '{', 1); + $pos = strpos($this->data, '{', 1); - $this->message .= substr($this->$key, 0, $pos); - $this->$key = substr($this->key, $pos); - $this->try_fix_json($key); + $this->message .= substr($this->data, 0, $pos); + $this->data = substr($this->data, $pos); + $this->try_fix_json(); } } diff --git a/src/Util/ReversedFileReader.php b/src/Util/ReversedFileReader.php index 8a3083f7d..eeedc1a5c 100644 --- a/src/Util/ReversedFileReader.php +++ b/src/Util/ReversedFileReader.php @@ -32,6 +32,21 @@ class ReversedFileReader implements \Iterator const BUFFER_SIZE = 4096; const SEPARATOR = "\n"; + /** @var int */ + private $filesize; + + /** @var int */ + private $pos; + + /** @var array */ + private $buffer; + + /** @var int */ + private $key; + + /** @var string */ + private $value; + public function __construct($filename) { $this->_fh = fopen($filename, 'r'); @@ -39,25 +54,25 @@ class ReversedFileReader implements \Iterator // this should use a custom exception. throw \Exception("Unable to open $filename"); } - $this->_filesize = filesize($filename); - $this->_pos = -1; - $this->_buffer = null; - $this->_key = -1; - $this->_value = null; + $this->filesize = filesize($filename); + $this->pos = -1; + $this->buffer = null; + $this->key = -1; + $this->value = null; } public function _read($size) { - $this->_pos -= $size; - fseek($this->_fh, $this->_pos); + $this->pos -= $size; + fseek($this->_fh, $this->pos); return fread($this->_fh, $size); } public function _readline() { - $buffer =& $this->_buffer; + $buffer =& $this->buffer; while (true) { - if ($this->_pos == 0) { + if ($this->pos == 0) { return array_pop($buffer); } if (count($buffer) > 1) { @@ -69,33 +84,33 @@ class ReversedFileReader implements \Iterator public function next() { - ++$this->_key; - $this->_value = $this->_readline(); + ++$this->key; + $this->value = $this->_readline(); } public function rewind() { - if ($this->_filesize > 0) { - $this->_pos = $this->_filesize; - $this->_value = null; - $this->_key = -1; - $this->_buffer = explode(self::SEPARATOR, $this->_read($this->_filesize % self::BUFFER_SIZE ?: self::BUFFER_SIZE)); + if ($this->filesize > 0) { + $this->pos = $this->filesize; + $this->value = null; + $this->key = -1; + $this->buffer = explode(self::SEPARATOR, $this->_read($this->filesize % self::BUFFER_SIZE ?: self::BUFFER_SIZE)); $this->next(); } } public function key() { - return $this->_key; + return $this->key; } public function current() { - return $this->_value; + return $this->value; } public function valid() { - return ! is_null($this->_value); + return ! is_null($this->value); } } diff --git a/view/templates/admin/logs/view.tpl b/view/templates/admin/logs/view.tpl index 166dea0ba..523cb6a20 100644 --- a/view/templates/admin/logs/view.tpl +++ b/view/templates/admin/logs/view.tpl @@ -7,40 +7,68 @@

{{$error nofilter}}

{{else}} - - - - - - - - - - - {{foreach $data as $row}} - - - - - - - {{foreach $row->get_data() as $k=>$v}} - - - + +

+ + + clear +

+ + +
DateLevelContextMessage
{{$row->date}}{{$row->level}}{{$row->context}}{{$row->message}}
+ + + + + + - {{/foreach}} - - {{foreach $row->get_source() as $k=>$v}} - - - + + + {{foreach $data as $row}} + + + + + + + {{foreach $row->get_data() as $k=>$v}} + + + + + {{/foreach}} + + {{foreach $row->get_source() as $k=>$v}} + + + + + {{/foreach}} {{/foreach}} - {{/foreach}} - -
Date + + + + Message
{{$row->date}}{{$row->level}}{{$row->context}}{{$row->message}}
+ + + {{/if}} From 84fa6688459ca9ff22771ffccca1c8097aa54b27 Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Mon, 24 May 2021 22:05:02 +0200 Subject: [PATCH 04/12] Fix code formatting --- src/Model/Log/ParsedLogIterator.php | 34 ++++---- src/Module/Admin/Logs/View.php | 24 +++--- src/Object/Log/ParsedLog.php | 26 ++++--- src/Util/ReversedFileReader.php | 117 ++++++++++++++-------------- 4 files changed, 102 insertions(+), 99 deletions(-) diff --git a/src/Model/Log/ParsedLogIterator.php b/src/Model/Log/ParsedLogIterator.php index 15714454e..3e1832d96 100644 --- a/src/Model/Log/ParsedLogIterator.php +++ b/src/Model/Log/ParsedLogIterator.php @@ -18,11 +18,11 @@ * along with this program. If not, see . * */ + namespace Friendica\Model\Log; -use \Friendica\Util\ReversedFileReader; -use \Friendica\Object\Log\ParsedLog; - +use Friendica\Util\ReversedFileReader; +use Friendica\Object\Log\ParsedLog; /** * An iterator which returns `\Friendica\Objec\Log\ParsedLog` instances @@ -36,7 +36,7 @@ class ParsedLogIterator implements \Iterator private $reader; /** @var ParsedLog current iterator value*/ - private $value; + private $value; /** @var int max number of lines to read */ private $limit; @@ -54,19 +54,19 @@ class ParsedLogIterator implements \Iterator * @param array $filter filters per column * @param string $search string to search to filter lines */ - public function __construct(string $filename, int $limit=0, array $filters=[], string $search="") + public function __construct(string $filename, int $limit = 0, array $filters = [], string $search = "") { - $this->reader = new ReversedFileReader($filename); - $this->value = null; - $this->limit = $limit; + $this->reader = new ReversedFileReader($filename); + $this->value = null; + $this->limit = $limit; $this->filters = $filters; - $this->search = $search; + $this->search = $search; } /** * Check if parsed log line match filters. * Always match if no filters are set. - * + * * @param ParsedLog $parsedlog * @return bool */ @@ -74,7 +74,7 @@ class ParsedLogIterator implements \Iterator { $match = true; foreach ($this->filters as $filter => $filtervalue) { - switch($filter) { + switch ($filter) { case "level": $match = $match && ($parsedlog->level == strtoupper($filtervalue)); break; @@ -89,7 +89,7 @@ class ParsedLogIterator implements \Iterator /** * Check if parsed log line match search. * Always match if no search query is set. - * + * * @param ParsedLog $parsedlog * @return bool */ @@ -98,20 +98,20 @@ class ParsedLogIterator implements \Iterator if ($this->search != "") { return strstr($parsedlog->logline, $this->search) !== false; } - return True; + return true; } /** * Read a line from reader and parse. * Returns null if limit is reached or the reader is invalid. - * + * * @param ParsedLog $parsedlog * @return ?ParsedLog */ private function read() { $this->reader->next(); - if ($this->limit > 0 && $this->reader->key() > $this->limit || !$this->reader->valid()) { + if ($this->limit > 0 && $this->reader->key() > $this->limit || !$this->reader->valid()) { return null; } @@ -126,7 +126,7 @@ class ParsedLogIterator implements \Iterator // if read() has not retuned none and // the line don't match filters or search // read the next line - while(is_null($parsed) == false && !($this->filter($parsed) && $this->search($parsed))) { + while (is_null($parsed) == false && !($this->filter($parsed) && $this->search($parsed))) { $parsed = $this->read(); } $this->value = $parsed; @@ -153,6 +153,4 @@ class ParsedLogIterator implements \Iterator { return ! is_null($this->value); } - } - diff --git a/src/Module/Admin/Logs/View.php b/src/Module/Admin/Logs/View.php index a51250727..26ad3a8b6 100644 --- a/src/Module/Admin/Logs/View.php +++ b/src/Module/Admin/Logs/View.php @@ -39,12 +39,12 @@ class View extends BaseAdmin $t = Renderer::getMarkupTemplate('admin/logs/view.tpl'); DI::page()->registerFooterScript(Theme::getPathForFile('js/module/admin/logs/view.js')); - $f = DI::config()->get('system', 'logfile'); - $data = null; + $f = DI::config()->get('system', 'logfile'); + $data = null; $error = null; - $search = $_GET['q'] ?? ''; + $filters_valid_values = [ 'level' => [ '', @@ -58,10 +58,10 @@ class View extends BaseAdmin 'context' => ['', 'index', 'worker'], ]; $filters = [ - 'level' => $_GET['level'] ?? '', + 'level' => $_GET['level'] ?? '', 'context' => $_GET['context'] ?? '', ]; - foreach($filters as $k=>$v) { + foreach ($filters as $k => $v) { if ($v == '' || !in_array($v, $filters_valid_values[$k])) { unset($filters[$k]); } @@ -77,14 +77,14 @@ class View extends BaseAdmin } } return Renderer::replaceMacros($t, [ - '$title' => DI::l10n()->t('Administration'), - '$page' => DI::l10n()->t('View Logs'), - '$data' => $data, - '$q' => $search, - '$filters' => $filters, + '$title' => DI::l10n()->t('Administration'), + '$page' => DI::l10n()->t('View Logs'), + '$data' => $data, + '$q' => $search, + '$filters' => $filters, '$filtersvalues' => $filters_valid_values, - '$error' => $error, - '$logname' => DI::config()->get('system', 'logfile'), + '$error' => $error, + '$logname' => DI::config()->get('system', 'logfile'), ]); } } diff --git a/src/Object/Log/ParsedLog.php b/src/Object/Log/ParsedLog.php index f48ce400e..c33f4f950 100644 --- a/src/Object/Log/ParsedLog.php +++ b/src/Object/Log/ParsedLog.php @@ -18,6 +18,7 @@ * along with this program. If not, see . * */ + namespace Friendica\Object\Log; /** @@ -64,18 +65,22 @@ class ParsedLog private function parse($logline) { list($logline, $jsonsource) = explode(' - ', $logline); + $jsondata = null; + if (strpos($logline, '{"') > 0) { list($logline, $jsondata) = explode('{"', $logline, 2); + $jsondata = '{"' . $jsondata; } preg_match(self::REGEXP, $logline, $matches); - $this->date = $matches[1]; + + $this->date = $matches[1]; $this->context = $matches[2]; - $this->level = $matches[3]; + $this->level = $matches[3]; $this->message = $matches[4]; - $this->data = $jsondata; - $this->source = $jsonsource; + $this->data = $jsondata; + $this->source = $jsonsource; $this->try_fix_json(); $this->logline = $logline; @@ -83,7 +88,7 @@ class ParsedLog /** * Fix message / data split - * + * * In log boundary between message and json data is not specified. * If message contains '{' the parser thinks there starts the json data. * This method try to parse the found json and if it fails, search for next '{' @@ -112,12 +117,12 @@ class ParsedLog * * @return array */ - public function get_data() { + public function get_data() + { $data = json_decode($this->data, true); if ($data) { - foreach($data as $k => $v) { - $v = print_r($v, true); - $data[$k] = $v; + foreach ($data as $k => $v) { + $data[$k] = print_r($v, true); } } return $data; @@ -128,7 +133,8 @@ class ParsedLog * * @return array */ - public function get_source() { + public function get_source() + { return json_decode($this->source, true); } } diff --git a/src/Util/ReversedFileReader.php b/src/Util/ReversedFileReader.php index eeedc1a5c..c889b5f93 100644 --- a/src/Util/ReversedFileReader.php +++ b/src/Util/ReversedFileReader.php @@ -21,7 +21,6 @@ namespace Friendica\Util; - /** * An iterator which returns lines from file in reversed order * @@ -29,87 +28,87 @@ namespace Friendica\Util; */ class ReversedFileReader implements \Iterator { - const BUFFER_SIZE = 4096; - const SEPARATOR = "\n"; + const BUFFER_SIZE = 4096; + const SEPARATOR = "\n"; - /** @var int */ - private $filesize; + /** @var int */ + private $filesize; - /** @var int */ - private $pos; + /** @var int */ + private $pos; - /** @var array */ - private $buffer; + /** @var array */ + private $buffer; - /** @var int */ - private $key; + /** @var int */ + private $key; - /** @var string */ - private $value; + /** @var string */ + private $value; - public function __construct($filename) - { - $this->_fh = fopen($filename, 'r'); + public function __construct($filename) + { + $this->_fh = fopen($filename, 'r'); if (!$this->_fh) { // this should use a custom exception. throw \Exception("Unable to open $filename"); } - $this->filesize = filesize($filename); - $this->pos = -1; - $this->buffer = null; - $this->key = -1; - $this->value = null; - } + $this->filesize = filesize($filename); + $this->pos = -1; + $this->buffer = null; + $this->key = -1; + $this->value = null; + } - public function _read($size) - { - $this->pos -= $size; - fseek($this->_fh, $this->pos); - return fread($this->_fh, $size); - } + public function _read($size) + { + $this->pos -= $size; + fseek($this->_fh, $this->pos); + return fread($this->_fh, $size); + } - public function _readline() - { - $buffer =& $this->buffer; - while (true) { - if ($this->pos == 0) { - return array_pop($buffer); - } - if (count($buffer) > 1) { - return array_pop($buffer); - } - $buffer = explode(self::SEPARATOR, $this->_read(self::BUFFER_SIZE) . $buffer[0]); - } - } + public function _readline() + { + $buffer = & $this->buffer; + while (true) { + if ($this->pos == 0) { + return array_pop($buffer); + } + if (count($buffer) > 1) { + return array_pop($buffer); + } + $buffer = explode(self::SEPARATOR, $this->_read(self::BUFFER_SIZE) . $buffer[0]); + } + } - public function next() - { - ++$this->key; - $this->value = $this->_readline(); - } + public function next() + { + ++$this->key; + $this->value = $this->_readline(); + } - public function rewind() - { - if ($this->filesize > 0) { - $this->pos = $this->filesize; - $this->value = null; - $this->key = -1; - $this->buffer = explode(self::SEPARATOR, $this->_read($this->filesize % self::BUFFER_SIZE ?: self::BUFFER_SIZE)); - $this->next(); - } - } + public function rewind() + { + if ($this->filesize > 0) { + $this->pos = $this->filesize; + $this->value = null; + $this->key = -1; + $this->buffer = explode(self::SEPARATOR, $this->_read($this->filesize % self::BUFFER_SIZE ?: self::BUFFER_SIZE)); + $this->next(); + } + } - public function key() + public function key() { return $this->key; } - public function current() + public function current() { return $this->value; } - public function valid() + public function valid() { return ! is_null($this->value); } From 5e5d9db1b3415b1b578468e9d39f0b97dfa93d77 Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Tue, 25 May 2021 14:10:36 +0200 Subject: [PATCH 05/12] logs view: update frio template add search and filter columns support. add "prev/next" buttons to details popup --- view/theme/frio/js/module/admin/logs/view.js | 58 ++++++++++++++++--- view/theme/frio/templates/admin/logs/view.tpl | 53 +++++++++++++++-- 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/view/theme/frio/js/module/admin/logs/view.js b/view/theme/frio/js/module/admin/logs/view.js index fc19af28a..149d019e9 100644 --- a/view/theme/frio/js/module/admin/logs/view.js +++ b/view/theme/frio/js/module/admin/logs/view.js @@ -1,10 +1,51 @@ $(function(){ - $(".log-event").on("click", function(ev) { - var $modal = $("#logdetail"); - var tr = $modal.find(".main-data tbody tr")[0]; - tr.innerHTML = ev.currentTarget.innerHTML; - var data = JSON.parse(ev.currentTarget.dataset.source); + /* column filter */ + $("a[data-filter]").on("click", function(ev) { + var filter = this.dataset.filter; + var value = this.dataset.filterValue; + var re = RegExp(filter+"=[a-z]*"); + var newhref = location.href; + if (!location.href.indexOf("?") < 0) { + newhref = location.href + "?" + filter + "=" + value; + } else if (location.href.match(re)) { + newhref = location.href.replace(RegExp(filter+"=[a-z]*"), filter+"="+value); + } else { + newhref = location.href + "&" + filter + "=" + value; + } + location.href = newhref; + return false; + }); + + /* log details dialog */ + $(".log-event").on("click", function(ev) { + show_details_for_element(ev.currentTarget); + }); + + $("[data-previous").on("click", function(ev){ + var currentid = document.getElementById("logdetail").dataset.rowId; + var $elm = $("#" + currentid).prev(); + if ($elm.length == 0) return; + show_details_for_element($elm[0]); + }); + + $("[data-next").on("click", function(ev){ + var currentid = document.getElementById("logdetail").dataset.rowId; + var $elm = $("#" + currentid).next(); + if ($elm.length == 0) return; + show_details_for_element($elm[0]); + }); + + + function show_details_for_element(element) { + var $modal = $("#logdetail"); + + $modal[0].dataset.rowId = element.id; + + var tr = $modal.find(".main-data tbody tr")[0]; + tr.innerHTML = element.innerHTML; + + var data = JSON.parse(element.dataset.source); $modal.find(".source-data td").each(function(i,elm){ var k = elm.dataset.value; elm.innerText = data[k]; @@ -12,15 +53,18 @@ $(function(){ var elm = $modal.find(".event-data")[0]; elm.innerHTML = ""; - var data = ev.currentTarget.dataset.data; + var data = element.dataset.data; if (data !== "") { elm.innerHTML = "

Data

"; data = JSON.parse(data); elm.innerHTML += recursive_details("", data); } + $("[data-previous").prop("disabled", $(element).prev().length == 0); + $("[data-next").prop("disabled", $(element).next().length == 0); + $modal.modal({}) - }) + } function recursive_details(s, data, lev=0) { for(var k in data) { diff --git a/view/theme/frio/templates/admin/logs/view.tpl b/view/theme/frio/templates/admin/logs/view.tpl index 99a96befd..1bc026916 100755 --- a/view/theme/frio/templates/admin/logs/view.tpl +++ b/view/theme/frio/templates/admin/logs/view.tpl @@ -6,13 +6,54 @@

{{$error nofilter}}

- {{else}} + {{else}} +
+
+ +
+
+ Show all +
+
+ - - + + @@ -28,14 +69,14 @@ {{elseif $row->level == "NOTICE"}}bg-info {{elseif $row->level == "DEBUG"}}text-muted {{/if}} - ">{{$row->level}} + ">{{$row->level}} {{/foreach}}
DateLevelContext + + + + + + Message
{{$row->context}} {{$row->message}}
- {{/if}} + {{/if}} From a62124285d0812cc50bf2b5832eef4ef98ac27ba Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Tue, 20 Jul 2021 18:03:20 +0200 Subject: [PATCH 06/12] Add tests for ParsedLog and fix parsing --- src/Object/Log/ParsedLog.php | 15 +++-- tests/src/Object/Log/ParsedLogTest.php | 93 ++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 tests/src/Object/Log/ParsedLogTest.php diff --git a/src/Object/Log/ParsedLog.php b/src/Object/Log/ParsedLog.php index c33f4f950..b1bc82112 100644 --- a/src/Object/Log/ParsedLog.php +++ b/src/Object/Log/ParsedLog.php @@ -64,7 +64,13 @@ class ParsedLog private function parse($logline) { - list($logline, $jsonsource) = explode(' - ', $logline); + // if data is empty is serialized as '[]'. To ease the parsing + // let's replace it with '{""}'. It will be replaced by null later + $logline = str_replace(' [] - {', ' {""} - {', $logline); + + // here we hope that there will not be the string ' - {' inside the $jsonsource value + list($logline, $jsonsource) = explode(' - {', $logline); + $jsonsource = '{' . $jsonsource; $jsondata = null; @@ -73,13 +79,14 @@ class ParsedLog $jsondata = '{"' . $jsondata; } + preg_match(self::REGEXP, $logline, $matches); $this->date = $matches[1]; $this->context = $matches[2]; $this->level = $matches[3]; - $this->message = $matches[4]; - $this->data = $jsondata; + $this->message = trim($matches[4]); + $this->data = $jsondata == '{""}' ? null : $jsondata; $this->source = $jsonsource; $this->try_fix_json(); @@ -96,7 +103,7 @@ class ParsedLog */ private function try_fix_json() { - if (is_null($this->data) || $this->data == "") { + if (is_null($this->data) || $this->data == '') { return; } try { diff --git a/tests/src/Object/Log/ParsedLogTest.php b/tests/src/Object/Log/ParsedLogTest.php new file mode 100644 index 000000000..ab98030fc --- /dev/null +++ b/tests/src/Object/Log/ParsedLogTest.php @@ -0,0 +1,93 @@ +. + * + */ + +namespace Friendica\Test\src\Object\Log; + +use Friendica\Object\Log\ParsedLog; +use PHPUnit\Framework\TestCase; + +/** + * Log parser testing class + */ +class ParsedLogTest extends TestCase +{ + public static function do_log_line($logline, $expected_data) + { + $parsed = new ParsedLog(0, $logline); + foreach ($expected_data as $k => $v) { + self::assertSame($parsed->$k, $v, '"'.$k.'" does not match expectation'); + } + } + + /** + * test parsing a generic log line + */ + public function testGenericLogLine() + { + self::do_log_line( + '2021-05-24T15:40:01Z worker [NOTICE]: Spool file does does not start with "item-" {"file":".","worker_id":"560c8b6","worker_cmd":"SpoolPost"} - {"file":"SpoolPost.php","line":40,"function":"execute","uid":"fd8c37","process_id":20846}', + [ + 'date' => '2021-05-24T15:40:01Z', + 'context' => 'worker', + 'level' => 'NOTICE', + 'message' => 'Spool file does does not start with "item-"', + 'data' => '{"file":".","worker_id":"560c8b6","worker_cmd":"SpoolPost"}', + 'source' => '{"file":"SpoolPost.php","line":40,"function":"execute","uid":"fd8c37","process_id":20846}', + ] + ); + } + + /** + * test parsing a log line with empty data + */ + public function testEmptyDataLogLine() + { + self::do_log_line( + '2021-05-24T15:23:58Z index [INFO]: No HTTP_SIGNATURE header [] - {"file":"HTTPSignature.php","line":476,"function":"getSigner","uid":"0a3934","process_id":14826}', + [ + 'date' => '2021-05-24T15:23:58Z', + 'context' => 'index', + 'level' => 'INFO', + 'message' => 'No HTTP_SIGNATURE header', + 'data' => null, + 'source' => '{"file":"HTTPSignature.php","line":476,"function":"getSigner","uid":"0a3934","process_id":14826}', + ] + ); + } + + /** + * test parsing a log line with various " - " in it + */ + public function testTrickyDashLogLine() + { + self::do_log_line( + '2021-05-24T15:30:01Z worker [NOTICE]: Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10 {"worker_id":"ece8fc8","worker_cmd":"Cron"} - {"file":"Worker.php","line":786,"function":"tooMuchWorkers","uid":"364d3c","process_id":20754}', + [ + 'date' => '2021-05-24T15:30:01Z', + 'context' => 'worker', + 'level' => 'NOTICE', + 'message' => 'Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10', + 'data' => '{"worker_id":"ece8fc8","worker_cmd":"Cron"}', + 'source' => '{"file":"Worker.php","line":786,"function":"tooMuchWorkers","uid":"364d3c","process_id":20754}', + ] + ); + } +} From ec4f53d56f9aa2f82b1cf327a7f0869251fb1dbb Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Thu, 19 Aug 2021 13:03:45 +0200 Subject: [PATCH 07/12] Add DI to ParsedLogIterator, replace constructors with fluent api --- src/DI.php | 8 ++++ src/Model/Log/ParsedLogIterator.php | 59 ++++++++++++++++++++++------- src/Module/Admin/Logs/View.php | 6 ++- src/Util/ReversedFileReader.php | 36 +++++++++++------- static/dependencies.config.php | 6 +++ 5 files changed, 88 insertions(+), 27 deletions(-) diff --git a/src/DI.php b/src/DI.php index 28ad130b4..cbc353161 100644 --- a/src/DI.php +++ b/src/DI.php @@ -394,6 +394,14 @@ abstract class DI return self::$dice->create(Model\Storage\IWritableStorage::class); } + /** + * @return Model\Log\ParsedLogIterator + */ + public static function parsedLogIterator() + { + return self::$dice->create(Model\Log\ParsedLogIterator::class); + } + // // "Network" namespace // diff --git a/src/Model/Log/ParsedLogIterator.php b/src/Model/Log/ParsedLogIterator.php index 3e1832d96..bde07ee4a 100644 --- a/src/Model/Log/ParsedLogIterator.php +++ b/src/Model/Log/ParsedLogIterator.php @@ -36,31 +36,64 @@ class ParsedLogIterator implements \Iterator private $reader; /** @var ParsedLog current iterator value*/ - private $value; + private $value = null; /** @var int max number of lines to read */ - private $limit; + private $limit = 0; /** @var array filters per column */ - private $filters; + private $filters = []; /** @var string search term */ - private $search; + private $search = ""; /** - * @param string $filename File to open - * @param int $limit Max num of lines to read - * @param array $filter filters per column - * @param string $search string to search to filter lines + * @param ReversedFileReader $reader */ - public function __construct(string $filename, int $limit = 0, array $filters = [], string $search = "") + public function __construct(ReversedFileReader $reader) + { + $this->reader = $reader; + } + + /** + * @param string $filename File to open + * @return $this + */ + public function open(string $filename) + { + $this->reader->open($filename); + return $this; + } + + /** + * @param int $limit Max num of lines to read + * @return $this + */ + public function withLimit(int $limit) + { + $this->limit = $limit; + return $this; + } + + /** + * @param array $filters filters per column + * @return $this + */ + public function withFilters(array $filters) { - $this->reader = new ReversedFileReader($filename); - $this->value = null; - $this->limit = $limit; $this->filters = $filters; - $this->search = $search; + return $this; + } + + /** + * @param string $search string to search to filter lines + * @return $this + */ + public function withSearch(string $search) + { + $this->search = $search; + return $this; } /** diff --git a/src/Module/Admin/Logs/View.php b/src/Module/Admin/Logs/View.php index 26ad3a8b6..222052380 100644 --- a/src/Module/Admin/Logs/View.php +++ b/src/Module/Admin/Logs/View.php @@ -71,7 +71,11 @@ class View extends BaseAdmin $error = DI::l10n()->t('Error trying to open %1$s log file.\r\n
Check to see if file %1$s exist and is readable.', $f); } else { try { - $data = new ParsedLogIterator($f, self::LIMIT, $filters, $search); + $data = DI::parsedLogIterator() + ->open($f) + ->withLimit(self::LIMIT) + ->withFilters($filters) + ->withSearch($search); } catch (Exception $e) { $error = DI::l10n()->t('Couldn\'t open %1$s log file.\r\n
Check to see if file %1$s is readable.', $f); } diff --git a/src/Util/ReversedFileReader.php b/src/Util/ReversedFileReader.php index c889b5f93..64d41b25d 100644 --- a/src/Util/ReversedFileReader.php +++ b/src/Util/ReversedFileReader.php @@ -31,25 +31,34 @@ class ReversedFileReader implements \Iterator const BUFFER_SIZE = 4096; const SEPARATOR = "\n"; - /** @var int */ - private $filesize; + /** @var resource */ + private $fh = null; /** @var int */ - private $pos; + private $filesize = -1; + + /** @var int */ + private $pos = -1; /** @var array */ - private $buffer; + private $buffer = null; /** @var int */ - private $key; + private $key = -1; /** @var string */ - private $value; + private $value = null; - public function __construct($filename) + /** + * Open $filename for read and reset iterator + * + * @param string $filename File to open + * @return $this + */ + public function open(string $filename) { - $this->_fh = fopen($filename, 'r'); - if (!$this->_fh) { + $this->fh = fopen($filename, 'r'); + if (!$this->fh) { // this should use a custom exception. throw \Exception("Unable to open $filename"); } @@ -58,16 +67,17 @@ class ReversedFileReader implements \Iterator $this->buffer = null; $this->key = -1; $this->value = null; + return $this; } - public function _read($size) + private function _read($size) { $this->pos -= $size; - fseek($this->_fh, $this->pos); - return fread($this->_fh, $size); + fseek($this->fh, $this->pos); + return fread($this->fh, $size); } - public function _readline() + private function _readline() { $buffer = & $this->buffer; while (true) { diff --git a/static/dependencies.config.php b/static/dependencies.config.php index 5efe78ddf..f07a61807 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -46,6 +46,7 @@ use Friendica\Database\Database; use Friendica\Factory; use Friendica\Model\Storage\IWritableStorage; use Friendica\Model\User\Cookie; +use Friendica\Model\Log\ParsedLogIterator; use Friendica\Network; use Friendica\Util; use Psr\Log\LoggerInterface; @@ -227,4 +228,9 @@ return [ $_SERVER ], ], + ParsedLogIterator::class => [ + 'constructParams' => [ + [Dice::INSTANCE => Util\ReversedFileReader::class], + ] + ] ]; From ce0d7616cc6b43aa7efdc532c9e3d45e532aeb26 Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Thu, 19 Aug 2021 14:28:29 +0200 Subject: [PATCH 08/12] Add tests for ParsedLogIterator --- tests/datasets/log/friendica.log.txt | 3 + tests/src/Model/Log/ParsedLogIteratorTest.php | 154 ++++++++++++++++++ tests/src/Object/Log/ParsedLogTest.php | 4 +- 3 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 tests/datasets/log/friendica.log.txt create mode 100644 tests/src/Model/Log/ParsedLogIteratorTest.php diff --git a/tests/datasets/log/friendica.log.txt b/tests/datasets/log/friendica.log.txt new file mode 100644 index 000000000..ed695bd8e --- /dev/null +++ b/tests/datasets/log/friendica.log.txt @@ -0,0 +1,3 @@ +2021-05-24T15:23:58Z index [INFO]: No HTTP_SIGNATURE header [] - {"file":"HTTPSignature.php","line":476,"function":"getSigner","uid":"0a3934","process_id":14826} +2021-05-24T15:30:01Z worker [NOTICE]: Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10 {"worker_id":"ece8fc8","worker_cmd":"Cron"} - {"file":"Worker.php","line":786,"function":"tooMuchWorkers","uid":"364d3c","process_id":20754} +2021-05-24T15:40:01Z worker [WARNING]: Spool file does does not start with "item-" {"file":".","worker_id":"560c8b6","worker_cmd":"SpoolPost"} - {"file":"SpoolPost.php","line":40,"function":"execute","uid":"fd8c37","process_id":20846} diff --git a/tests/src/Model/Log/ParsedLogIteratorTest.php b/tests/src/Model/Log/ParsedLogIteratorTest.php new file mode 100644 index 000000000..c97195547 --- /dev/null +++ b/tests/src/Model/Log/ParsedLogIteratorTest.php @@ -0,0 +1,154 @@ +. + * + */ + +namespace Friendica\Test\src\Object\Log; + +use Friendica\Util\ReversedFileReader; +use Friendica\Model\Log\ParsedLogIterator; + +use PHPUnit\Framework\TestCase; + + +/** + * Parsed log iterator testing class + */ +class ParsedLogIteratorTest extends TestCase +{ + protected $pli; + + public static function assertParsed($parsed, $expected_data) + { + foreach ($expected_data as $k => $v) { + self::assertSame($parsed->$k, $v, '"'.$k.'" does not match expectation'); + } + } + + protected function setUp() + { + $logfile = dirname(__DIR__) . DIRECTORY_SEPARATOR . + '..' . DIRECTORY_SEPARATOR . + '..' . DIRECTORY_SEPARATOR . + 'datasets' . DIRECTORY_SEPARATOR . + 'log' . DIRECTORY_SEPARATOR . + 'friendica.log.txt'; + + $reader = new ReversedFileReader(); + $this->pli = new ParsedLogIterator($reader); + $this->pli->open($logfile); + } + + public function testIsIterable() + { + self::assertIsIterable($this->pli); + } + + public function testEverything() + { + self::assertCount(3, iterator_to_array($this->pli, false)); + } + + public function testLimit() + { + $this->pli->withLimit(2); + self::assertCount(2, iterator_to_array($this->pli, false)); + } + + public function testFilterByLevel() + { + $this->pli->withFilters(['level' => 'INFO']); + $pls = iterator_to_array($this->pli, false); + self::assertCount(1, $pls); + self::assertParsed( + $pls[0], + [ + 'date' => '2021-05-24T15:23:58Z', + 'context' => 'index', + 'level' => 'INFO', + 'message' => 'No HTTP_SIGNATURE header', + 'data' => null, + 'source' => '{"file":"HTTPSignature.php","line":476,"function":"getSigner","uid":"0a3934","process_id":14826}', + ] + ); + } + + public function testFilterByContext() + { + $this->pli->withFilters(['context' => 'worker']); + $pls = iterator_to_array($this->pli, false); + self::assertCount(2, $pls); + self::assertParsed( + $pls[0], + [ + 'date' => '2021-05-24T15:40:01Z', + 'context' => 'worker', + 'level' => 'WARNING', + 'message' => 'Spool file does does not start with "item-"', + 'data' => '{"file":".","worker_id":"560c8b6","worker_cmd":"SpoolPost"}', + 'source' => '{"file":"SpoolPost.php","line":40,"function":"execute","uid":"fd8c37","process_id":20846}', + ] + ); + } + + public function testFilterCombined() + { + $this->pli->withFilters(['level' => 'NOTICE', 'context' => 'worker']); + $pls = iterator_to_array($this->pli, false); + self::assertCount(1, $pls); + self::assertParsed( + $pls[0], + [ + 'date' => '2021-05-24T15:30:01Z', + 'context' => 'worker', + 'level' => 'NOTICE', + 'message' => 'Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10', + 'data' => '{"worker_id":"ece8fc8","worker_cmd":"Cron"}', + 'source' => '{"file":"Worker.php","line":786,"function":"tooMuchWorkers","uid":"364d3c","process_id":20754}', + ] + ); + } + + public function testSearch() + { + $this->pli->withSearch("maximum"); + $pls = iterator_to_array($this->pli, false); + self::assertCount(1, $pls); + self::assertParsed( + $pls[0], + [ + 'date' => '2021-05-24T15:30:01Z', + 'context' => 'worker', + 'level' => 'NOTICE', + 'message' => 'Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10', + 'data' => '{"worker_id":"ece8fc8","worker_cmd":"Cron"}', + 'source' => '{"file":"Worker.php","line":786,"function":"tooMuchWorkers","uid":"364d3c","process_id":20754}', + ] + ); + } + + public function testFilterAndSearch() + { + $this->pli + ->withFilters(['context' => 'worker']) + ->withSearch("header"); + $pls = iterator_to_array($this->pli, false); + self::assertCount(0, $pls); + } +} diff --git a/tests/src/Object/Log/ParsedLogTest.php b/tests/src/Object/Log/ParsedLogTest.php index ab98030fc..ff3d06f55 100644 --- a/tests/src/Object/Log/ParsedLogTest.php +++ b/tests/src/Object/Log/ParsedLogTest.php @@ -43,11 +43,11 @@ class ParsedLogTest extends TestCase public function testGenericLogLine() { self::do_log_line( - '2021-05-24T15:40:01Z worker [NOTICE]: Spool file does does not start with "item-" {"file":".","worker_id":"560c8b6","worker_cmd":"SpoolPost"} - {"file":"SpoolPost.php","line":40,"function":"execute","uid":"fd8c37","process_id":20846}', + '2021-05-24T15:40:01Z worker [WARNING]: Spool file does does not start with "item-" {"file":".","worker_id":"560c8b6","worker_cmd":"SpoolPost"} - {"file":"SpoolPost.php","line":40,"function":"execute","uid":"fd8c37","process_id":20846}', [ 'date' => '2021-05-24T15:40:01Z', 'context' => 'worker', - 'level' => 'NOTICE', + 'level' => 'WARNING', 'message' => 'Spool file does does not start with "item-"', 'data' => '{"file":".","worker_id":"560c8b6","worker_cmd":"SpoolPost"}', 'source' => '{"file":"SpoolPost.php","line":40,"function":"execute","uid":"fd8c37","process_id":20846}', From dd2abc7aff18998205e18c7021097285fe72c821 Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Thu, 19 Aug 2021 15:20:25 +0200 Subject: [PATCH 09/12] Fix code standard --- src/Util/ReversedFileReader.php | 2 +- tests/src/Model/Log/ParsedLogIteratorTest.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Util/ReversedFileReader.php b/src/Util/ReversedFileReader.php index 64d41b25d..c4fe131ae 100644 --- a/src/Util/ReversedFileReader.php +++ b/src/Util/ReversedFileReader.php @@ -51,7 +51,7 @@ class ReversedFileReader implements \Iterator /** * Open $filename for read and reset iterator - * + * * @param string $filename File to open * @return $this */ diff --git a/tests/src/Model/Log/ParsedLogIteratorTest.php b/tests/src/Model/Log/ParsedLogIteratorTest.php index c97195547..5bbfa229d 100644 --- a/tests/src/Model/Log/ParsedLogIteratorTest.php +++ b/tests/src/Model/Log/ParsedLogIteratorTest.php @@ -26,7 +26,6 @@ use Friendica\Model\Log\ParsedLogIterator; use PHPUnit\Framework\TestCase; - /** * Parsed log iterator testing class */ @@ -50,7 +49,7 @@ class ParsedLogIteratorTest extends TestCase 'log' . DIRECTORY_SEPARATOR . 'friendica.log.txt'; - $reader = new ReversedFileReader(); + $reader = new ReversedFileReader(); $this->pli = new ParsedLogIterator($reader); $this->pli->open($logfile); } From 5520f100b24043468e9eb276bd5d6000177e5be1 Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Thu, 19 Aug 2021 15:26:26 +0200 Subject: [PATCH 10/12] Fix `ParsedLogIteratorTest::setUp()` --- tests/src/Model/Log/ParsedLogIteratorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/Model/Log/ParsedLogIteratorTest.php b/tests/src/Model/Log/ParsedLogIteratorTest.php index 5bbfa229d..846deab1c 100644 --- a/tests/src/Model/Log/ParsedLogIteratorTest.php +++ b/tests/src/Model/Log/ParsedLogIteratorTest.php @@ -40,7 +40,7 @@ class ParsedLogIteratorTest extends TestCase } } - protected function setUp() + protected function setUp(): void { $logfile = dirname(__DIR__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . From 7f695197aae9d87f5c04c2f7801d97852d72a3bc Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Fri, 20 Aug 2021 09:47:53 +0200 Subject: [PATCH 11/12] Fix review points - Fix headers hierarchy - Improve accessibility: - set mouse pointer - make rows focusable - open on key press - add tooltip with "title" - add role and aria attributes - Rename `ParsedLog` to `ParsedLogLine` - Add docs to `ReversedFileReader`'s implementation of `Iterator`'s methods - Add docs to `ParsedLogIterator`'s implementation of `Iterator`'s methods - Remove unnecessary comment - Add more test for parsing log lines and fix some edge cases - Fix function name in snake-case to camelCase - Remove `DIRECTORY_SEPARATOR` --- src/Model/Log/ParsedLogIterator.php | 63 ++++++++---- .../Log/{ParsedLog.php => ParsedLogLine.php} | 51 ++++++---- src/Util/ReversedFileReader.php | 41 ++++++++ tests/src/Model/Log/ParsedLogIteratorTest.php | 7 +- ...arsedLogTest.php => ParsedLogLineTest.php} | 96 ++++++++++++++++++- view/js/module/admin/logs/view.js | 34 ++++++- view/templates/admin/logs/view.tpl | 12 ++- view/theme/frio/css/style.css | 7 ++ view/theme/frio/js/module/admin/logs/view.js | 17 +++- view/theme/frio/templates/admin/logs/view.tpl | 6 +- 10 files changed, 280 insertions(+), 54 deletions(-) rename src/Object/Log/{ParsedLog.php => ParsedLogLine.php} (76%) rename tests/src/Object/Log/{ParsedLogTest.php => ParsedLogLineTest.php} (54%) diff --git a/src/Model/Log/ParsedLogIterator.php b/src/Model/Log/ParsedLogIterator.php index bde07ee4a..856edb38f 100644 --- a/src/Model/Log/ParsedLogIterator.php +++ b/src/Model/Log/ParsedLogIterator.php @@ -22,10 +22,10 @@ namespace Friendica\Model\Log; use Friendica\Util\ReversedFileReader; -use Friendica\Object\Log\ParsedLog; +use Friendica\Object\Log\ParsedLogLine; /** - * An iterator which returns `\Friendica\Objec\Log\ParsedLog` instances + * An iterator which returns `\Friendica\Objec\Log\ParsedLogLine` instances * * Uses `\Friendica\Util\ReversedFileReader` to fetch log lines * from newest to oldest. @@ -35,7 +35,7 @@ class ParsedLogIterator implements \Iterator /** @var \Iterator */ private $reader; - /** @var ParsedLog current iterator value*/ + /** @var ParsedLogLine current iterator value*/ private $value = null; /** @var int max number of lines to read */ @@ -100,19 +100,19 @@ class ParsedLogIterator implements \Iterator * Check if parsed log line match filters. * Always match if no filters are set. * - * @param ParsedLog $parsedlog + * @param ParsedLogLine $parsedlogline * @return bool */ - private function filter($parsedlog) + private function filter($parsedlogline) { $match = true; foreach ($this->filters as $filter => $filtervalue) { switch ($filter) { case "level": - $match = $match && ($parsedlog->level == strtoupper($filtervalue)); + $match = $match && ($parsedlogline->level == strtoupper($filtervalue)); break; case "context": - $match = $match && ($parsedlog->context == $filtervalue); + $match = $match && ($parsedlogline->context == $filtervalue); break; } } @@ -123,13 +123,13 @@ class ParsedLogIterator implements \Iterator * Check if parsed log line match search. * Always match if no search query is set. * - * @param ParsedLog $parsedlog + * @param ParsedLogLine $parsedlogline * @return bool */ - private function search($parsedlog) + private function search($parsedlogline) { if ($this->search != "") { - return strstr($parsedlog->logline, $this->search) !== false; + return strstr($parsedlogline->logline, $this->search) !== false; } return true; } @@ -138,8 +138,8 @@ class ParsedLogIterator implements \Iterator * Read a line from reader and parse. * Returns null if limit is reached or the reader is invalid. * - * @param ParsedLog $parsedlog - * @return ?ParsedLog + * @param ParsedLogLine $parsedlogline + * @return ?ParsedLogLine */ private function read() { @@ -149,22 +149,34 @@ class ParsedLogIterator implements \Iterator } $line = $this->reader->current(); - return new ParsedLog($this->reader->key(), $line); + return new ParsedLogLine($this->reader->key(), $line); } + + /** + * Fetch next parsed log line which match with filters or search and + * set it as current iterator value. + * + * @see Iterator::next() + * @return void + */ public function next() { $parsed = $this->read(); - // if read() has not retuned none and - // the line don't match filters or search - // read the next line while (is_null($parsed) == false && !($this->filter($parsed) && $this->search($parsed))) { $parsed = $this->read(); } $this->value = $parsed; } + + /** + * Rewind the iterator to the first matching log line + * + * @see Iterator::rewind() + * @return void + */ public function rewind() { $this->value = null; @@ -172,16 +184,35 @@ class ParsedLogIterator implements \Iterator $this->next(); } + /** + * Return current parsed log line number + * + * @see Iterator::key() + * @see ReversedFileReader::key() + * @return int + */ public function key() { return $this->reader->key(); } + /** + * Return current iterator value + * + * @see Iterator::current() + * @return ?ParsedLogLing + */ public function current() { return $this->value; } + /** + * Checks if current iterator value is valid, that is, not null + * + * @see Iterator::valid() + * @return bool + */ public function valid() { return ! is_null($this->value); diff --git a/src/Object/Log/ParsedLog.php b/src/Object/Log/ParsedLogLine.php similarity index 76% rename from src/Object/Log/ParsedLog.php rename to src/Object/Log/ParsedLogLine.php index b1bc82112..93a15374b 100644 --- a/src/Object/Log/ParsedLog.php +++ b/src/Object/Log/ParsedLogLine.php @@ -24,7 +24,7 @@ namespace Friendica\Object\Log; /** * Parse a log line and offer some utility methods */ -class ParsedLog +class ParsedLogLine { const REGEXP = '/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^ ]*) (\w+) \[(\w*)\]: (.*)/'; @@ -64,16 +64,23 @@ class ParsedLog private function parse($logline) { + $this->logline = $logline; + // if data is empty is serialized as '[]'. To ease the parsing // let's replace it with '{""}'. It will be replaced by null later $logline = str_replace(' [] - {', ' {""} - {', $logline); - // here we hope that there will not be the string ' - {' inside the $jsonsource value - list($logline, $jsonsource) = explode(' - {', $logline); - $jsonsource = '{' . $jsonsource; + + if (strstr($logline, ' - {') === false) { + // the log line is not well formed + $jsonsource = null; + } else { + // here we hope that there will not be the string ' - {' inside the $jsonsource value + list($logline, $jsonsource) = explode(' - {', $logline); + $jsonsource = '{' . $jsonsource; + } $jsondata = null; - if (strpos($logline, '{"') > 0) { list($logline, $jsondata) = explode('{"', $logline, 2); @@ -82,15 +89,20 @@ class ParsedLog preg_match(self::REGEXP, $logline, $matches); - $this->date = $matches[1]; - $this->context = $matches[2]; - $this->level = $matches[3]; - $this->message = trim($matches[4]); - $this->data = $jsondata == '{""}' ? null : $jsondata; - $this->source = $jsonsource; - $this->try_fix_json(); + if (count($matches) == 0) { + // regexp not matching + $this->message = $this->logline; + } else { + $this->date = $matches[1]; + $this->context = $matches[2]; + $this->level = $matches[3]; + $this->message = $matches[4]; + $this->data = $jsondata == '{""}' ? null : $jsondata; + $this->source = $jsonsource; + $this->tryfixjson(); + } - $this->logline = $logline; + $this->message = trim($this->message); } /** @@ -101,7 +113,7 @@ class ParsedLog * This method try to parse the found json and if it fails, search for next '{' * in json data and retry */ - private function try_fix_json() + private function tryfixjson() { if (is_null($this->data) || $this->data == '') { return; @@ -112,10 +124,15 @@ class ParsedLog // try to find next { in $str and move string before to 'message' $pos = strpos($this->data, '{', 1); + if ($pos === false) { + $this->message .= $this->data; + $this->data = null; + return; + } $this->message .= substr($this->data, 0, $pos); $this->data = substr($this->data, $pos); - $this->try_fix_json(); + $this->tryfixjson(); } } @@ -124,7 +141,7 @@ class ParsedLog * * @return array */ - public function get_data() + public function getData() { $data = json_decode($this->data, true); if ($data) { @@ -140,7 +157,7 @@ class ParsedLog * * @return array */ - public function get_source() + public function getSource() { return json_decode($this->source, true); } diff --git a/src/Util/ReversedFileReader.php b/src/Util/ReversedFileReader.php index c4fe131ae..248792f17 100644 --- a/src/Util/ReversedFileReader.php +++ b/src/Util/ReversedFileReader.php @@ -70,6 +70,11 @@ class ReversedFileReader implements \Iterator return $this; } + /** + * Read $size bytes behind last position + * + * @return string + */ private function _read($size) { $this->pos -= $size; @@ -77,6 +82,12 @@ class ReversedFileReader implements \Iterator return fread($this->fh, $size); } + /** + * Read next line from end of file + * Return null if no lines are left to read + * + * @return ?string + */ private function _readline() { $buffer = & $this->buffer; @@ -91,12 +102,24 @@ class ReversedFileReader implements \Iterator } } + /** + * Fetch next line from end and set it as current iterator value. + * + * @see Iterator::next() + * @return void + */ public function next() { ++$this->key; $this->value = $this->_readline(); } + /** + * Rewind iterator to the first line at the end of file + * + * @see Iterator::rewind() + * @return void + */ public function rewind() { if ($this->filesize > 0) { @@ -108,16 +131,34 @@ class ReversedFileReader implements \Iterator } } + /** + * Return current line number, starting from zero at the end of file + * + * @see Iterator::key() + * @return int + */ public function key() { return $this->key; } + /** + * Return current line + * + * @see Iterator::current() + * @return string + */ public function current() { return $this->value; } + /** + * Checks if current iterator value is valid, that is, we readed all lines in files + * + * @see Iterator::valid() + * @return bool + */ public function valid() { return ! is_null($this->value); diff --git a/tests/src/Model/Log/ParsedLogIteratorTest.php b/tests/src/Model/Log/ParsedLogIteratorTest.php index 846deab1c..df9f8393a 100644 --- a/tests/src/Model/Log/ParsedLogIteratorTest.php +++ b/tests/src/Model/Log/ParsedLogIteratorTest.php @@ -42,12 +42,7 @@ class ParsedLogIteratorTest extends TestCase protected function setUp(): void { - $logfile = dirname(__DIR__) . DIRECTORY_SEPARATOR . - '..' . DIRECTORY_SEPARATOR . - '..' . DIRECTORY_SEPARATOR . - 'datasets' . DIRECTORY_SEPARATOR . - 'log' . DIRECTORY_SEPARATOR . - 'friendica.log.txt'; + $logfile = dirname(__DIR__) . '/../../datasets/log/friendica.log.txt'; $reader = new ReversedFileReader(); $this->pli = new ParsedLogIterator($reader); diff --git a/tests/src/Object/Log/ParsedLogTest.php b/tests/src/Object/Log/ParsedLogLineTest.php similarity index 54% rename from tests/src/Object/Log/ParsedLogTest.php rename to tests/src/Object/Log/ParsedLogLineTest.php index ff3d06f55..9705c34ea 100644 --- a/tests/src/Object/Log/ParsedLogTest.php +++ b/tests/src/Object/Log/ParsedLogLineTest.php @@ -21,17 +21,17 @@ namespace Friendica\Test\src\Object\Log; -use Friendica\Object\Log\ParsedLog; +use Friendica\Object\Log\ParsedLogLine; use PHPUnit\Framework\TestCase; /** * Log parser testing class */ -class ParsedLogTest extends TestCase +class ParsedLogLineTest extends TestCase { public static function do_log_line($logline, $expected_data) { - $parsed = new ParsedLog(0, $logline); + $parsed = new ParsedLogLine(0, $logline); foreach ($expected_data as $k => $v) { self::assertSame($parsed->$k, $v, '"'.$k.'" does not match expectation'); } @@ -90,4 +90,94 @@ class ParsedLogTest extends TestCase ] ); } + + /** + * test non conforming log line + */ + public function testNonConformingLogLine() + { + self::do_log_line( + 'this log line is not formatted as expected', + [ + 'date' => null, + 'context' => null, + 'level' => null, + 'message' => 'this log line is not formatted as expected', + 'data' => null, + 'source' => null, + ] + ); + } + + /** + * test missing source + */ + public function testMissingSource() + { + self::do_log_line( + '2021-05-24T15:30:01Z worker [NOTICE]: Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10 {"worker_id":"ece8fc8","worker_cmd":"Cron"}', + [ + 'date' => '2021-05-24T15:30:01Z', + 'context' => 'worker', + 'level' => 'NOTICE', + 'message' => 'Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10', + 'data' => '{"worker_id":"ece8fc8","worker_cmd":"Cron"}', + 'source' => null, + ] + ); + } + + /** + * test missing data + */ + public function testMissingData() + { + self::do_log_line( + '2021-05-24T15:30:01Z worker [NOTICE]: Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10 - {"file":"Worker.php","line":786,"function":"tooMuchWorkers","uid":"364d3c","process_id":20754}', + [ + 'date' => '2021-05-24T15:30:01Z', + 'context' => 'worker', + 'level' => 'NOTICE', + 'message' => 'Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10', + 'data' => null, + 'source' => '{"file":"Worker.php","line":786,"function":"tooMuchWorkers","uid":"364d3c","process_id":20754}', + ] + ); + } + + /** + * test missing data and source + */ + public function testMissingDataAndSource() + { + self::do_log_line( + '2021-05-24T15:30:01Z worker [NOTICE]: Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10', + [ + 'date' => '2021-05-24T15:30:01Z', + 'context' => 'worker', + 'level' => 'NOTICE', + 'message' => 'Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10', + 'data' => null, + 'source' => null, + ] + ); + } + + /** + * test missing source and invalid data + */ + public function testMissingSourceAndInvalidData() + { + self::do_log_line( + '2021-05-24T15:30:01Z worker [NOTICE]: Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10 {"invalidjson {really', + [ + 'date' => '2021-05-24T15:30:01Z', + 'context' => 'worker', + 'level' => 'NOTICE', + 'message' => 'Load: 0.01/20 - processes: 0/1/6 (0:0, 30:1) - maximum: 10/10 {"invalidjson {really', + 'data' => null, + 'source' => null, + ] + ); + } } diff --git a/view/js/module/admin/logs/view.js b/view/js/module/admin/logs/view.js index 45d08a5ed..5ad4e0a71 100644 --- a/view/js/module/admin/logs/view.js +++ b/view/js/module/admin/logs/view.js @@ -1,7 +1,33 @@ -function log_show_details(id) { +(function(){ + function log_show_details(elm) { + const id = elm.id; + var hidden = true; + document + .querySelectorAll('[data-id="' + id + '"]') + .forEach(edetails => { + hidden = edetails.classList.toggle('hidden'); + }); + document + .querySelectorAll('[aria-expanded="true"]') + .forEach(eexpanded => { + eexpanded.setAttribute('aria-expanded', false); + }); + + if (!hidden) { + elm.setAttribute('aria-expanded', true); + } + } + document - .querySelectorAll('[data-id="' + id + '"]') + .querySelectorAll('.log-event') .forEach(elm => { - elm.classList.toggle('hidden') + elm.addEventListener("click", evt => { + log_show_details(evt.currentTarget); + }); + elm.addEventListener("keydown", evt => { + if (evt.keyCode == 13 || evt.keyCode == 32) { + log_show_details(evt.currentTarget); + } + }); }); -} +})(); \ No newline at end of file diff --git a/view/templates/admin/logs/view.tpl b/view/templates/admin/logs/view.tpl index 523cb6a20..e15a4a01b 100644 --- a/view/templates/admin/logs/view.tpl +++ b/view/templates/admin/logs/view.tpl @@ -1,7 +1,7 @@

{{$title}} - {{$page}}

-

{{$logname}}

+

{{$logname}}

{{if $error }}

{{$error nofilter}}

@@ -44,14 +44,18 @@ {{foreach $data as $row}} - + {{$row->date}} {{$row->level}} {{$row->context}} {{$row->message}} Data - {{foreach $row->get_data() as $k=>$v}} + {{foreach $row->getData() as $k=>$v}} {{$k}} @@ -60,7 +64,7 @@ {{/foreach}} Source - {{foreach $row->get_source() as $k=>$v}} + {{foreach $row->getSource() as $k=>$v}} {{$k}} {{$v}} diff --git a/view/theme/frio/css/style.css b/view/theme/frio/css/style.css index 1bf908e1d..bcf1557ae 100644 --- a/view/theme/frio/css/style.css +++ b/view/theme/frio/css/style.css @@ -98,6 +98,13 @@ details summary { display: list-item; } +/** + * clickable table rows + */ +.table > tbody > td[role="button"] { + cursor: pointer; +} + /** * mobile aside */ diff --git a/view/theme/frio/js/module/admin/logs/view.js b/view/theme/frio/js/module/admin/logs/view.js index 149d019e9..245518822 100644 --- a/view/theme/frio/js/module/admin/logs/view.js +++ b/view/theme/frio/js/module/admin/logs/view.js @@ -21,6 +21,12 @@ $(function(){ $(".log-event").on("click", function(ev) { show_details_for_element(ev.currentTarget); }); + $(".log-event").on("keydown", function(ev) { + if (ev.keyCode == 13 || ev.keyCode == 32) { + show_details_for_element(ev.currentTarget); + } + }); + $("[data-previous").on("click", function(ev){ var currentid = document.getElementById("logdetail").dataset.rowId; @@ -37,9 +43,15 @@ $(function(){ }); - function show_details_for_element(element) { - var $modal = $("#logdetail"); + const $modal = $("#logdetail"); + $modal.on("hidden.bs.modal", function(ev){ + document + .querySelectorAll('[aria-expanded="true"]') + .forEach(elm => elm.setAttribute("aria-expanded", false)) + }); + + function show_details_for_element(element) { $modal[0].dataset.rowId = element.id; var tr = $modal.find(".main-data tbody tr")[0]; @@ -64,6 +76,7 @@ $(function(){ $("[data-next").prop("disabled", $(element).next().length == 0); $modal.modal({}) + element.setAttribute("aria-expanded", true); } function recursive_details(s, data, lev=0) { diff --git a/view/theme/frio/templates/admin/logs/view.tpl b/view/theme/frio/templates/admin/logs/view.tpl index 1bc026916..1e1123fbd 100755 --- a/view/theme/frio/templates/admin/logs/view.tpl +++ b/view/theme/frio/templates/admin/logs/view.tpl @@ -1,7 +1,7 @@

{{$title}} - {{$page}}

-

{{$logname}}

+

{{$logname}}

{{if $error }}

{{$error nofilter}}

@@ -59,7 +59,9 @@ {{foreach $data as $row}} - {{$row->date}}