Add search and filter to log view
This commit is contained in:
parent
b8fc6a8c02
commit
5b9aeeeca9
5 changed files with 248 additions and 81 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,38 @@
|
||||||
<p>{{$error nofilter}}</p>
|
<p>{{$error nofilter}}</p>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
<form>
|
||||||
|
<p>
|
||||||
|
<input type="search" name="q" value="{{$q}}" placeholder="search"></input>
|
||||||
|
<input type="Submit" value="search">
|
||||||
|
<a href="/admin/logs/view">clear</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th>Level</th>
|
<th>
|
||||||
<th>Context</th>
|
<select name="level" onchange="this.form.submit()">
|
||||||
|
{{foreach $filtersvalues.level as $v }}
|
||||||
|
<option {{if $filters.level == $v}}selected{{/if}} value="{{$v}}">
|
||||||
|
{{if $v == ""}}Level{{/if}}
|
||||||
|
{{$v}}
|
||||||
|
</option>
|
||||||
|
{{/foreach}}
|
||||||
|
</select>
|
||||||
|
</th>
|
||||||
|
<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>
|
<th>Message</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -24,6 +50,7 @@
|
||||||
<td>{{$row->context}}</td>
|
<td>{{$row->context}}</td>
|
||||||
<td>{{$row->message}}</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}}
|
{{foreach $row->get_data() as $k=>$v}}
|
||||||
<tr class="hidden" data-id="ev-{{$row->id}}">
|
<tr class="hidden" data-id="ev-{{$row->id}}">
|
||||||
<th>{{$k}}</th>
|
<th>{{$k}}</th>
|
||||||
|
@ -42,5 +69,6 @@
|
||||||
{{/foreach}}
|
{{/foreach}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</form>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue