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:
@@ -155,12 +155,14 @@ class Firewall extends Component
|
||||
|
||||
// Firewall Logs
|
||||
public array $firewallLogs = [];
|
||||
public int $logLimit = 1000;
|
||||
public int $logLimit = 5000; // Fetch more logs to ensure fresh data
|
||||
public string $logError = '';
|
||||
|
||||
// Pagination for logs
|
||||
public int $currentPage = 1;
|
||||
public int $perPage = 50;
|
||||
public bool $autoRefreshLogs = false;
|
||||
public ?string $lastLogRefresh = null;
|
||||
|
||||
// Available IPs for rules
|
||||
public array $availableIps = [];
|
||||
@@ -228,10 +230,18 @@ class Firewall extends Component
|
||||
? 'application/json, text/plain, */*'
|
||||
: 'application/json';
|
||||
|
||||
$request = Http::withHeaders([
|
||||
$headers = [
|
||||
'Authorization' => $authHeader,
|
||||
'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) {
|
||||
'post' => $request->post($fullUrl, $data),
|
||||
@@ -1136,94 +1146,76 @@ class Firewall extends Component
|
||||
$this->logError = '';
|
||||
|
||||
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 = [];
|
||||
$start = 0;
|
||||
$batchSize = 500;
|
||||
$maxLogs = $this->logLimit;
|
||||
|
||||
do {
|
||||
$response = $this->firewallRequest(
|
||||
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log',
|
||||
'get',
|
||||
[
|
||||
'limit' => $batchSize,
|
||||
'start' => $start
|
||||
]
|
||||
);
|
||||
|
||||
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();
|
||||
$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) {
|
||||
// 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'];
|
||||
$allRawLogs[] = $item['t'];
|
||||
} 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', [
|
||||
'raw_count' => count($allRawLogs),
|
||||
'first_3_logs' => array_slice($allRawLogs, 0, 3)
|
||||
@@ -1231,9 +1223,21 @@ class Firewall extends Component
|
||||
|
||||
$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', [
|
||||
'total_raw_logs' => count($allRawLogs),
|
||||
'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) {
|
||||
|
||||
@@ -726,43 +726,54 @@
|
||||
</div>
|
||||
|
||||
{{-- 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">
|
||||
<h3 class="text-lg font-semibold">Firewall Logs</h3>
|
||||
<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>
|
||||
<h3 class="text-lg font-semibold">Firewall Logs</h3>
|
||||
<div class="flex items-center gap-3 mt-1">
|
||||
<p class="text-xs text-base/60">Showing the latest logs first (most recent at top)</p>
|
||||
@if($lastLogRefresh)
|
||||
<span class="text-xs text-success/80">• Last updated: {{ $lastLogRefresh }}</span>
|
||||
@endif
|
||||
<span class="text-xs text-info/80">• Auto-refreshes every 30s</span>
|
||||
</div>
|
||||
<span wire:loading.remove wire:target="loadFirewallLogs">Refresh</span>
|
||||
<span wire:loading wire:target="loadFirewallLogs">Loading...</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<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>
|
||||
|
||||
@if(!empty($logError))
|
||||
|
||||
Reference in New Issue
Block a user