From a5231ab5842615c026d49fa1d1e8feda7139064a Mon Sep 17 00:00:00 2001 From: devRaGonSa Date: Tue, 19 May 2026 13:54:57 +0200 Subject: [PATCH] fix: deduplicate rcon admin log events by canonical message --- backend/app/rcon_admin_log_storage.py | 63 ++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/backend/app/rcon_admin_log_storage.py b/backend/app/rcon_admin_log_storage.py index c30da28..459e95e 100644 --- a/backend/app/rcon_admin_log_storage.py +++ b/backend/app/rcon_admin_log_storage.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import re import sqlite3 from collections.abc import Mapping from pathlib import Path @@ -29,12 +30,15 @@ def initialize_rcon_admin_log_storage(*, db_path: Path | None = None) -> Path: relative_time TEXT, event_type TEXT NOT NULL, raw_message TEXT NOT NULL, + canonical_message TEXT NOT NULL, parsed_payload_json TEXT NOT NULL, raw_entry_json TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE(target_key, server_time, raw_message) + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); + CREATE INDEX IF NOT EXISTS idx_rcon_admin_log_events_dedupe + ON rcon_admin_log_events(target_key, server_time, canonical_message); + CREATE INDEX IF NOT EXISTS idx_rcon_admin_log_events_target_time ON rcon_admin_log_events(target_key, server_time DESC); @@ -42,6 +46,7 @@ def initialize_rcon_admin_log_storage(*, db_path: Path | None = None) -> Path: ON rcon_admin_log_events(event_type); """ ) + _ensure_canonical_message_column(connection) return resolved_path @@ -65,9 +70,11 @@ def persist_rcon_admin_log_entries( with connect_sqlite_writer(resolved_path) as connection: for entry in entries: parsed = parse_rcon_admin_log_entry(entry) + raw_message = str(parsed.get("raw_message") or "") + canonical_message = _canonicalize_admin_log_message(raw_message) cursor = connection.execute( """ - INSERT OR IGNORE INTO rcon_admin_log_events ( + INSERT INTO rcon_admin_log_events ( target_key, external_server_id, event_timestamp, @@ -75,9 +82,18 @@ def persist_rcon_admin_log_entries( relative_time, event_type, raw_message, + canonical_message, parsed_payload_json, raw_entry_json - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ) + SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + WHERE NOT EXISTS ( + SELECT 1 + FROM rcon_admin_log_events + WHERE target_key = ? + AND server_time IS ? + AND canonical_message = ? + ) """, ( target_key, @@ -86,9 +102,13 @@ def persist_rcon_admin_log_entries( parsed.get("server_time"), parsed.get("relative_time"), parsed.get("event_type") or "unknown", - parsed.get("raw_message") or "", + raw_message, + canonical_message, json.dumps(parsed, ensure_ascii=False, separators=(",", ":")), json.dumps(entry, ensure_ascii=False, separators=(",", ":")), + target_key, + parsed.get("server_time"), + canonical_message, ), ) if int(cursor.rowcount or 0): @@ -103,6 +123,39 @@ def persist_rcon_admin_log_entries( } +_PREFIX_RE = re.compile(r"^\[.*?\(\d+\)\]\s+", re.DOTALL) + + +def _canonicalize_admin_log_message(raw_message: str) -> str: + """Return a stable message body for deduplication across repeated AdminLog reads.""" + normalized = str(raw_message or "").strip() + return _PREFIX_RE.sub("", normalized).strip() + + +def _ensure_canonical_message_column(connection: sqlite3.Connection) -> None: + columns = { + row["name"] + for row in connection.execute("PRAGMA table_info(rcon_admin_log_events)").fetchall() + } + if "canonical_message" not in columns: + connection.execute( + "ALTER TABLE rcon_admin_log_events ADD COLUMN canonical_message TEXT NOT NULL DEFAULT ''" + ) + connection.execute( + """ + UPDATE rcon_admin_log_events + SET canonical_message = raw_message + WHERE canonical_message = '' + """ + ) + connection.execute( + """ + CREATE INDEX IF NOT EXISTS idx_rcon_admin_log_events_dedupe + ON rcon_admin_log_events(target_key, server_time, canonical_message) + """ + ) + + def list_rcon_admin_log_event_counts(*, db_path: Path | None = None) -> list[dict[str, object]]: """Return event counts grouped by target and event type.""" resolved_path = db_path or get_storage_path()