Skip to content

Commit

Permalink
more improvements, support Nginx error logs
Browse files Browse the repository at this point in the history
  • Loading branch information
arukompas committed Jul 12, 2023
1 parent 37c82f6 commit c0e98c3
Show file tree
Hide file tree
Showing 14 changed files with 303 additions and 22 deletions.
12 changes: 12 additions & 0 deletions config/log-viewer.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@
// 'my_secret.log'
],

'http_logs' => [
'include_files' => [
'/var/log/httpd/*',
'/var/log/nginx/*',
// '/absolute/paths/supported',
],

'exclude_files' => [
// 'my_secret.log'
],
],

/*
|--------------------------------------------------------------------------
| Shorter stack trace filters.
Expand Down
10 changes: 8 additions & 2 deletions src/HttpAccessLog.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

class HttpAccessLog extends HttpLog
{
static string $regex = '/(\S+) (\S+) (\S+) \[(.+)\] "(\S+) (\S+) (\S+)" (\S+) (\S+) "([^"]*)" "([^"]*)"/';

public ?string $ip;

public ?string $identity;
Expand Down Expand Up @@ -55,8 +57,7 @@ public function __construct(

public function parseText(string $text = ''): array
{
$regex = '/(\S+) (\S+) (\S+) \[(.+)\] "(\S+) (\S+) (\S+)" (\S+) (\S+) "([^"]*)" "([^"]*)"/';
preg_match($regex, $text, $matches);
preg_match(self::$regex, $text, $matches);

return [
'ip' => $matches[1] ?? null,
Expand All @@ -77,4 +78,9 @@ public function parseDateTime(?string $datetime): ?CarbonInterface
{
return $datetime ? Carbon::parse($datetime) : null;
}

public static function matches(string $text): bool
{
return preg_match(self::$regex, $text) === 1;
}
}
14 changes: 10 additions & 4 deletions src/HttpErrorLog.php → src/HttpApacheErrorLog.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
use Carbon\CarbonInterface;
use Illuminate\Support\Carbon;

class HttpErrorLog extends HttpLog
class HttpApacheErrorLog extends HttpLog
{
static string $regex = '/\[(?<dttm>.*?)\]\s\[(?:(?<module>.*?):)?(?<level>.*?)\]\s\[pid\s(?<pid>\d*)\](?:\s\[client\s(?<client>.*?)\])?\s(?<message>.*)/';

public ?CarbonInterface $datetime;

public ?string $module;
Expand Down Expand Up @@ -40,15 +42,14 @@ public function __construct(

public function parseText(string $text): array
{
$regex = '/\[(?<dttm>.*?)\]\s\[(?:(?<module>.*?):)?(?<level>.*?)\]\s\[pid\s(?<pid>\d*)\]\s\[client\s(?<client>.*?)\]\s(?<message>.*)/';
preg_match($regex, $this->text, $matches);
preg_match(self::$regex, $this->text, $matches);

return [
'datetime' => $matches['dttm'] ?? null,
'module' => $matches['module'] ?? null,
'level' => $matches['level'] ?? null,
'pid' => $matches['pid'] ?? null,
'client' => $matches['client'] ?? null,
'client' => isset($matches['client']) ? ($matches['client'] ?: null) : null,
'message' => $matches['message'] ?? null,
];
}
Expand All @@ -57,4 +58,9 @@ public function parseDateTime(?string $datetime): ?CarbonInterface
{
return $datetime ? Carbon::createFromFormat('D M d H:i:s.u Y', $datetime) : null;
}

public static function matches(string $text): bool
{
return preg_match(self::$regex, $text) === 1;
}
}
5 changes: 5 additions & 0 deletions src/HttpLog.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ public function __construct(
) {
$this->text = rtrim($this->text);
}

public static function matches(string $text): bool
{
return preg_match(static::$regex, $text) === 1;
}
}
22 changes: 20 additions & 2 deletions src/HttpLogReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,29 @@ protected function makeLog(string $text, int $filePosition): HttpLog
{
return match ($this->file->type) {
LogFile::TYPE_HTTP_ACCESS => new HttpAccessLog($text, $this->file->identifier, $filePosition),
LogFile::TYPE_HTTP_ERROR => new HttpErrorLog($text, $this->file->identifier, $filePosition),
default => throw new \Exception('Unknown log file type: '.$this->file->type),
LogFile::TYPE_HTTP_ERROR_APACHE => new HttpApacheErrorLog($text, $this->file->identifier, $filePosition),
LogFile::TYPE_HTTP_ERROR_NGINX => new HttpNginxErrorLog($text, $this->file->identifier, $filePosition),
default => $this->makeLogByGuessingType($text, $filePosition),
};
}

protected function makeLogByGuessingType(string $text, int $filePosition): HttpLog
{
if (HttpAccessLog::matches($text)) {
return new HttpAccessLog($text, $this->file->identifier, $filePosition);
}

if (HttpApacheErrorLog::matches($text)) {
return new HttpApacheErrorLog($text, $this->file->identifier, $filePosition);
}

if (HttpNginxErrorLog::matches($text)) {
return new HttpNginxErrorLog($text, $this->file->identifier, $filePosition);
}

throw new \Exception('Could not determine the log type for "'.$text.'".');
}

public function __destruct()
{
$this->close();
Expand Down
59 changes: 59 additions & 0 deletions src/HttpNginxErrorLog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Opcodes\LogViewer;

use Carbon\CarbonInterface;
use Illuminate\Support\Carbon;

class HttpNginxErrorLog extends HttpLog
{
static string $regex = '/^(?P<datetime>[\d+\/ :]+) \[(?P<errortype>.+)\] .*?: (?P<errormessage>.+?)(?:, client: (?P<client>.+?))?(?:, server: (?P<server>.+?))?(?:, request: "?(?P<request>.+?)"?)?(?:, host: "?(?P<host>.+?)"?)?$/';

public ?CarbonInterface $datetime;
public ?string $level;
public ?string $message;
public ?string $client;
public ?string $server;
public ?string $request;
public ?string $host;

public function __construct(
public string $text,
public ?string $fileIdentifier = null,
public ?int $filePosition = null,
) {
parent::__construct($text, $fileIdentifier, $filePosition);

$matches = $this->parseText($text);

$this->datetime = $this->parseDateTime($matches['datetime'])?->tz(
config('log-viewer.timezone', config('app.timezone', 'UTC'))
);
$this->level = $matches['level'];
$this->message = $matches['message'];
$this->client = $matches['client'];
$this->server = $matches['server'];
$this->request = $matches['request'];
$this->host = $matches['host'];
}

public function parseText(string $text): array
{
$result = preg_match(self::$regex, $this->text, $matches);

return [
'datetime' => $matches['datetime'] ?? null,
'level' => $matches['errortype'] ?? null,
'message' => $matches['errormessage'] ?? null,
'client' => isset($matches['client']) ? ($matches['client'] ?: null) : null,
'server' => $matches['server'] ?? null,
'request' => $matches['request'] ?? null,
'host' => $matches['host'] ?? null,
];
}

public function parseDateTime(?string $datetime): ?CarbonInterface
{
return $datetime ? Carbon::createFromFormat('Y/m/d H:i:s', $datetime) : null;
}
}
33 changes: 30 additions & 3 deletions src/LogFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@
namespace Opcodes\LogViewer;

use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Opcodes\LogViewer\Events\LogFileDeleted;
use Opcodes\LogViewer\Exceptions\InvalidRegularExpression;
use Opcodes\LogViewer\Facades\Cache;
use Opcodes\LogViewer\Utils\Utils;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

class LogFile
{
const TYPE_LARAVEL = 'laravel';

const TYPE_HTTP_ACCESS = 'http_access';

const TYPE_HTTP_ERROR = 'http_error';
const TYPE_HTTP_ERROR_APACHE = 'http_error_apache';
const TYPE_HTTP_ERROR_NGINX = 'http_error_nginx';

use Concerns\LogFile\HasMetadata;
use Concerns\LogFile\CanCacheData;
Expand Down Expand Up @@ -45,6 +46,32 @@ public function __construct(string $path, string $type = self::TYPE_LARAVEL)
$this->loadMetadata();
}

public static function makeAndGuessType($filePath): self
{
$typeCacheKey = 'log-viewer::file-type-'.md5($filePath);
$type = Cache::get($typeCacheKey);

if (isset($type)) {
return new self($filePath, $type);
}

$reader = new HttpLogReader(new self($filePath));
$logEntry = $reader->next();

$type = match (get_class($logEntry)) {
HttpAccessLog::class => LogFile::TYPE_HTTP_ACCESS,
HttpApacheErrorLog::class => LogFile::TYPE_HTTP_ERROR_APACHE,
HttpNginxErrorLog::class => LogFile::TYPE_HTTP_ERROR_NGINX,
default => null,
};

if (isset($type)) {
Cache::put($typeCacheKey, $type, Carbon::now()->addMonth());
}

return new self($filePath, $type);
}

public function index(string $query = null): LogIndex
{
if (! isset($this->_logIndexCache[$query])) {
Expand Down
30 changes: 28 additions & 2 deletions src/LogViewerService.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class LogViewerService

protected mixed $hostsResolver;

protected function getFilePaths(): array
protected function getLaravelLogFilePaths(): array
{
// Because we'll use the base path as a parameter for `glob`, we should escape any
// glob's special characters and treat those as actual characters of the path.
Expand Down Expand Up @@ -70,6 +70,25 @@ protected function getFilePaths(): array
return array_values(array_reverse($files));
}

protected function getHttpLogFilePaths(): array
{
$files = [];

foreach (config('log-viewer.http_logs.include_files', []) as $pattern) {
$files = array_merge($files, $this->getFilePathsMatchingPattern($pattern));
}

foreach (config('log-viewer.http_logs.exclude_files', []) as $pattern) {
$files = array_diff($files, $this->getFilePathsMatchingPattern($pattern));
}

$files = array_map('realpath', $files);

$files = array_filter($files, 'is_file');

return array_values(array_reverse($files));
}

protected function getFilePathsMatchingPattern($pattern)
{
// The GLOB_BRACE flag is not available on some non GNU systems, like Solaris or Alpine Linux.
Expand All @@ -88,10 +107,17 @@ public function basePathForLogs(): string
public function getFiles(): LogFileCollection
{
if (! isset($this->_cachedFiles)) {
$this->_cachedFiles = (new LogFileCollection($this->getFilePaths()))
$laravelLogFiles = (new LogFileCollection($this->getLaravelLogFilePaths()))
->unique()
->map(fn ($filePath) => new LogFile($filePath))
->values();

$httpLogFiles = (new LogFileCollection($this->getHttpLogFilePaths()))
->unique()
->map(fn ($filePath) => LogFile::makeAndGuessType($filePath))
->values();

$this->_cachedFiles = $laravelLogFiles->merge($httpLogFiles);
}

return $this->_cachedFiles;
Expand Down
21 changes: 19 additions & 2 deletions tests/Pest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ function dummyLogData(int $lines = null, string $type = LogFile::TYPE_LARAVEL):
fn ($_) => match ($type) {
LogFile::TYPE_LARAVEL => makeLaravelLogEntry(),
LogFile::TYPE_HTTP_ACCESS => makeHttpAccessLogEntry(),
LogFile::TYPE_HTTP_ERROR => makeHttpErrorLogEntry(),
LogFile::TYPE_HTTP_ERROR_APACHE => makeHttpApacheErrorLogEntry(),
LogFile::TYPE_HTTP_ERROR_NGINX => makeHttpNginxErrorLogEntry(),
},
range(1, $lines)
));
Expand Down Expand Up @@ -94,7 +95,7 @@ function makeHttpAccessLogEntry(CarbonInterface $date = null, string $method = '
EOF;
}

function makeHttpErrorLogEntry(CarbonInterface $date = null, string $module = null, string $level = null, int $pid = null, string $client = null, string $message = null): string
function makeHttpApacheErrorLogEntry(CarbonInterface $date = null, string $module = null, string $level = null, int $pid = null, string $client = null, string $message = null): string
{
$dateFormatted = $date instanceof CarbonInterface ? $date->format('D M d H:i:s.u Y') : now()->format('D M d H:i:s.u Y');
$module ??= 'php';
Expand All @@ -108,6 +109,22 @@ function makeHttpErrorLogEntry(CarbonInterface $date = null, string $module = nu
EOF;
}

function makeHttpNginxErrorLogEntry(CarbonInterface $date = null, string $level = null, string $message = null, string $client = null, string $server = null, string $request = null, string $host = null): string
{
$dateFormatted = $date instanceof CarbonInterface ? $date->format('Y/m/d H:i:s') : now()->format('Y/m/d H:i:s');
$level ??= 'error';
$pid ??= rand(1, 9999);
$client ??= rand(1, 255).'.'.rand(1, 255).'.'.rand(1, 255).'.'.rand(1, 255);
$message ??= 'Testing log entry';
$server ??= '127.0.0.1:80';
$request ??= 'GET / HTTP/1.1';
$host ??= 'localhost';

return <<<EOF
$dateFormatted [$level] 23263#0: $message, client: $client, server: $server, request: "$request", host: "$host"
EOF;
}

function createLogIndex($file = null, $query = null, array $predefinedLogs = []): LogIndex
{
if (is_null($file)) {
Expand Down
9 changes: 9 additions & 0 deletions tests/Unit/AccessLogs/Fixtures/errors_nginx_dummy.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
2023/01/04 11:18:33 [alert] 95160#0: *1473 setsockopt(TCP_NODELAY) failed (22: Invalid argument) while keepalive, client: 127.0.0.1, server: 127.0.0.1:80
2023/07/01 22:05:03 [warn] 21925#0: the "listen ... http2" directive is deprecated, use the "http2" directive instead in /Users/test/.config/valet/Nginx/blog9.test:9
2023/07/01 22:05:03 [warn] 21925#0: the "http2_push_preload" directive is obsolete, ignored in /Users/test/.config/valet/Nginx/blog9.test:15
2023/07/01 22:05:03 [warn] 21925#0: the "listen ... http2" directive is deprecated, use the "http2" directive instead in /Users/test/.config/valet/Nginx/system.test:9
2023/07/01 22:05:03 [warn] 21925#0: the "http2_push_preload" directive is obsolete, ignored in /Users/test/.config/valet/Nginx/system.test:15
2023/07/01 22:05:09 [warn] 22583#0: the "listen ... http2" directive is deprecated, use the "http2" directive instead in /Users/test/.config/valet/Nginx/blog9.test:9
2023/07/01 22:05:09 [warn] 22583#0: the "http2_push_preload" directive is obsolete, ignored in /Users/test/.config/valet/Nginx/blog9.test:15
2023/07/01 22:05:09 [warn] 22583#0: the "listen ... http2" directive is deprecated, use the "http2" directive instead in /Users/test/.config/valet/Nginx/system.test:9
2023/07/01 22:05:09 [warn] 22583#0: the "http2_push_preload" directive is obsolete, ignored in /Users/test/.config/valet/Nginx/system.test:15
Loading

0 comments on commit c0e98c3

Please sign in to comment.