fix: deduplicate rcon admin log events by canonical message
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -29,12 +30,15 @@ def initialize_rcon_admin_log_storage(*, db_path: Path | None = None) -> Path:
|
|||||||
relative_time TEXT,
|
relative_time TEXT,
|
||||||
event_type TEXT NOT NULL,
|
event_type TEXT NOT NULL,
|
||||||
raw_message TEXT NOT NULL,
|
raw_message TEXT NOT NULL,
|
||||||
|
canonical_message TEXT NOT NULL,
|
||||||
parsed_payload_json TEXT NOT NULL,
|
parsed_payload_json TEXT NOT NULL,
|
||||||
raw_entry_json TEXT NOT NULL,
|
raw_entry_json TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
UNIQUE(target_key, server_time, raw_message)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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
|
CREATE INDEX IF NOT EXISTS idx_rcon_admin_log_events_target_time
|
||||||
ON rcon_admin_log_events(target_key, server_time DESC);
|
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);
|
ON rcon_admin_log_events(event_type);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
_ensure_canonical_message_column(connection)
|
||||||
|
|
||||||
return resolved_path
|
return resolved_path
|
||||||
|
|
||||||
@@ -65,9 +70,11 @@ def persist_rcon_admin_log_entries(
|
|||||||
with connect_sqlite_writer(resolved_path) as connection:
|
with connect_sqlite_writer(resolved_path) as connection:
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
parsed = parse_rcon_admin_log_entry(entry)
|
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(
|
cursor = connection.execute(
|
||||||
"""
|
"""
|
||||||
INSERT OR IGNORE INTO rcon_admin_log_events (
|
INSERT INTO rcon_admin_log_events (
|
||||||
target_key,
|
target_key,
|
||||||
external_server_id,
|
external_server_id,
|
||||||
event_timestamp,
|
event_timestamp,
|
||||||
@@ -75,9 +82,18 @@ def persist_rcon_admin_log_entries(
|
|||||||
relative_time,
|
relative_time,
|
||||||
event_type,
|
event_type,
|
||||||
raw_message,
|
raw_message,
|
||||||
|
canonical_message,
|
||||||
parsed_payload_json,
|
parsed_payload_json,
|
||||||
raw_entry_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,
|
target_key,
|
||||||
@@ -86,9 +102,13 @@ def persist_rcon_admin_log_entries(
|
|||||||
parsed.get("server_time"),
|
parsed.get("server_time"),
|
||||||
parsed.get("relative_time"),
|
parsed.get("relative_time"),
|
||||||
parsed.get("event_type") or "unknown",
|
parsed.get("event_type") or "unknown",
|
||||||
parsed.get("raw_message") or "",
|
raw_message,
|
||||||
|
canonical_message,
|
||||||
json.dumps(parsed, ensure_ascii=False, separators=(",", ":")),
|
json.dumps(parsed, ensure_ascii=False, separators=(",", ":")),
|
||||||
json.dumps(entry, 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):
|
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]]:
|
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."""
|
"""Return event counts grouped by target and event type."""
|
||||||
resolved_path = db_path or get_storage_path()
|
resolved_path = db_path or get_storage_path()
|
||||||
|
|||||||
Reference in New Issue
Block a user