feat: implement log grouping in Proxmox Firewall component to enhance log display and usability, allowing similar log entries to be collapsed and expanded for better clarity
This commit is contained in:
@@ -923,6 +923,9 @@ class Firewall extends Component
|
||||
}
|
||||
}
|
||||
|
||||
// Group similar logs
|
||||
$this->firewallLogs = $this->groupSimilarLogs($this->firewallLogs);
|
||||
|
||||
\Log::debug('Firewall logs processing complete', [
|
||||
'raw_logs_count' => count($rawLogs),
|
||||
'parsed_logs_count' => count($this->firewallLogs),
|
||||
@@ -957,6 +960,76 @@ class Firewall extends Component
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group similar log entries together
|
||||
*/
|
||||
protected function groupSimilarLogs(array $logs): array
|
||||
{
|
||||
if (empty($logs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$grouped = [];
|
||||
$groups = [];
|
||||
|
||||
foreach ($logs as $log) {
|
||||
// Create a signature for grouping: action + protocol + source + destination + dest_port
|
||||
$signature = sprintf(
|
||||
'%s|%s|%s|%s|%s',
|
||||
$log['action'] ?? '',
|
||||
$log['protocol'] ?? '',
|
||||
$log['source'] ?? '',
|
||||
$log['destination'] ?? '',
|
||||
$log['destination_port'] ?? ''
|
||||
);
|
||||
|
||||
if (!isset($groups[$signature])) {
|
||||
$groups[$signature] = [
|
||||
'signature' => $signature,
|
||||
'count' => 0,
|
||||
'first_log' => $log,
|
||||
'last_timestamp' => $log['timestamp_iso'] ?? null,
|
||||
'first_timestamp' => $log['timestamp_iso'] ?? null,
|
||||
'logs' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$groups[$signature]['count']++;
|
||||
$groups[$signature]['logs'][] = $log;
|
||||
|
||||
// Update timestamps
|
||||
if ($log['timestamp_iso']) {
|
||||
if (!$groups[$signature]['first_timestamp'] || $log['timestamp_iso'] < $groups[$signature]['first_timestamp']) {
|
||||
$groups[$signature]['first_timestamp'] = $log['timestamp_iso'];
|
||||
}
|
||||
if (!$groups[$signature]['last_timestamp'] || $log['timestamp_iso'] > $groups[$signature]['last_timestamp']) {
|
||||
$groups[$signature]['last_timestamp'] = $log['timestamp_iso'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert groups to array and add group metadata
|
||||
foreach ($groups as $group) {
|
||||
$firstLog = $group['first_log'];
|
||||
$firstLog['is_group'] = $group['count'] > 1;
|
||||
$firstLog['group_count'] = $group['count'];
|
||||
$firstLog['grouped_logs'] = $group['logs'];
|
||||
$firstLog['group_first_timestamp'] = $group['first_timestamp'];
|
||||
$firstLog['group_last_timestamp'] = $group['last_timestamp'];
|
||||
|
||||
$grouped[] = $firstLog;
|
||||
}
|
||||
|
||||
// Sort by most recent timestamp
|
||||
usort($grouped, function($a, $b) {
|
||||
$aTime = $a['group_last_timestamp'] ?? $a['timestamp_iso'] ?? '';
|
||||
$bTime = $b['group_last_timestamp'] ?? $b['timestamp_iso'] ?? '';
|
||||
return strcmp($bTime, $aTime); // Descending order (most recent first)
|
||||
});
|
||||
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
protected function parseFirewallLog(string $logText): array
|
||||
{
|
||||
$result = [
|
||||
|
||||
@@ -452,14 +452,68 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="(log, index) in filteredLogs" :key="index">
|
||||
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
|
||||
<td class="py-3 px-4 text-sm" x-text="log.timestamp_display || '-' "></td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold uppercase" :class="log.level_badge_class" x-text="log.level_label || '-' "></span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold" :class="log.action_badge_class" x-text="log.action || '-' "></span>
|
||||
</td>
|
||||
<template x-if="true">
|
||||
<template x-if="log.is_group">
|
||||
<!-- Grouped log row -->
|
||||
<tr x-data="{ expanded: false }" class="border-b border-neutral/20">
|
||||
<td colspan="8" class="p-0">
|
||||
<div class="hover:bg-background-secondary/50">
|
||||
<div class="flex items-center py-3 px-4 cursor-pointer" @click="expanded = !expanded">
|
||||
<div class="flex-1 grid grid-cols-8 gap-4 items-center">
|
||||
<div class="text-sm" x-text="log.timestamp_display || '-'"></div>
|
||||
<div class="text-sm">
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold uppercase" :class="log.level_badge_class" x-text="log.level_label || '-'"></span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold" :class="log.action_badge_class" x-text="log.action || '-'"></span>
|
||||
</div>
|
||||
<div class="text-sm" x-text="log.protocol || '-'"></div>
|
||||
<div class="text-sm font-mono" x-text="log.source || '-'"></div>
|
||||
<div class="text-sm" x-text="log.source_port || '-'"></div>
|
||||
<div class="text-sm font-mono" x-text="log.destination || '-'"></div>
|
||||
<div class="text-sm" x-text="log.destination_port || '-'"></div>
|
||||
</div>
|
||||
<div class="ml-4 flex items-center gap-2">
|
||||
<span class="px-2 py-1 bg-primary/10 text-primary rounded text-xs font-semibold" x-text="`${log.group_count}x`"></span>
|
||||
<svg class="size-4 text-base/50 transition-transform" :class="{ 'rotate-180': expanded }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Expanded details -->
|
||||
<div x-show="expanded" x-collapse class="border-t border-neutral/20 bg-background/50">
|
||||
<div class="p-4 space-y-2">
|
||||
<div class="text-xs text-base/60 mb-2">Showing <span x-text="log.group_count"></span> similar events:</div>
|
||||
<div class="max-h-96 overflow-y-auto space-y-1">
|
||||
<template x-for="(subLog, subIndex) in log.grouped_logs" :key="subIndex">
|
||||
<div class="text-xs font-mono bg-background-secondary/50 p-2 rounded border border-neutral/10">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-base/60" x-text="subLog.timestamp_display"></span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-1.5 py-0.5 rounded text-[10px] font-semibold uppercase" :class="subLog.level_badge_class" x-text="subLog.level_label"></span>
|
||||
<span class="px-1.5 py-0.5 rounded text-[10px] font-semibold" :class="subLog.action_badge_class" x-text="subLog.action"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-base/70 break-all" x-text="subLog.raw"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="!log.is_group">
|
||||
<!-- Single log row -->
|
||||
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
|
||||
<td class="py-3 px-4 text-sm" x-text="log.timestamp_display || '-' "></td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold uppercase" :class="log.level_badge_class" x-text="log.level_label || '-' "></span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold" :class="log.action_badge_class" x-text="log.action || '-' "></span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="flex items-center gap-1">
|
||||
<span x-text="log.protocol || '-'"></span>
|
||||
@@ -692,8 +746,10 @@
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm" x-text="log.destination_port || '-' "></td>
|
||||
</tr>
|
||||
<td class="py-3 px-4 text-sm" x-text="log.destination_port || '-' "></td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user