Add fail2ban DB index management and socket-based path resolution

- New get_fail2ban_db_path() in setup_service resolves DB path from configured socket path
- New ensure_fail2ban_indexes() creates missing performance indexes on bans table
- Call ensure_fail2ban_indexes on every startup before first ban query
- Remove completed tasks from Docs/Tasks.md
- Update Docs/PERFORMANCE.md with index findings
This commit is contained in:
Copilot
2026-05-03 12:17:31 +02:00
committed by Lukas
parent 0133489920
commit 22db607875
6 changed files with 189 additions and 50 deletions

View File

@@ -5,6 +5,10 @@ from __future__ import annotations
import json
from datetime import UTC, datetime
import structlog
log: structlog.stdlib.BoundLogger = structlog.get_logger()
def escape_like(s: str) -> str:
"""Escape SQLite LIKE wildcard characters in a string.
@@ -21,6 +25,42 @@ def escape_like(s: str) -> str:
return s.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
async def ensure_fail2ban_indexes(db_path: str) -> None:
"""Create performance indexes on the fail2ban bans table if missing.
The fail2ban database schema does not include an index on timeofban alone,
only composite indexes (jail, timeofban) and (jail, ip). Queries that filter
by timeofban >= X ORDER BY timeofban DESC require a full table scan.
This function adds the missing index idempotently (CREATE INDEX IF NOT EXISTS)
each time the application starts. The overhead of the check is negligible
compared to the query speedup on large tables.
Args:
db_path: Path to the fail2ban SQLite database.
"""
import aiosqlite
index_name = "idx_bans_timeofban_desc"
# Check existing indexes using read-only connection
async with aiosqlite.connect(f"file:{db_path}?mode=ro", uri=True) as db:
async with db.execute(
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='bans'"
) as cursor:
rows = await cursor.fetchall()
existing = [str(r[0]) for r in rows]
if index_name not in existing:
log.info("creating_fail2ban_bans_index", index=index_name, db_path=db_path)
async with aiosqlite.connect(db_path) as db:
await db.execute(f"CREATE INDEX IF NOT EXISTS {index_name} ON bans(timeofban DESC);")
await db.commit()
log.info("fail2ban_bans_index_created", index=index_name)
else:
log.debug("fail2ban_bans_index_exists", index=index_name)
def ts_to_iso(unix_ts: int) -> str:
"""Convert a Unix timestamp to an ISO 8601 UTC string."""
return datetime.fromtimestamp(unix_ts, tz=UTC).isoformat()