Logviewer hinzugefügt und dokploy-widget-fehler behoben
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Filament\Resources\InfrastructureActionLogs;
|
|||||||
use App\Filament\Resources\InfrastructureActionLogs\Pages\ManageInfrastructureActionLogs;
|
use App\Filament\Resources\InfrastructureActionLogs\Pages\ManageInfrastructureActionLogs;
|
||||||
use App\Models\InfrastructureActionLog;
|
use App\Models\InfrastructureActionLog;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
|
use Filament\Actions\ViewAction;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Support\Icons\Heroicon;
|
use Filament\Support\Icons\Heroicon;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
@@ -55,7 +56,7 @@ class InfrastructureActionLogResource extends Resource
|
|||||||
//
|
//
|
||||||
])
|
])
|
||||||
->recordActions([
|
->recordActions([
|
||||||
Tables\Actions\ViewAction::make(),
|
ViewAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->toolbarActions([
|
||||||
//
|
//
|
||||||
|
|||||||
157
app/Filament/SuperAdmin/Pages/LogViewerPage.php
Normal file
157
app/Filament/SuperAdmin/Pages/LogViewerPage.php
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\SuperAdmin\Pages;
|
||||||
|
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
|
class LogViewerPage extends Page
|
||||||
|
{
|
||||||
|
protected static ?string $navigationLabel = 'Logs';
|
||||||
|
|
||||||
|
protected static ?string $title = 'Application Logs';
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||||
|
|
||||||
|
protected string $view = 'filament.super-admin.pages.log-viewer';
|
||||||
|
|
||||||
|
public array $logFiles = [];
|
||||||
|
|
||||||
|
public string $selectedFile = '';
|
||||||
|
|
||||||
|
public string $filter = '';
|
||||||
|
|
||||||
|
public int $limit = 200;
|
||||||
|
|
||||||
|
public array $entries = [];
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->logFiles = $this->discoverLogFiles();
|
||||||
|
$this->selectedFile = $this->logFiles[0]['name'] ?? '';
|
||||||
|
$this->refreshLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedSelectedFile(): void
|
||||||
|
{
|
||||||
|
$this->refreshLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedFilter(): void
|
||||||
|
{
|
||||||
|
$this->refreshLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedLimit(): void
|
||||||
|
{
|
||||||
|
$this->limit = max(50, min(1000, $this->limit));
|
||||||
|
$this->refreshLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function discoverLogFiles(): array
|
||||||
|
{
|
||||||
|
$paths = File::glob(storage_path('logs/*.log')) ?: [];
|
||||||
|
|
||||||
|
$files = collect($paths)
|
||||||
|
->map(fn (string $path) => [
|
||||||
|
'name' => basename($path),
|
||||||
|
'path' => $path,
|
||||||
|
'modified' => File::lastModified($path) ?: 0,
|
||||||
|
'size' => File::size($path) ?: 0,
|
||||||
|
])
|
||||||
|
->sortByDesc('modified')
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function refreshLines(): void
|
||||||
|
{
|
||||||
|
$path = storage_path('logs/'.$this->selectedFile);
|
||||||
|
|
||||||
|
if (! File::exists($path)) {
|
||||||
|
$this->entries = [];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawLines = $this->tailFile($path, $this->limit * 2);
|
||||||
|
|
||||||
|
if ($this->filter !== '') {
|
||||||
|
$needle = mb_strtolower($this->filter);
|
||||||
|
$rawLines = array_values(array_filter(
|
||||||
|
$rawLines,
|
||||||
|
fn (string $line) => str_contains(mb_strtolower($line), $needle)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawLines = array_slice($rawLines, -$this->limit * 2);
|
||||||
|
$rawLines = $this->mergeStackTraces($rawLines);
|
||||||
|
$rawLines = array_slice(array_reverse($rawLines), 0, $this->limit);
|
||||||
|
|
||||||
|
$this->entries = array_map([$this, 'formatLine'], $rawLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return last $lines lines of file. Simpler approach is fine for small logs in admin view.
|
||||||
|
*/
|
||||||
|
protected function tailFile(string $path, int $lines = 200): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$contents = File::get($path);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$all = preg_split('/\r\n|\r|\n/', (string) $contents) ?: [];
|
||||||
|
|
||||||
|
return array_slice($all, -$lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function mergeStackTraces(array $lines): array
|
||||||
|
{
|
||||||
|
$merged = [];
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$trimmed = ltrim($line);
|
||||||
|
$isStackLine = str_starts_with($trimmed, '#')
|
||||||
|
|| str_starts_with($trimmed, 'Stack trace:')
|
||||||
|
|| str_starts_with($line, ' ');
|
||||||
|
|
||||||
|
if ($isStackLine && ! empty($merged)) {
|
||||||
|
$merged[array_key_last($merged)] .= PHP_EOL.$line;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($line === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$merged[] = $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formatLine(string $line): array
|
||||||
|
{
|
||||||
|
$level = 'info';
|
||||||
|
$timestamp = null;
|
||||||
|
|
||||||
|
if (preg_match('/\\[(?<ts>[^\\]]+)\\]/', $line, $matches)) {
|
||||||
|
$timestamp = $matches['ts'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/\\.(EMERGENCY|ALERT|CRITICAL|ERROR|WARNING|NOTICE|INFO|DEBUG)/i', $line, $matches)) {
|
||||||
|
$level = strtolower($matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'text' => $line,
|
||||||
|
'level' => $level,
|
||||||
|
'timestamp' => $timestamp,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,8 +47,11 @@ class DokployPlatformHealth extends Widget
|
|||||||
$results[] = [
|
$results[] = [
|
||||||
'label' => ucfirst($label),
|
'label' => ucfirst($label),
|
||||||
'compose_id' => $composeId,
|
'compose_id' => $composeId,
|
||||||
|
'name' => $composeId,
|
||||||
'status' => 'unreachable',
|
'status' => 'unreachable',
|
||||||
'error' => $exception->getMessage(),
|
'error' => $exception->getMessage(),
|
||||||
|
'services' => [],
|
||||||
|
'last_deploy' => null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<x-filament::page>
|
||||||
|
@php($selectedMeta = collect($logFiles)->firstWhere('name', $selectedFile))
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="grid gap-3 md:grid-cols-3">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase text-slate-500">Logdatei</label>
|
||||||
|
<select wire:model="selectedFile" class="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm focus:border-rose-300 focus:ring-1 focus:ring-rose-200">
|
||||||
|
@foreach($logFiles as $file)
|
||||||
|
<option value="{{ $file['name'] }}">
|
||||||
|
{{ $file['name'] }} ({{ number_format($file['size'] / 1024, 1) }} KB)
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-slate-500">Zuletzt aktualisiert: {{ $selectedMeta ? \Carbon\Carbon::createFromTimestamp($selectedMeta['modified'])->diffForHumans() : '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase text-slate-500">Filter (Level/Text)</label>
|
||||||
|
<input type="text" wire:model.debounce.400ms="filter" placeholder="error|warning|payment" class="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm focus:border-rose-300 focus:ring-1 focus:ring-rose-200" />
|
||||||
|
<p class="text-xs text-slate-500">Einfache Teilstring-Suche. Kein Regex.</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase text-slate-500">Zeilenlimit</label>
|
||||||
|
<input type="number" min="50" max="1000" step="50" wire:model.lazy="limit" class="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm focus:border-rose-300 focus:ring-1 focus:ring-rose-200" />
|
||||||
|
<p class="text-xs text-slate-500">Letzte Zeilen (nach Filter).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||||
|
<div class="flex items-center justify-between border-b border-slate-200 px-4 py-2 text-xs uppercase tracking-wide text-slate-500 dark:border-slate-800 dark:text-slate-400">
|
||||||
|
<span>{{ $selectedFile ?: 'Keine Datei' }}</span>
|
||||||
|
<span>{{ $filter ? 'Gefiltert' : 'Ungefiltert' }} · {{ count($entries) }} Zeilen</span>
|
||||||
|
</div>
|
||||||
|
<div class="max-h-[70vh] overflow-y-auto">
|
||||||
|
<ul class="divide-y divide-slate-100 text-xs dark:divide-slate-800">
|
||||||
|
@forelse($entries as $entry)
|
||||||
|
<li class="px-4 py-2">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<span @class([
|
||||||
|
'inline-flex min-w-[64px] items-center justify-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
||||||
|
'bg-red-100 text-red-800 dark:bg-red-500/20 dark:text-red-200' => $entry['level'] === 'error' || $entry['level'] === 'critical',
|
||||||
|
'bg-amber-100 text-amber-900 dark:bg-amber-500/20 dark:text-amber-100' => $entry['level'] === 'warning',
|
||||||
|
'bg-blue-100 text-blue-800 dark:bg-blue-500/20 dark:text-blue-100' => $entry['level'] === 'info',
|
||||||
|
'bg-slate-200 text-slate-800 dark:bg-slate-700 dark:text-slate-200' => ! in_array($entry['level'], ['error','critical','warning','info']),
|
||||||
|
])>
|
||||||
|
{{ strtoupper($entry['level']) }}
|
||||||
|
</span>
|
||||||
|
<div class="flex-1 space-y-1">
|
||||||
|
@if($entry['timestamp'])
|
||||||
|
<p class="text-[11px] text-slate-500 dark:text-slate-400">{{ $entry['timestamp'] }}</p>
|
||||||
|
@endif
|
||||||
|
<p class="whitespace-pre-wrap leading-relaxed text-slate-800 dark:text-slate-100">{{ $entry['text'] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
@empty
|
||||||
|
<li class="px-4 py-3 text-slate-500 dark:text-slate-400">Keine Logeinträge gefunden.</li>
|
||||||
|
@endforelse
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament::page>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-slate-600 dark:text-slate-200">{{ $compose['label'] }}</p>
|
<p class="text-sm font-semibold text-slate-600 dark:text-slate-200">{{ $compose['label'] }}</p>
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400">{{ $compose['name'] }}</p>
|
<p class="text-xs text-slate-500 dark:text-slate-400">{{ $compose['name'] ?? '–' }}</p>
|
||||||
<p class="text-[11px] text-slate-400 dark:text-slate-500">{{ $compose['compose_id'] }}</p>
|
<p class="text-[11px] text-slate-400 dark:text-slate-500">{{ $compose['compose_id'] }}</p>
|
||||||
</div>
|
</div>
|
||||||
<span @class([
|
<span @class([
|
||||||
|
|||||||
Reference in New Issue
Block a user