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:
2025-12-02 07:55:10 -05:00
parent 941c43ead3
commit 3158634ab3
2 changed files with 139 additions and 10 deletions

View File

@@ -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 = [

View File

@@ -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>