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:
@@ -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).';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user