Added LogViewer class and CLI command

This commit is contained in:
Andy Miller
2019-01-31 15:39:46 -07:00
parent d50e5d954d
commit a3fea3d0fc
4 changed files with 221 additions and 0 deletions

View File

@@ -3,6 +3,7 @@
1. [](#new)
* Added index file support for Flex Objects
* Added `LogViewer` helper class and CLI command: `bin/grav logviewer`
1. [](#improved)
* Improved error detection for broken Flex Objects
* Removed apc and xcache support, made apc alias of apcu

View File

@@ -45,5 +45,6 @@ $app->addCommands(array(
new \Grav\Console\Cli\NewProjectCommand(),
new \Grav\Console\Cli\SchedulerCommand(),
new \Grav\Console\Cli\SecurityCommand(),
new \Grav\Console\Cli\LogViewerCommand(),
));
$app->run();

View File

@@ -0,0 +1,130 @@
<?php
/**
* @package Grav\Common\Helpers
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
class LogViewer
{
protected $pattern = '/\[(?P<date>.*)\] (?P<logger>\w+).(?P<level>\w+): (?P<message>.*[^ ]+) (?P<context>[^ ]+) (?P<extra>[^ ]+)/';
/**
* Get the objects of a tailed file
*
* @param $filepath
* @param int $lines
* @param bool $desc
* @return array
*/
public function objectTail($filepath, $lines = 1, $desc = true)
{
$data = $this->tail($filepath, $lines);
$tailed_log = explode(PHP_EOL, $data);
$line_objects = [];
foreach ($tailed_log as $line) {
$line_objects[] = $this->parse($line);
}
return $desc ? $line_objects : array_reverse($line_objects);
}
/**
* Optimized way to get just the last few entries of a log file
*
* @param $filepath
* @param int $lines
* @return bool|string
*/
public function tail($filepath, $lines = 1) {
$f = @fopen($filepath, "rb");
if ($f === false) return false;
else $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
fseek($f, -1, SEEK_END);
if (fread($f, 1) != "\n") $lines -= 1;
// Start reading
$output = '';
$chunk = '';
// While we would like more
while (ftell($f) > 0 && $lines >= 0) {
// Figure out how far back we should jump
$seek = min(ftell($f), $buffer);
// Do the jump (backwards, relative to where we are)
fseek($f, -$seek, SEEK_CUR);
// Read a chunk and prepend it to our output
$output = ($chunk = fread($f, $seek)) . $output;
// Jump back to where we started reading
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
// Decrease our line counter
$lines -= substr_count($chunk, "\n");
}
// While we have too many lines
// (Because of buffer size we might have read too many)
while ($lines++ < 0) {
// Find first newline and remove all text before that
$output = substr($output, strpos($output, "\n") + 1);
}
// Close file and return
fclose($f);
return trim($output);
}
public static function levelColor($level)
{
$colors = [
'DEBUG' => 'green',
'INFO' => 'cyan',
'NOTICE' => 'yellow',
'WARNING' => 'yellow',
'ERROR' => 'red',
'CRITICAL' => 'red',
'ALERT' => 'red',
'EMERGENCY' => 'magenta'
];
return $colors[$level] ?? 'white';
}
/**
* Parse a monolog row into array bits
*
* @param $line
* @return array
*/
public function parse($line)
{
if( !is_string($line) || strlen($line) === 0) {
return array();
}
preg_match($this->pattern, $line, $data);
if (!isset($data['date'])) {
return array();
}
preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches);
if (is_array($matches) && isset($matches[1])) {
$data['message'] = trim($matches[1]);
$data['trace'] = trim($matches[2]);
}
return array(
'date' => \DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
'logger' => $data['logger'],
'level' => $data['level'],
'message' => $data['message'],
'trace' => $data['trace'] ?? null,
'context' => json_decode($data['context'], true),
'extra' => json_decode($data['extra'], true)
);
}
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Common\Grav;
use Grav\Common\Helpers\LogViewer;
use Grav\Common\Utils;
use Grav\Console\ConsoleCommand;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle;
class LogViewerCommand extends ConsoleCommand
{
protected function configure()
{
$this
->setName('logviewer')
->addOption(
'file',
'f',
InputOption::VALUE_OPTIONAL,
'custom log file location (default = grav.log)'
)
->addOption(
'lines',
'l',
InputOption::VALUE_OPTIONAL,
'number of lines (default = 10)'
)
->setDescription('Display the last few entries of Grav log')
->setHelp("Display the last few entries of Grav log");
}
/**
* @return int|null|void
*/
protected function serve()
{
$grav = Grav::instance();
$grav->setup();
$file = $this->input->getOption('file') ?? 'grav.log';
$lines = $this->input->getOption('lines') ?? 20;
$verbose = $this->input->getOption('verbose', false);
$io = new SymfonyStyle($this->input, $this->output);
$io->title('Log Viewer');
$io->writeln(sprintf('viewing last %s entries in <white>%s</white>', $lines, $file));
$io->newLine();
$viewer = new LogViewer();
$logfile = $grav['locator']->findResource("log://" . $file);
if ($logfile) {
$rows = $viewer->objectTail($logfile, $lines, true);
foreach ($rows as $log) {
$date = $log['date'];
$level_color = LogViewer::levelColor($log['level']);
if ($date instanceof \DateTime) {
$output = "<yellow>{$log['date']->format('Y-m-d h:i:s')}</yellow> [<{$level_color}>{$log['level']}</{$level_color}>]";
if ($log['trace'] && $verbose) {
$output .= " <white>{$log['message']}</white> - {$log['trace']}";
} else {
$output .= " {$log['message']}";
}
$io->writeln($output);
if ($verbose) {
$io->newLine();
}
}
}
} else {
$io->error('cannot find the log file: logs/' . $file);
}
}
}