113 lines
3.5 KiB
PHP
113 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Admin\Developer;
|
|
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Pagination\LengthAwarePaginator as Paginator;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\File;
|
|
|
|
class LogViewerService
|
|
{
|
|
private const ENTRY_PATTERN = '/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2})?)\]\s+(\w+)\.(\w+):\s?(.*)$/s';
|
|
|
|
/**
|
|
* @return Collection<int, array{name: string, size: int, modified_at: string}>
|
|
*/
|
|
public function availableFiles(): Collection
|
|
{
|
|
return collect(File::glob(storage_path('logs/laravel*.log')))
|
|
->map(fn (string $path) => [
|
|
'name' => basename($path),
|
|
'size' => File::size($path),
|
|
'modified_at' => date('Y-m-d H:i:s', File::lastModified($path)),
|
|
'modified_timestamp' => File::lastModified($path),
|
|
])
|
|
->sortByDesc('modified_timestamp')
|
|
->values()
|
|
->map(fn (array $file) => collect($file)->except('modified_timestamp')->all());
|
|
}
|
|
|
|
public function latestFileName(): ?string
|
|
{
|
|
return $this->availableFiles()->first()['name'] ?? null;
|
|
}
|
|
|
|
public function paginated(string $fileName, int $perPage = 25, string $search = '', ?string $level = null): LengthAwarePaginator
|
|
{
|
|
$entries = $this->parse($fileName)
|
|
->when($level, fn (Collection $q) => $q->where('level', strtoupper($level)))
|
|
->when($search, fn (Collection $q) => $q->filter(
|
|
fn (array $entry) => str_contains(strtolower($entry['message']), strtolower($search))
|
|
))
|
|
->values();
|
|
|
|
$page = request()->integer('page', 1);
|
|
$items = $entries->forPage($page, $perPage)->values();
|
|
|
|
return new Paginator(
|
|
$items,
|
|
$entries->count(),
|
|
$perPage,
|
|
$page,
|
|
['path' => request()->url(), 'query' => request()->query()],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, array{id: string, timestamp: string, environment: string, level: string, message: string, raw: string}>
|
|
*/
|
|
private function parse(string $fileName): Collection
|
|
{
|
|
$path = $this->resolvePath($fileName);
|
|
|
|
if (! $path) {
|
|
return collect();
|
|
}
|
|
|
|
$lines = preg_split('/\R/', File::get($path)) ?: [];
|
|
|
|
$entries = [];
|
|
$current = null;
|
|
|
|
foreach ($lines as $line) {
|
|
if (preg_match(self::ENTRY_PATTERN, $line, $matches)) {
|
|
if ($current) {
|
|
$entries[] = $current;
|
|
}
|
|
|
|
$current = [
|
|
'timestamp' => $matches[1],
|
|
'environment' => $matches[2],
|
|
'level' => strtoupper($matches[3]),
|
|
'message' => $matches[4],
|
|
'raw' => $line,
|
|
];
|
|
} elseif ($current !== null && trim($line) !== '') {
|
|
$current['message'] .= "\n".$line;
|
|
$current['raw'] .= "\n".$line;
|
|
}
|
|
}
|
|
|
|
if ($current) {
|
|
$entries[] = $current;
|
|
}
|
|
|
|
return collect($entries)
|
|
->reverse()
|
|
->values()
|
|
->map(fn (array $entry, int $index) => [...$entry, 'id' => (string) $index]);
|
|
}
|
|
|
|
private function resolvePath(string $fileName): ?string
|
|
{
|
|
$allowed = $this->availableFiles()->pluck('name');
|
|
|
|
if (! $allowed->contains($fileName)) {
|
|
return null;
|
|
}
|
|
|
|
return storage_path('logs/'.$fileName);
|
|
}
|
|
}
|