refactor: improve Proxmox Firewall log parsing with enhanced handling for various log formats, detailed logging of parsing results, and better error management

This commit is contained in:
2025-12-03 04:08:41 -05:00
parent 2329897941
commit 93d215999c

View File

@@ -812,6 +812,9 @@ class Firewall extends Component
$batchSize = 500; $batchSize = 500;
$maxLogs = $this->logLimit; $maxLogs = $this->logLimit;
$fetchedCount = 0; $fetchedCount = 0;
$totalBodyLength = 0;
$lastContentType = '';
$lastIsJson = false;
// Keep fetching until we have all logs or reach the limit // Keep fetching until we have all logs or reach the limit
do { do {
@@ -846,36 +849,62 @@ class Firewall extends Component
// Proxmox firewall log endpoint can return either text/plain or JSON format // Proxmox firewall log endpoint can return either text/plain or JSON format
// Try to parse as JSON first, then fall back to text/plain // Try to parse as JSON first, then fall back to text/plain
$body = $response->body(); $body = $response->body();
$totalBodyLength += strlen($body);
$batchRawLogs = []; $batchRawLogs = [];
// Check if response is JSON // Check if response is JSON
$contentType = $response->header('Content-Type'); $contentType = $response->header('Content-Type');
$lastContentType = $contentType ?? '';
$isJson = str_contains($contentType ?? '', 'json'); $isJson = str_contains($contentType ?? '', 'json');
$lastIsJson = $isJson;
if ($isJson) { if ($isJson) {
try { try {
$jsonData = $response->json(); $jsonData = $response->json();
// Proxmox may return logs in different formats: // Proxmox may return logs in different formats:
// 1. { "data": [ {line}, {line}, ... ] } // 1. { "data": [ {"n": 1, "t": "log text"}, ... ] }
// 2. { "data": "line1\nline2\n..." } // 2. { "data": [ "line1", "line2", ... ] }
// 3. [ {line}, {line}, ... ] // 3. { "data": "line1\nline2\n..." }
// 4. [ {"n": 1, "t": "log text"}, ... ]
if (isset($jsonData['data'])) { if (isset($jsonData['data'])) {
if (is_array($jsonData['data'])) { if (is_array($jsonData['data'])) {
$batchRawLogs = $jsonData['data']; // Handle array of log objects or strings
foreach ($jsonData['data'] as $item) {
if (is_array($item) && isset($item['t'])) {
// Proxmox format: {"n": 1, "t": "log text"}
$batchRawLogs[] = $item['t'];
} elseif (is_string($item)) {
// Plain string format
$batchRawLogs[] = $item;
} elseif (is_array($item)) {
// Other array format, try to get text from various fields
$text = $item['text'] ?? $item['message'] ?? $item['line'] ?? json_encode($item);
$batchRawLogs[] = $text;
}
}
} elseif (is_string($jsonData['data'])) { } elseif (is_string($jsonData['data'])) {
// Multi-line string format
$lines = preg_split('/\r\n|\r|\n/', $jsonData['data']); $lines = preg_split('/\r\n|\r|\n/', $jsonData['data']);
$batchRawLogs = array_filter($lines, static function ($line) { $batchRawLogs = array_filter($lines, static function ($line) {
return ! empty(trim($line)); return ! empty(trim($line));
}); });
} }
} elseif (is_array($jsonData)) { } elseif (is_array($jsonData)) {
$batchRawLogs = $jsonData; // Array at root level
foreach ($jsonData as $item) {
if (is_array($item) && isset($item['t'])) {
$batchRawLogs[] = $item['t'];
} elseif (is_string($item)) {
$batchRawLogs[] = $item;
}
}
} }
} catch (\Exception $e) { } catch (\Exception $e) {
// Fall through to text parsing // Fall through to text parsing
\Log::warning('Failed to parse JSON response, falling back to text', [ \Log::warning('Failed to parse JSON response, falling back to text', [
'error' => $e->getMessage() 'error' => $e->getMessage(),
'response_sample' => substr($body, 0, 500)
]); ]);
} }
} }
@@ -915,50 +944,78 @@ class Firewall extends Component
\Log::info('Firewall logs fetched from Proxmox API', [ \Log::info('Firewall logs fetched from Proxmox API', [
'raw_count' => count($rawLogs), 'raw_count' => count($rawLogs),
'body_length' => strlen($body), 'total_body_length' => $totalBodyLength,
'content_type' => $contentType, 'last_content_type' => $lastContentType,
'is_json' => $isJson, 'last_is_json' => $lastIsJson,
'sample' => ! empty($rawLogs) ? substr(is_array($rawLogs[0]) ? json_encode($rawLogs[0]) : $rawLogs[0], 0, 200) : null, 'batches_fetched' => ($start / $batchSize),
'first_5_lines' => array_slice($rawLogs, 0, 5), 'first_log_type' => ! empty($rawLogs) ? gettype($rawLogs[0]) : 'empty',
'first_log_length' => ! empty($rawLogs) && is_string($rawLogs[0]) ? strlen($rawLogs[0]) : 'N/A',
'sample' => ! empty($rawLogs) ? substr(is_array($rawLogs[0]) ? json_encode($rawLogs[0]) : $rawLogs[0], 0, 300) : null,
'first_3_logs_full' => array_slice($rawLogs, 0, 3),
]); ]);
$this->firewallLogs = []; $this->firewallLogs = [];
foreach ($rawLogs as $logEntry) { $successfullyParsed = 0;
$skippedEmpty = 0;
$skippedInvalid = 0;
foreach ($rawLogs as $index => $logEntry) {
$logText = ''; $logText = '';
if (is_string($logEntry)) { if (is_string($logEntry)) {
$logText = trim($logEntry); $logText = trim($logEntry);
} elseif (is_array($logEntry)) { } elseif (is_array($logEntry)) {
// Handle various possible array formats from Proxmox API // Handle various possible array formats from Proxmox API
// Note: 't' is the text field, 'n' is the line number - prioritize 't'!
$logText = trim( $logText = trim(
$logEntry['raw'] $logEntry['t'] // Proxmox format: {"n": 1, "t": "log text"}
?? $logEntry['n'] ?? $logEntry['text'] // Alternative text field
?? $logEntry['t'] ?? $logEntry['raw'] // Raw log field
?? $logEntry['text'] ?? $logEntry['message'] // Message field
?? $logEntry['log'] ?? $logEntry['log'] // Log field
?? $logEntry['message'] ?? $logEntry['line'] // Line field
?? $logEntry['line'] ?? json_encode($logEntry) // Last resort: encode the whole thing
?? json_encode($logEntry)
); );
} }
if ($logText === '' || strlen($logText) < 10) { if ($logText === '' || strlen($logText) < 10) {
$skippedEmpty++;
continue; continue;
} }
try { try {
$parsed = $this->parseFirewallLog($logText); $parsed = $this->parseFirewallLog($logText);
if ($parsed !== null) { // Check if the log was actually parsed (has at least action or source/destination)
if ($parsed !== null && ($parsed['action'] !== null || $parsed['source'] !== null || $parsed['destination'] !== null)) {
$this->firewallLogs[] = $parsed; $this->firewallLogs[] = $parsed;
$successfullyParsed++;
} else {
$skippedInvalid++;
if ($index < 5) { // Log first 5 unparseable entries for debugging
\Log::warning('Firewall log could not be parsed (missing key fields)', [
'entry' => substr($logText, 0, 300),
'parsed' => $parsed
]);
}
} }
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::warning('Failed to parse firewall log entry', [ $skippedInvalid++;
'entry' => substr($logText, 0, 200), if ($index < 5) { // Log first 5 errors for debugging
'error' => $e->getMessage() \Log::warning('Failed to parse firewall log entry', [
]); 'entry' => substr($logText, 0, 300),
'error' => $e->getMessage()
]);
}
} }
} }
\Log::info('Firewall log parsing summary', [
'total_raw_logs' => count($rawLogs),
'successfully_parsed' => $successfullyParsed,
'skipped_empty' => $skippedEmpty,
'skipped_invalid' => $skippedInvalid
]);
\Log::info('Firewall logs final count', [ \Log::info('Firewall logs final count', [
'total_logs' => count($this->firewallLogs), 'total_logs' => count($this->firewallLogs),
@@ -966,7 +1023,7 @@ class Firewall extends Component
]); ]);
if (empty($this->firewallLogs) && ! empty($rawLogs)) { if (empty($this->firewallLogs) && ! empty($rawLogs)) {
$this->logError = 'Logs received but could not be processed.'; $this->logError = 'Logs received (' . count($rawLogs) . ') but could not be processed. Successfully parsed: ' . $successfullyParsed . ', Skipped invalid: ' . $skippedInvalid . '. Check Laravel logs for details.';
} elseif (empty($this->firewallLogs)) { } elseif (empty($this->firewallLogs)) {
$this->logError = 'No firewall logs available. Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options).'; $this->logError = 'No firewall logs available. Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options).';
} }