siakad-itm/app/Services/Admin/Developer/LogViewerService.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);
}
}