from __future__ import annotations import logging import re from dataclasses import dataclass from typing import Any, Iterable @dataclass(frozen=True) class QueryLogEvent: """Structured query log event. Attributes: query_name: Short name for the operation (e.g. "get_open_tasks"). sql: SQL statement with parameters redacted/normalized. params: Parameters (redacted where needed). """ query_name: str sql: str params: tuple[Any, ...] | None _SECRET_KEYS = re.compile(r"(pass(word)?|secret|token|key)", re.IGNORECASE) def _redact_value(value: Any) -> Any: """Redact a single value for logging. Args: value: Value to redact. Returns: Redacted value. """ if value is None: return None if isinstance(value, (int, float, bool)): return value s = str(value) if len(s) <= 4: return "***" return s[:2] + "***" + s[-2:] def redact_params(params: Iterable[Any] | None) -> tuple[Any, ...] | None: """Redact query parameters for safe logging. Args: params: DB-API parameter sequence. Returns: Redacted parameter tuple. """ if params is None: return None return tuple(_redact_value(p) for p in params) def normalize_sql(sql: str) -> str: """Normalize SQL for logging. This removes excessive whitespace to keep log lines readable. Args: sql: SQL statement. Returns: Normalized SQL. """ return " ".join(sql.split()) def log_query( *, logger: logging.Logger, query_name: str, sql: str, params: Iterable[Any] | None = None, ) -> None: """Emit a safe query log line. Args: logger: Logger to use. query_name: Short name for the operation. sql: SQL statement. params: DB-API parameters. """ event = QueryLogEvent( query_name=query_name, sql=normalize_sql(sql), params=redact_params(params), ) logger.debug("DB query: %s", event)