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
|
// 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) {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user