From 0f1a2823166b030d00fd20b1c4461dff4695a3ad Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 7 Jul 2025 14:25:20 +0000 Subject: [PATCH] logging: add "only_once=False" param to logger.info/warning/etc calls very basic form of opt-in rate-limiting of log spam --- electrum/i18n.py | 12 +++++++++--- electrum/logging.py | 23 +++++++++++++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/electrum/i18n.py b/electrum/i18n.py index 348411c4f15a..5734f72e5610 100644 --- a/electrum/i18n.py +++ b/electrum/i18n.py @@ -58,17 +58,23 @@ def safe_translator(msg: str, **kwargs): try: parsed2 = list(sf.parse(translation)) except ValueError: # malformed format string in translation - _logger.info(f"rejected translation string: failed to parse. original={msg!r}. {translation=!r}") + _logger.warning( + f"rejected translation string: failed to parse. original={msg!r}. {translation=!r}", + only_once=True) return msg # num of replacement fields must match: if len(parsed1) != len(parsed2): - _logger.info(f"rejected translation string: num replacement fields mismatch. original={msg!r}. {translation=!r}") + _logger.warning( + f"rejected translation string: num replacement fields mismatch. original={msg!r}. {translation=!r}", + only_once=True) return msg # set of "field_name"s must not change. (re-ordering is explicitly allowed): field_names1 = set(tupl[1] for tupl in parsed1) field_names2 = set(tupl[1] for tupl in parsed2) if field_names1 != field_names2: - _logger.info(f"rejected translation string: set of field_names mismatch. original={msg!r}. {translation=!r}") + _logger.warning( + f"rejected translation string: set of field_names mismatch. original={msg!r}. {translation=!r}", + only_once=True) return msg # checks done. return translation diff --git a/electrum/logging.py b/electrum/logging.py index 32181b817130..ce67e84fddf6 100644 --- a/electrum/logging.py +++ b/electrum/logging.py @@ -9,9 +9,10 @@ import pathlib import os import platform -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Set import copy import subprocess +import hashlib if TYPE_CHECKING: from .simple_config import SimpleConfig @@ -190,6 +191,24 @@ def _process_verbosity_log_levels(verbosity): raise Exception(f"invalid log filter: {filt}") +class _CustomLogger(logging.getLoggerClass()): + def __init__(self, name, *args, **kwargs): + super().__init__(name, *args, **kwargs) + self.msg_hashes_seen = set() # type: Set[bytes] + # ^ note: size grows without bounds, but only for log lines using "only_once". + + def _log(self, level, msg: str, *args, only_once: bool = False, **kwargs) -> None: + """Overridden to add 'only_once' arg to logger.debug()/logger.info()/logger.warning()/etc.""" + if only_once: # if set, this logger will only log this msg a single time during its lifecycle + msg_hash = hashlib.sha256(msg.encode("utf-8")).digest() + if msg_hash in self.msg_hashes_seen: + return + self.msg_hashes_seen.add(msg_hash) + super()._log(level, msg, *args, **kwargs) + +logging.setLoggerClass(_CustomLogger) + + # enable logs universally (including for other libraries) root_logger = logging.getLogger() root_logger.setLevel(logging.WARNING) @@ -216,7 +235,7 @@ def _process_verbosity_log_levels(verbosity): # --- External API -def get_logger(name: str) -> logging.Logger: +def get_logger(name: str) -> _CustomLogger: if name.startswith("electrum."): name = name[9:] return electrum_logger.getChild(name)