feat: implement caching for network analysis to improve performance and reduce load times

This commit is contained in:
2025-12-01 07:53:05 -05:00
parent 5e784c9dbf
commit 5c2c8734db

View File

@@ -154,23 +154,45 @@ class Statistics extends Component
{
Log::debug('Network Analysis: loadNetworkAnalysis() called', [
'current_loading_state' => $this->networkAnalysisLoading,
'has_last_updated' => !empty($this->networkAnalysis['lastUpdated']),
'has_error' => !empty($this->networkAnalysis['error']),
'has_last_updated' => ! empty($this->networkAnalysis['lastUpdated']),
'has_error' => ! empty($this->networkAnalysis['error']),
]);
// Prevent duplicate loads
if ($this->networkAnalysisLoading) {
Log::debug('Network Analysis: Already loading, skipping');
// Use a short-lived cache to avoid repeatedly performing heavy analysis
$cacheKey = 'proxmox_network_analysis_' . $this->server->id;
// If we already have a fairly recent result, reuse it immediately
$cachedAnalysis = Cache::get($cacheKey);
if (is_array($cachedAnalysis) && ! empty($cachedAnalysis['lastUpdated']) && empty($this->networkAnalysis['error'])) {
Log::debug('Network Analysis: served from cache', [
'cache_key' => $cacheKey,
'last_updated' => $cachedAnalysis['lastUpdated'] ?? null,
]);
$this->networkAnalysis = $cachedAnalysis;
$this->networkAnalysisLoading = false;
// Reset retry count when we successfully hydrate from cache
$this->networkAnalysisRetryCount = 0;
return;
}
if (!$this->coreRouterEnabled()) {
// Prevent duplicate loads while a request is already in-flight
if ($this->networkAnalysisLoading) {
Log::debug('Network Analysis: Already loading, skipping new request');
return;
}
if (! $this->coreRouterEnabled()) {
$this->networkAnalysis['error'] = 'Core Router API is not configured.';
$this->networkAnalysis['connections'] = [];
$this->networkAnalysis['topConnections'] = [];
$this->networkAnalysis['topPorts'] = [];
$this->networkAnalysis['security'] = [];
$this->networkAnalysisLoading = false;
return;
}
@@ -182,6 +204,7 @@ class Statistics extends Component
$this->networkAnalysis['topPorts'] = [];
$this->networkAnalysis['security'] = [];
$this->networkAnalysisLoading = false;
return;
}
@@ -190,20 +213,20 @@ class Statistics extends Component
try {
$client = app(CoreRouterClient::class);
// Fetch all firewall states (no API-level filtering)
$allStates = $client->listStates();
// Normalize server IPs for matching
$normalizedServerIps = array_map(function($ip) {
$normalizedServerIps = array_map(static function ($ip) {
return strtolower(trim($ip));
}, $serverIps);
// Sample a few states to debug extraction
// Sample a few states to debug extraction (kept minimal for performance)
$sampleStatesForDebug = [];
foreach (array_slice($allStates, 0, 10) as $sampleState) {
$rawSrc = (string)($sampleState['source'] ?? '');
$rawDst = (string)($sampleState['destination'] ?? '');
$rawSrc = (string) ($sampleState['source'] ?? '');
$rawDst = (string) ($sampleState['destination'] ?? '');
$sampleStatesForDebug[] = [
'raw_source' => $rawSrc,
'raw_destination' => $rawDst,
@@ -212,35 +235,35 @@ class Statistics extends Component
'protocol' => $sampleState['protocol'] ?? 'N/A',
];
}
Log::debug('Core Router filtering states', [
'total_states' => count($allStates),
'server_ips' => $serverIps,
'normalized_server_ips' => $normalizedServerIps,
'sample_states_extraction' => $sampleStatesForDebug,
]);
// Filter states: keep only where source OR destination matches a server IP
$filteredStates = [];
$sampleChecks = [];
foreach ($allStates as $state) {
$rawSource = (string)($state['source'] ?? '');
$rawDestination = (string)($state['destination'] ?? '');
$rawSource = (string) ($state['source'] ?? '');
$rawDestination = (string) ($state['destination'] ?? '');
// Extract IP from "IP[port]" or "IP:port" format
$sourceIp = $this->extractIpFromField($rawSource);
$destinationIp = $this->extractIpFromField($rawDestination);
$sourceNormalized = strtolower(trim($sourceIp));
$destNormalized = strtolower(trim($destinationIp));
$matches = false;
$matchReason = '';
// Normalize raw fields for case-insensitive matching
$rawSourceLower = strtolower($rawSource);
$rawDestinationLower = strtolower($rawDestination);
// Check exact match
foreach ($normalizedServerIps as $serverIp) {
if ($sourceNormalized === $serverIp) {
@@ -265,10 +288,10 @@ class Statistics extends Component
break;
}
}
if ($matches) {
$filteredStates[] = $state;
// Log first few matches for debugging
if (count($sampleChecks) < 5) {
$sampleChecks[] = [
@@ -284,41 +307,10 @@ class Statistics extends Component
}
}
}
// Also search for specific target IPs in raw states for debugging (check ALL states)
$targetIp = '2a0b:4e07:8:1::2:10';
$targetIpLower = strtolower($targetIp);
$targetIpv4 = '45.154.99.28';
$targetIpv4Lower = strtolower($targetIpv4);
$statesWithTargetIp = [];
foreach ($allStates as $state) {
$rawSrc = (string)($state['source'] ?? '');
$rawDst = (string)($state['destination'] ?? '');
$rawSrcLower = strtolower($rawSrc);
$rawDstLower = strtolower($rawDst);
// Check both IPv6 and IPv4 target IPs
if (stripos($rawSrcLower, $targetIpLower) !== false ||
stripos($rawDstLower, $targetIpLower) !== false ||
stripos($rawSrcLower, $targetIpv4Lower) !== false ||
stripos($rawDstLower, $targetIpv4Lower) !== false) {
$statesWithTargetIp[] = [
'id' => $state['id'] ?? null,
'source' => $rawSrc,
'destination' => $rawDst,
'protocol' => $state['protocol'] ?? 'N/A',
'extracted_source' => $this->extractIpFromField($rawSrc),
'extracted_destination' => $this->extractIpFromField($rawDst),
];
if (count($statesWithTargetIp) >= 20) break; // Increase limit for better debugging
}
}
Log::debug('Core Router filtered states result', [
'filtered_count' => count($filteredStates),
'sample_matches' => $sampleChecks,
'target_ip' => $targetIp,
'states_with_target_ip_in_raw' => $statesWithTargetIp,
]);
// Format connections
@@ -333,7 +325,7 @@ class Statistics extends Component
'sample_connections' => array_slice($connections, 0, 3),
]);
$this->networkAnalysis = [
$analysis = [
'connections' => array_slice($connections, 0, 100),
'topConnections' => array_slice($topConnections, 0, 10),
'topPorts' => array_slice($topPorts, 0, 10),
@@ -341,33 +333,41 @@ class Statistics extends Component
'lastUpdated' => now()->toDateTimeString(),
'error' => null,
];
$this->networkAnalysis = $analysis;
// Persist the computed analysis for a short period to speed up subsequent loads
Cache::put($cacheKey, $analysis, now()->addSeconds(30));
// Reset retry count on successful load
$this->networkAnalysisRetryCount = 0;
} catch (\Throwable $e) {
report($e);
$errorMessage = $e->getMessage();
$is502Error = str_contains($errorMessage, 'HTTP 502') || str_contains($errorMessage, '(HTTP 502)');
// Always initialize error arrays first
$this->networkAnalysis['connections'] = [];
$this->networkAnalysis['topConnections'] = [];
$this->networkAnalysis['topPorts'] = [];
$this->networkAnalysis['security'] = [];
// On failure, do not leave any stale cache behind for this server
Cache::forget($cacheKey);
// Check if it's a 502 error and we haven't exceeded max retries
if ($is502Error && $this->networkAnalysisRetryCount < $this->networkAnalysisMaxRetries) {
$this->networkAnalysisRetryCount++;
Log::warning('Core Router API returned 502 error, will retry', [
'retry_count' => $this->networkAnalysisRetryCount,
'max_retries' => $this->networkAnalysisMaxRetries,
]);
// Set error message indicating retry
$this->networkAnalysis['error'] = 'Temporary server error (HTTP 502). Retrying... (Attempt ' . $this->networkAnalysisRetryCount . '/' . $this->networkAnalysisMaxRetries . ')';
// Dispatch event to retry after a delay
$this->dispatch('network-analysis-502-retry', [
'retryCount' => $this->networkAnalysisRetryCount,
@@ -378,7 +378,7 @@ class Statistics extends Component
$this->networkAnalysis['error'] = 'Unable to load network analysis: ' . $errorMessage;
}
} finally {
// Always clear loading state in finally block
// Always clear loading state in finally block so UI can recover
$this->networkAnalysisLoading = false;
}
}
@@ -388,9 +388,10 @@ class Statistics extends Component
*/
public function retryNetworkAnalysis(): void
{
// Reset loading state before retry
// Simple helper that can be invoked from Livewire/JS if needed.
// We intentionally avoid blocking sleep here; the browser-side
// retry logic already introduces a delay between attempts.
$this->networkAnalysisLoading = false;
sleep(2); // Wait 2 seconds before retry
$this->loadNetworkAnalysis();
}