* 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;
}
- public function next()
+ /**
+ * Check if parsed log line match filters.
+ * Always match if no filters are set.
+ *
+ * @param ParsedLog $parsedlog
+ * @return bool
+ */
+ private function filter($parsedlog)
{
- $this->reader->next();
- if ($this->_limit > 0 && $this->reader->key() > $this->_limit) {
- $this->_value = null;
- return;
+ $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;
}
- if ($this->reader->valid()) {
- $line = $this->reader->current();
- $this->_value = new ParsedLog($this->reader->key(), $line);
- } else {
- $this->_value = null;
+ 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()
+ {
+ $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();
}
public function current()
{
- return $this->_value;
+ return $this->value;
}
public function valid()
{
- return ! is_null($this->_value);
+ return ! is_null($this->value);
}
}
use Friendica\Core\Theme;
use Friendica\Module\BaseAdmin;
use Friendica\Model\Log\ParsedLogIterator;
+use Psr\Log\LogLevel;
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 <strong>%1$s</strong> log file.\r\n<br/>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 <strong>%1$s</strong> log file.\r\n<br/>Check to see if file %1$s is readable.', $f);
}
'$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'),
]);
}
}
{
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)
$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();
}
}
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');
// 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) {
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);
}
}
<p>{{$error nofilter}}</p>
</div>
{{else}}
- <table>
- <thead>
- <tr>
- <th>Date</th>
- <th>Level</th>
- <th>Context</th>
- <th>Message</th>
- </tr>
- </thead>
- <tbody>
- {{foreach $data as $row}}
- <tr id="ev-{{$row->id}}" onClick="log_show_details('ev-{{$row->id}}')">
- <td>{{$row->date}}</td>
- <td>{{$row->level}}</td>
- <td>{{$row->context}}</td>
- <td>{{$row->message}}</td>
- </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>
+ <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>
+ <thead>
+ <tr>
+ <th>Date</th>
+ <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>
</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>
+ </thead>
+ <tbody>
+ {{foreach $data as $row}}
+ <tr id="ev-{{$row->id}}" onClick="log_show_details('ev-{{$row->id}}')">
+ <td>{{$row->date}}</td>
+ <td>{{$row->level}}</td>
+ <td>{{$row->context}}</td>
+ <td>{{$row->message}}</td>
</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}}
- </tbody>
- </table>
+ </tbody>
+ </table>
+ </form>
{{/if}}
</div>