feat: enhance Proxmox firewall logs functionality by increasing log limit to 5000, adding auto-refresh feature, and improving log fetching mechanism for better data accuracy and user experience

This commit is contained in:
2025-12-04 19:58:46 -05:00
parent 0adfdb3741
commit 5801f90c88
2 changed files with 130 additions and 115 deletions

View File

@@ -155,12 +155,14 @@ class Firewall extends Component
// Firewall Logs // Firewall Logs
public array $firewallLogs = []; public array $firewallLogs = [];
public int $logLimit = 1000; public int $logLimit = 5000; // Fetch more logs to ensure fresh data
public string $logError = ''; public string $logError = '';
// Pagination for logs // Pagination for logs
public int $currentPage = 1; public int $currentPage = 1;
public int $perPage = 50; public int $perPage = 50;
public bool $autoRefreshLogs = false;
public ?string $lastLogRefresh = null;
// Available IPs for rules // Available IPs for rules
public array $availableIps = []; public array $availableIps = [];
@@ -228,10 +230,18 @@ class Firewall extends Component
? 'application/json, text/plain, */*' ? 'application/json, text/plain, */*'
: 'application/json'; : 'application/json';
$request = Http::withHeaders([ $headers = [
'Authorization' => $authHeader, 'Authorization' => $authHeader,
'Accept' => $acceptHeader, 'Accept' => $acceptHeader,
])->withoutVerifying(); ];
// Prevent caching for log requests
if (str_contains($url, '/firewall/log')) {
$headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
$headers['Pragma'] = 'no-cache';
}
$request = Http::withHeaders($headers)->withoutVerifying();
$response = match($method) { $response = match($method) {
'post' => $request->post($fullUrl, $data), 'post' => $request->post($fullUrl, $data),
@@ -1136,94 +1146,76 @@ class Firewall extends Component
$this->logError = ''; $this->logError = '';
try { try {
// Fetch logs in batches to handle large log sets // Fetch all available logs without pagination to get the freshest data
// Add timestamp to prevent caching
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log',
'get',
[
'limit' => $this->logLimit,
'start' => 0,
'_dc' => time() // Cache buster
]
);
if (!$response->successful()) {
$this->logError = 'Failed to fetch logs (HTTP ' . $response->status() . ')';
Log::warning('Firewall log API request failed', [
'status' => $response->status(),
'server_id' => $this->server->id,
'vm_id' => $this->server->vm_id,
]);
$this->firewallLogs = [];
return;
}
// Proxmox firewall log endpoint can return JSON or text/plain
$body = $response->body();
$allRawLogs = []; $allRawLogs = [];
$start = 0;
$batchSize = 500;
$maxLogs = $this->logLimit;
do { // Check content type
$response = $this->firewallRequest( $contentType = $response->header('Content-Type');
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log', $isJson = str_contains($contentType ?? '', 'json');
'get',
[ if ($isJson) {
'limit' => $batchSize, try {
'start' => $start $jsonData = $response->json();
]
); if (isset($jsonData['data'])) {
if (is_array($jsonData['data'])) {
if (!$response->successful()) { foreach ($jsonData['data'] as $item) {
$this->logError = 'Failed to fetch logs (HTTP ' . $response->status() . ')';
Log::warning('Firewall log API request failed', [
'status' => $response->status(),
'server_id' => $this->server->id,
'vm_id' => $this->server->vm_id,
]);
$this->firewallLogs = [];
return;
}
// Proxmox firewall log endpoint can return JSON or text/plain
$body = $response->body();
$batchRawLogs = [];
// Check content type
$contentType = $response->header('Content-Type');
$isJson = str_contains($contentType ?? '', 'json');
if ($isJson) {
try {
$jsonData = $response->json();
if (isset($jsonData['data'])) {
if (is_array($jsonData['data'])) {
foreach ($jsonData['data'] as $item) {
if (is_array($item) && isset($item['t'])) {
$batchRawLogs[] = $item['t'];
} elseif (is_string($item)) {
$batchRawLogs[] = $item;
}
}
} elseif (is_string($jsonData['data'])) {
$lines = preg_split('/\r\n|\r|\n/', $jsonData['data']);
$batchRawLogs = array_filter($lines, fn($line) => !empty(trim($line)));
}
} elseif (is_array($jsonData)) {
foreach ($jsonData as $item) {
if (is_array($item) && isset($item['t'])) { if (is_array($item) && isset($item['t'])) {
$batchRawLogs[] = $item['t']; $allRawLogs[] = $item['t'];
} elseif (is_string($item)) { } elseif (is_string($item)) {
$batchRawLogs[] = $item; $allRawLogs[] = $item;
} }
} }
} elseif (is_string($jsonData['data'])) {
$lines = preg_split('/\r\n|\r|\n/', $jsonData['data']);
$allRawLogs = array_filter($lines, fn($line) => !empty(trim($line)));
}
} elseif (is_array($jsonData)) {
foreach ($jsonData as $item) {
if (is_array($item) && isset($item['t'])) {
$allRawLogs[] = $item['t'];
} elseif (is_string($item)) {
$allRawLogs[] = $item;
}
} }
} catch (\Exception $e) {
Log::warning('Failed to parse JSON response', ['error' => $e->getMessage()]);
} }
} catch (\Exception $e) {
Log::warning('Failed to parse JSON response', ['error' => $e->getMessage()]);
} }
// Fall back to text/plain format
if (empty($batchRawLogs) && !empty($body)) {
$lines = preg_split('/\r\n|\r|\n/', $body);
$batchRawLogs = array_filter($lines, fn($line) => !empty(trim($line)));
}
$batchRawLogs = array_values($batchRawLogs);
$allRawLogs = array_merge($allRawLogs, $batchRawLogs);
$fetchedCount = count($batchRawLogs);
$start += $batchSize;
// Break if we got fewer logs than requested or reached limit
if ($fetchedCount < $batchSize || count($allRawLogs) >= $maxLogs) {
break;
}
} while (true);
// Limit to maxLogs
if (count($allRawLogs) > $maxLogs) {
$allRawLogs = array_slice($allRawLogs, 0, $maxLogs);
} }
// Fall back to text/plain format
if (empty($allRawLogs) && !empty($body)) {
$lines = preg_split('/\r\n|\r|\n/', $body);
$allRawLogs = array_filter($lines, fn($line) => !empty(trim($line)));
}
$allRawLogs = array_values($allRawLogs);
Log::info('Firewall logs fetched from Proxmox API', [ Log::info('Firewall logs fetched from Proxmox API', [
'raw_count' => count($allRawLogs), 'raw_count' => count($allRawLogs),
'first_3_logs' => array_slice($allRawLogs, 0, 3) 'first_3_logs' => array_slice($allRawLogs, 0, 3)
@@ -1231,9 +1223,21 @@ class Firewall extends Component
$this->firewallLogs = $this->parseFirewallLogs($allRawLogs); $this->firewallLogs = $this->parseFirewallLogs($allRawLogs);
// Reset to page 1 when refreshing logs
$this->currentPage = 1;
// Set last refresh timestamp
$this->lastLogRefresh = now()->format('Y-m-d H:i:s');
// Notify user of successful refresh
$this->notify('Loaded ' . count($this->firewallLogs) . ' firewall log entries', 'success');
Log::info('Firewall log parsing summary', [ Log::info('Firewall log parsing summary', [
'total_raw_logs' => count($allRawLogs), 'total_raw_logs' => count($allRawLogs),
'successfully_parsed' => count($this->firewallLogs), 'successfully_parsed' => count($this->firewallLogs),
'last_refresh' => $this->lastLogRefresh,
'first_log_timestamp' => $this->firewallLogs[0]['timestamp'] ?? 'none',
'last_log_timestamp' => $this->firewallLogs[count($this->firewallLogs) - 1]['timestamp'] ?? 'none',
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@@ -726,43 +726,54 @@
</div> </div>
{{-- LOGS TAB --}} {{-- LOGS TAB --}}
<div x-show="activeTab === 'logs'" x-transition> <div x-show="activeTab === 'logs'" x-transition wire:poll.30s="loadFirewallLogs">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">Firewall Logs</h3> <div>
<button type="button" wire:click="loadFirewallLogs" wire:loading.attr="disabled" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2"> <h3 class="text-lg font-semibold">Firewall Logs</h3>
<span wire:loading.remove wire:target="loadFirewallLogs"> <div class="flex items-center gap-3 mt-1">
<x-ri-refresh-line class="size-4" /> <p class="text-xs text-base/60">Showing the latest logs first (most recent at top)</p>
</span> @if($lastLogRefresh)
<div wire:loading wire:target="loadFirewallLogs" class="loader-container"> <span class="text-xs text-success/80">• Last updated: {{ $lastLogRefresh }}</span>
<style> @endif
.loader-container { <span class="text-xs text-info/80">• Auto-refreshes every 30s</span>
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.loader {
width: 16px;
aspect-ratio: 1;
border-radius: 50%;
padding: 2px;
background:
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 1.5px)/6px 3px,
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) top right/50% 50% content-box content-box,
radial-gradient(farthest-side at top, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) bottom/100% 50% content-box content-box;
background-repeat: no-repeat;
animation: loader-spin 1s infinite linear;
}
@keyframes loader-spin {
100% { transform: rotate(1turn); }
}
</style>
<div class="loader"></div>
</div> </div>
<span wire:loading.remove wire:target="loadFirewallLogs">Refresh</span> </div>
<span wire:loading wire:target="loadFirewallLogs">Loading...</span> <div class="flex items-center gap-3">
</button> <button type="button" wire:click="loadFirewallLogs" wire:loading.attr="disabled" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2">
<span wire:loading.remove wire:target="loadFirewallLogs">
<x-ri-refresh-line class="size-4" />
</span>
<div wire:loading wire:target="loadFirewallLogs" class="loader-container">
<style>
.loader-container {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.loader {
width: 16px;
aspect-ratio: 1;
border-radius: 50%;
padding: 2px;
background:
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 1.5px)/6px 3px,
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) top right/50% 50% content-box content-box,
radial-gradient(farthest-side at top, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) bottom/100% 50% content-box content-box;
background-repeat: no-repeat;
animation: loader-spin 1s infinite linear;
}
@keyframes loader-spin {
100% { transform: rotate(1turn); }
}
</style>
<div class="loader"></div>
</div>
<span wire:loading.remove wire:target="loadFirewallLogs">Refresh</span>
<span wire:loading wire:target="loadFirewallLogs">Loading...</span>
</button>
</div>
</div> </div>
@if(!empty($logError)) @if(!empty($logError))