diff --git a/src/Model/Log/ParsedLogIterator.php b/src/Model/Log/ParsedLogIterator.php index 621381ac55..15714454e7 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 339a28b6a5..a512507278 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 21bd538765..f48ce400e7 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 8a3083f7d8..eeedc1a5cc 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 166dea0ba9..523cb6a201 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}}