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`
This commit is contained in:
fabrixxm 2021-08-20 09:47:53 +02:00
parent 5520f100b2
commit 7f695197aa
10 changed files with 280 additions and 54 deletions

View File

@ -22,10 +22,10 @@
namespace Friendica\Model\Log; namespace Friendica\Model\Log;
use Friendica\Util\ReversedFileReader; 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 * Uses `\Friendica\Util\ReversedFileReader` to fetch log lines
* from newest to oldest. * from newest to oldest.
@ -35,7 +35,7 @@ class ParsedLogIterator implements \Iterator
/** @var \Iterator */ /** @var \Iterator */
private $reader; private $reader;
/** @var ParsedLog current iterator value*/ /** @var ParsedLogLine current iterator value*/
private $value = null; private $value = null;
/** @var int max number of lines to read */ /** @var int max number of lines to read */
@ -100,19 +100,19 @@ class ParsedLogIterator implements \Iterator
* Check if parsed log line match filters. * Check if parsed log line match filters.
* Always match if no filters are set. * Always match if no filters are set.
* *
* @param ParsedLog $parsedlog * @param ParsedLogLine $parsedlogline
* @return bool * @return bool
*/ */
private function filter($parsedlog) private function filter($parsedlogline)
{ {
$match = true; $match = true;
foreach ($this->filters as $filter => $filtervalue) { foreach ($this->filters as $filter => $filtervalue) {
switch ($filter) { switch ($filter) {
case "level": case "level":
$match = $match && ($parsedlog->level == strtoupper($filtervalue)); $match = $match && ($parsedlogline->level == strtoupper($filtervalue));
break; break;
case "context": case "context":
$match = $match && ($parsedlog->context == $filtervalue); $match = $match && ($parsedlogline->context == $filtervalue);
break; break;
} }
} }
@ -123,13 +123,13 @@ class ParsedLogIterator implements \Iterator
* Check if parsed log line match search. * Check if parsed log line match search.
* Always match if no search query is set. * Always match if no search query is set.
* *
* @param ParsedLog $parsedlog * @param ParsedLogLine $parsedlogline
* @return bool * @return bool
*/ */
private function search($parsedlog) private function search($parsedlogline)
{ {
if ($this->search != "") { if ($this->search != "") {
return strstr($parsedlog->logline, $this->search) !== false; return strstr($parsedlogline->logline, $this->search) !== false;
} }
return true; return true;
} }
@ -138,8 +138,8 @@ class ParsedLogIterator implements \Iterator
* Read a line from reader and parse. * Read a line from reader and parse.
* Returns null if limit is reached or the reader is invalid. * Returns null if limit is reached or the reader is invalid.
* *
* @param ParsedLog $parsedlog * @param ParsedLogLine $parsedlogline
* @return ?ParsedLog * @return ?ParsedLogLine
*/ */
private function read() private function read()
{ {
@ -149,22 +149,34 @@ class ParsedLogIterator implements \Iterator
} }
$line = $this->reader->current(); $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() public function next()
{ {
$parsed = $this->read(); $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))) { while (is_null($parsed) == false && !($this->filter($parsed) && $this->search($parsed))) {
$parsed = $this->read(); $parsed = $this->read();
} }
$this->value = $parsed; $this->value = $parsed;
} }
/**
* Rewind the iterator to the first matching log line
*
* @see Iterator::rewind()
* @return void
*/
public function rewind() public function rewind()
{ {
$this->value = null; $this->value = null;
@ -172,16 +184,35 @@ class ParsedLogIterator implements \Iterator
$this->next(); $this->next();
} }
/**
* Return current parsed log line number
*
* @see Iterator::key()
* @see ReversedFileReader::key()
* @return int
*/
public function key() public function key()
{ {
return $this->reader->key(); return $this->reader->key();
} }
/**
* Return current iterator value
*
* @see Iterator::current()
* @return ?ParsedLogLing
*/
public function current() public function current()
{ {
return $this->value; return $this->value;
} }
/**
* Checks if current iterator value is valid, that is, not null
*
* @see Iterator::valid()
* @return bool
*/
public function valid() public function valid()
{ {
return ! is_null($this->value); return ! is_null($this->value);

View File

@ -24,7 +24,7 @@ namespace Friendica\Object\Log;
/** /**
* Parse a log line and offer some utility methods * 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*)\]: (.*)/'; 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) private function parse($logline)
{ {
$this->logline = $logline;
// if data is empty is serialized as '[]'. To ease the parsing // if data is empty is serialized as '[]'. To ease the parsing
// let's replace it with '{""}'. It will be replaced by null later // let's replace it with '{""}'. It will be replaced by null later
$logline = str_replace(' [] - {', ' {""} - {', $logline); $logline = str_replace(' [] - {', ' {""} - {', $logline);
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 // here we hope that there will not be the string ' - {' inside the $jsonsource value
list($logline, $jsonsource) = explode(' - {', $logline); list($logline, $jsonsource) = explode(' - {', $logline);
$jsonsource = '{' . $jsonsource; $jsonsource = '{' . $jsonsource;
}
$jsondata = null; $jsondata = null;
if (strpos($logline, '{"') > 0) { if (strpos($logline, '{"') > 0) {
list($logline, $jsondata) = explode('{"', $logline, 2); list($logline, $jsondata) = explode('{"', $logline, 2);
@ -82,15 +89,20 @@ class ParsedLog
preg_match(self::REGEXP, $logline, $matches); preg_match(self::REGEXP, $logline, $matches);
if (count($matches) == 0) {
// regexp not matching
$this->message = $this->logline;
} else {
$this->date = $matches[1]; $this->date = $matches[1];
$this->context = $matches[2]; $this->context = $matches[2];
$this->level = $matches[3]; $this->level = $matches[3];
$this->message = trim($matches[4]); $this->message = $matches[4];
$this->data = $jsondata == '{""}' ? null : $jsondata; $this->data = $jsondata == '{""}' ? null : $jsondata;
$this->source = $jsonsource; $this->source = $jsonsource;
$this->try_fix_json(); $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 '{' * 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() private function tryfixjson()
{ {
if (is_null($this->data) || $this->data == '') { if (is_null($this->data) || $this->data == '') {
return; return;
@ -112,10 +124,15 @@ class ParsedLog
// 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->data, '{', 1); $pos = strpos($this->data, '{', 1);
if ($pos === false) {
$this->message .= $this->data;
$this->data = null;
return;
}
$this->message .= substr($this->data, 0, $pos); $this->message .= substr($this->data, 0, $pos);
$this->data = substr($this->data, $pos); $this->data = substr($this->data, $pos);
$this->try_fix_json(); $this->tryfixjson();
} }
} }
@ -124,7 +141,7 @@ class ParsedLog
* *
* @return array * @return array
*/ */
public function get_data() public function getData()
{ {
$data = json_decode($this->data, true); $data = json_decode($this->data, true);
if ($data) { if ($data) {
@ -140,7 +157,7 @@ class ParsedLog
* *
* @return array * @return array
*/ */
public function get_source() public function getSource()
{ {
return json_decode($this->source, true); return json_decode($this->source, true);
} }

View File

@ -70,6 +70,11 @@ class ReversedFileReader implements \Iterator
return $this; return $this;
} }
/**
* Read $size bytes behind last position
*
* @return string
*/
private function _read($size) private function _read($size)
{ {
$this->pos -= $size; $this->pos -= $size;
@ -77,6 +82,12 @@ class ReversedFileReader implements \Iterator
return fread($this->fh, $size); 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() private function _readline()
{ {
$buffer = & $this->buffer; $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() public function next()
{ {
++$this->key; ++$this->key;
$this->value = $this->_readline(); $this->value = $this->_readline();
} }
/**
* Rewind iterator to the first line at the end of file
*
* @see Iterator::rewind()
* @return void
*/
public function rewind() public function rewind()
{ {
if ($this->filesize > 0) { 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() public function key()
{ {
return $this->key; return $this->key;
} }
/**
* Return current line
*
* @see Iterator::current()
* @return string
*/
public function current() public function current()
{ {
return $this->value; 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() public function valid()
{ {
return ! is_null($this->value); return ! is_null($this->value);

View File

@ -42,12 +42,7 @@ class ParsedLogIteratorTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
$logfile = dirname(__DIR__) . DIRECTORY_SEPARATOR . $logfile = dirname(__DIR__) . '/../../datasets/log/friendica.log.txt';
'..' . DIRECTORY_SEPARATOR .
'..' . DIRECTORY_SEPARATOR .
'datasets' . DIRECTORY_SEPARATOR .
'log' . DIRECTORY_SEPARATOR .
'friendica.log.txt';
$reader = new ReversedFileReader(); $reader = new ReversedFileReader();
$this->pli = new ParsedLogIterator($reader); $this->pli = new ParsedLogIterator($reader);

View File

@ -21,17 +21,17 @@
namespace Friendica\Test\src\Object\Log; namespace Friendica\Test\src\Object\Log;
use Friendica\Object\Log\ParsedLog; use Friendica\Object\Log\ParsedLogLine;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
* Log parser testing class * Log parser testing class
*/ */
class ParsedLogTest extends TestCase class ParsedLogLineTest extends TestCase
{ {
public static function do_log_line($logline, $expected_data) 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) { foreach ($expected_data as $k => $v) {
self::assertSame($parsed->$k, $v, '"'.$k.'" does not match expectation'); 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,
]
);
}
} }

View File

@ -1,7 +1,33 @@
function log_show_details(id) { (function(){
function log_show_details(elm) {
const id = elm.id;
var hidden = true;
document document
.querySelectorAll('[data-id="' + id + '"]') .querySelectorAll('[data-id="' + id + '"]')
.forEach(elm => { .forEach(edetails => {
elm.classList.toggle('hidden') 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('.log-event')
.forEach(elm => {
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);
}
});
});
})();

View File

@ -1,7 +1,7 @@
<div id="adminpage"> <div id="adminpage">
<h1>{{$title}} - {{$page}}</h1> <h1>{{$title}} - {{$page}}</h1>
<h3>{{$logname}}</h3> <h2>{{$logname}}</h2>
{{if $error }} {{if $error }}
<div id="admin-error-message-wrapper" class="alert alert-warning"> <div id="admin-error-message-wrapper" class="alert alert-warning">
<p>{{$error nofilter}}</p> <p>{{$error nofilter}}</p>
@ -44,14 +44,18 @@
</thead> </thead>
<tbody> <tbody>
{{foreach $data as $row}} {{foreach $data as $row}}
<tr id="ev-{{$row->id}}" onClick="log_show_details('ev-{{$row->id}}')"> <tr id="ev-{{$row->id}}" class="log-event"
role="button" tabIndex="0"
aria-label="View details" aria-haspopup="true" aria-expanded="false"
style="cursor:pointer;"
title="Click to view details">
<td>{{$row->date}}</td> <td>{{$row->date}}</td>
<td>{{$row->level}}</td> <td>{{$row->level}}</td>
<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> <tr class="hidden" data-id="ev-{{$row->id}}"><th colspan="4">Data</th></tr>
{{foreach $row->get_data() as $k=>$v}} {{foreach $row->getData() 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>
<td colspan="3"> <td colspan="3">
@ -60,7 +64,7 @@
</tr> </tr>
{{/foreach}} {{/foreach}}
<tr class="hidden" data-id="ev-{{$row->id}}"><th colspan="4">Source</th></tr> <tr class="hidden" data-id="ev-{{$row->id}}"><th colspan="4">Source</th></tr>
{{foreach $row->get_source() as $k=>$v}} {{foreach $row->getSource() 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>
<td colspan="3">{{$v}}</td> <td colspan="3">{{$v}}</td>

View File

@ -98,6 +98,13 @@ details summary {
display: list-item; display: list-item;
} }
/**
* clickable table rows
*/
.table > tbody > td[role="button"] {
cursor: pointer;
}
/** /**
* mobile aside * mobile aside
*/ */

View File

@ -21,6 +21,12 @@ $(function(){
$(".log-event").on("click", function(ev) { $(".log-event").on("click", function(ev) {
show_details_for_element(ev.currentTarget); 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){ $("[data-previous").on("click", function(ev){
var currentid = document.getElementById("logdetail").dataset.rowId; var currentid = document.getElementById("logdetail").dataset.rowId;
@ -37,9 +43,15 @@ $(function(){
}); });
function show_details_for_element(element) { const $modal = $("#logdetail");
var $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; $modal[0].dataset.rowId = element.id;
var tr = $modal.find(".main-data tbody tr")[0]; var tr = $modal.find(".main-data tbody tr")[0];
@ -64,6 +76,7 @@ $(function(){
$("[data-next").prop("disabled", $(element).next().length == 0); $("[data-next").prop("disabled", $(element).next().length == 0);
$modal.modal({}) $modal.modal({})
element.setAttribute("aria-expanded", true);
} }
function recursive_details(s, data, lev=0) { function recursive_details(s, data, lev=0) {

View File

@ -1,7 +1,7 @@
<div id="adminpage"> <div id="adminpage">
<h1>{{$title}} - {{$page}}</h1> <h1>{{$title}} - {{$page}}</h1>
<h3>{{$logname}}</h3> <h2>{{$logname}}</h2>
{{if $error }} {{if $error }}
<div id="admin-error-message-wrapper" class="alert alert-warning"> <div id="admin-error-message-wrapper" class="alert alert-warning">
<p>{{$error nofilter}}</p> <p>{{$error nofilter}}</p>
@ -60,6 +60,8 @@
<tbody> <tbody>
{{foreach $data as $row}} {{foreach $data as $row}}
<tr id="ev-{{$row->id}}" class="log-event" <tr id="ev-{{$row->id}}" class="log-event"
role="button" tabIndex="0"
aria-label="View details" aria-haspopup="true" aria-expanded="false"
data-data="{{$row->data}}" data-source="{{$row->source}}"> data-data="{{$row->data}}" data-source="{{$row->source}}">
<td>{{$row->date}}</td> <td>{{$row->date}}</td>
<td class=" <td class="