Add search and filter to log view

This commit is contained in:
fabrixxm 2021-05-24 21:47:10 +02:00
parent b8fc6a8c02
commit 5b9aeeeca9
5 changed files with 248 additions and 81 deletions

View File

@ -28,36 +28,113 @@ use \Friendica\Object\Log\ParsedLog;
* An iterator which returns `\Friendica\Objec\Log\ParsedLog` instances * An iterator which returns `\Friendica\Objec\Log\ParsedLog` instances
* *
* Uses `\Friendica\Util\ReversedFileReader` to fetch log lines * Uses `\Friendica\Util\ReversedFileReader` to fetch log lines
* from newest to oldest * from newest to oldest.
*/ */
class ParsedLogIterator implements \Iterator 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->reader = new ReversedFileReader($filename);
$this->_value = null; $this->value = null;
$this->_limit = $limit; $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() public function next()
{ {
$this->reader->next(); $parsed = $this->read();
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;
}
}
// 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() public function rewind()
{ {
$this->_value = null; $this->value = null;
$this->reader->rewind(); $this->reader->rewind();
$this->next(); $this->next();
} }
@ -69,12 +146,12 @@ class ParsedLogIterator implements \Iterator
public function current() public function current()
{ {
return $this->_value; return $this->value;
} }
public function valid() public function valid()
{ {
return ! is_null($this->_value); return ! is_null($this->value);
} }
} }

View File

@ -26,6 +26,7 @@ use Friendica\Core\Renderer;
use Friendica\Core\Theme; use Friendica\Core\Theme;
use Friendica\Module\BaseAdmin; use Friendica\Module\BaseAdmin;
use Friendica\Model\Log\ParsedLogIterator; use Friendica\Model\Log\ParsedLogIterator;
use Psr\Log\LogLevel;
class View extends BaseAdmin class View extends BaseAdmin
{ {
@ -43,11 +44,34 @@ class View extends BaseAdmin
$error = null; $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)) { if (!file_exists($f)) {
$error = DI::l10n()->t('Error trying to open <strong>%1$s</strong> log file.\r\n<br/>Check to see if file %1$s exist and is readable.', $f); $error = DI::l10n()->t('Error trying to open <strong>%1$s</strong> log file.\r\n<br/>Check to see if file %1$s exist and is readable.', $f);
} else { } else {
try { try {
$data = new ParsedLogIterator($f, self::LIMIT); $data = new ParsedLogIterator($f, self::LIMIT, $filters, $search);
} catch (Exception $e) { } catch (Exception $e) {
$error = DI::l10n()->t('Couldn\'t open <strong>%1$s</strong> log file.\r\n<br/>Check to see if file %1$s is readable.', $f); $error = DI::l10n()->t('Couldn\'t open <strong>%1$s</strong> log file.\r\n<br/>Check to see if file %1$s is readable.', $f);
} }
@ -56,8 +80,11 @@ class View extends BaseAdmin
'$title' => DI::l10n()->t('Administration'), '$title' => DI::l10n()->t('Administration'),
'$page' => DI::l10n()->t('View Logs'), '$page' => DI::l10n()->t('View Logs'),
'$data' => $data, '$data' => $data,
'$q' => $search,
'$filters' => $filters,
'$filtersvalues' => $filters_valid_values,
'$error' => $error, '$error' => $error,
'$logname' => DI::config()->get('system', 'logfile') '$logname' => DI::config()->get('system', 'logfile'),
]); ]);
} }
} }

View File

@ -27,22 +27,38 @@ class ParsedLog
{ {
const REGEXP = '/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^ ]*) (\w+) \[(\w*)\]: (.*)/'; const REGEXP = '/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^ ]*) (\w+) \[(\w*)\]: (.*)/';
/** @var int */
public $id = 0; public $id = 0;
/** @var string */
public $date = null; public $date = null;
/** @var string */
public $context = null; public $context = null;
/** @var string */
public $level = null; public $level = null;
/** @var string */
public $message = null; public $message = null;
/** @var string */
public $data = null; public $data = null;
/** @var string */
public $source = null; public $source = null;
/** @var string */
public $logline;
/** /**
* @param int line id
* @param string $logline Source log line to parse * @param string $logline Source log line to parse
*/ */
public function __construct(int $id, string $logline) public function __construct(int $id, string $logline)
{ {
$this->id = $id; $this->id = $id;
$this->parse($logline); $this->parse($logline);
$this->stop = false;
} }
private function parse($logline) private function parse($logline)
@ -60,30 +76,34 @@ class ParsedLog
$this->message = $matches[4]; $this->message = $matches[4];
$this->data = $jsondata; $this->data = $jsondata;
$this->source = $jsonsource; $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. * In log boundary between message and json data is not specified.
* If message contains '{' the parser thinks there starts the json data. * 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 '{' * This method try to parse the found json and if it fails, search for next '{'
* in json data and retry * 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; return;
} }
try { 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) { } catch (\JsonException $e) {
// try to find next { in $str and move string before to 'message' // 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->message .= substr($this->data, 0, $pos);
$this->$key = substr($this->key, $pos); $this->data = substr($this->data, $pos);
$this->try_fix_json($key); $this->try_fix_json();
} }
} }

View File

@ -32,6 +32,21 @@ class ReversedFileReader implements \Iterator
const BUFFER_SIZE = 4096; const BUFFER_SIZE = 4096;
const SEPARATOR = "\n"; 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) public function __construct($filename)
{ {
$this->_fh = fopen($filename, 'r'); $this->_fh = fopen($filename, 'r');
@ -39,25 +54,25 @@ class ReversedFileReader implements \Iterator
// this should use a custom exception. // this should use a custom exception.
throw \Exception("Unable to open $filename"); throw \Exception("Unable to open $filename");
} }
$this->_filesize = filesize($filename); $this->filesize = filesize($filename);
$this->_pos = -1; $this->pos = -1;
$this->_buffer = null; $this->buffer = null;
$this->_key = -1; $this->key = -1;
$this->_value = null; $this->value = null;
} }
public function _read($size) public function _read($size)
{ {
$this->_pos -= $size; $this->pos -= $size;
fseek($this->_fh, $this->_pos); fseek($this->_fh, $this->pos);
return fread($this->_fh, $size); return fread($this->_fh, $size);
} }
public function _readline() public function _readline()
{ {
$buffer =& $this->_buffer; $buffer =& $this->buffer;
while (true) { while (true) {
if ($this->_pos == 0) { if ($this->pos == 0) {
return array_pop($buffer); return array_pop($buffer);
} }
if (count($buffer) > 1) { if (count($buffer) > 1) {
@ -69,33 +84,33 @@ class ReversedFileReader implements \Iterator
public function next() public function next()
{ {
++$this->_key; ++$this->key;
$this->_value = $this->_readline(); $this->value = $this->_readline();
} }
public function rewind() public function rewind()
{ {
if ($this->_filesize > 0) { if ($this->filesize > 0) {
$this->_pos = $this->_filesize; $this->pos = $this->filesize;
$this->_value = null; $this->value = null;
$this->_key = -1; $this->key = -1;
$this->_buffer = explode(self::SEPARATOR, $this->_read($this->_filesize % self::BUFFER_SIZE ?: self::BUFFER_SIZE)); $this->buffer = explode(self::SEPARATOR, $this->_read($this->filesize % self::BUFFER_SIZE ?: self::BUFFER_SIZE));
$this->next(); $this->next();
} }
} }
public function key() public function key()
{ {
return $this->_key; return $this->key;
} }
public function current() public function current()
{ {
return $this->_value; return $this->value;
} }
public function valid() public function valid()
{ {
return ! is_null($this->_value); return ! is_null($this->value);
} }
} }

View File

@ -7,40 +7,68 @@
<p>{{$error nofilter}}</p> <p>{{$error nofilter}}</p>
</div> </div>
{{else}} {{else}}
<table> <form>
<thead> <p>
<tr> <input type="search" name="q" value="{{$q}}" placeholder="search"></input>
<th>Date</th> <input type="Submit" value="search">
<th>Level</th> <a href="/admin/logs/view">clear</a>
<th>Context</th> </p>
<th>Message</th>
</tr>
</thead> <table>
<tbody> <thead>
{{foreach $data as $row}} <tr>
<tr id="ev-{{$row->id}}" onClick="log_show_details('ev-{{$row->id}}')"> <th>Date</th>
<td>{{$row->date}}</td> <th>
<td>{{$row->level}}</td> <select name="level" onchange="this.form.submit()">
<td>{{$row->context}}</td> {{foreach $filtersvalues.level as $v }}
<td>{{$row->message}}</td> <option {{if $filters.level == $v}}selected{{/if}} value="{{$v}}">
</tr> {{if $v == ""}}Level{{/if}}
{{foreach $row->get_data() as $k=>$v}} {{$v}}
<tr class="hidden" data-id="ev-{{$row->id}}"> </option>
<th>{{$k}}</th> {{/foreach}}
<td colspan="3"> </select>
<pre>{{$v nofilter}}</pre> </th>
</td> <th>
<select name="context" onchange="this.form.submit()">
{{foreach $filtersvalues.context as $v }}
<option {{if $filters.context == $v}}selected{{/if}} value="{{$v}}">
{{if $v == ""}}Context{{/if}}
{{$v}}
</option>
{{/foreach}}
</select>
</th>
<th>Message</th>
</tr> </tr>
{{/foreach}} </thead>
<tr class="hidden" data-id="ev-{{$row->id}}"><th colspan="4">Source</th></tr> <tbody>
{{foreach $row->get_source() as $k=>$v}} {{foreach $data as $row}}
<tr class="hidden" data-id="ev-{{$row->id}}"> <tr id="ev-{{$row->id}}" onClick="log_show_details('ev-{{$row->id}}')">
<th>{{$k}}</th> <td>{{$row->date}}</td>
<td colspan="3">{{$v}}</td> <td>{{$row->level}}</td>
<td>{{$row->context}}</td>
<td>{{$row->message}}</td>
</tr> </tr>
<tr class="hidden" data-id="ev-{{$row->id}}"><th colspan="4">Data</th></tr>
{{foreach $row->get_data() as $k=>$v}}
<tr class="hidden" data-id="ev-{{$row->id}}">
<th>{{$k}}</th>
<td colspan="3">
<pre>{{$v nofilter}}</pre>
</td>
</tr>
{{/foreach}}
<tr class="hidden" data-id="ev-{{$row->id}}"><th colspan="4">Source</th></tr>
{{foreach $row->get_source() as $k=>$v}}
<tr class="hidden" data-id="ev-{{$row->id}}">
<th>{{$k}}</th>
<td colspan="3">{{$v}}</td>
</tr>
{{/foreach}}
{{/foreach}} {{/foreach}}
{{/foreach}} </tbody>
</tbody> </table>
</table> </form>
{{/if}} {{/if}}
</div> </div>