logging: add config.LOGS_MAX_TOTAL_SIZE_BYTES: to limit size on disk
What changed, and why it matters
This commit adds a disk-space cap for Electrum's debug log files. It changes how old logs are cleaned up on startup and switches the active log file to a rotating handler so individual files cannot grow without bound. There is no direct security vulnerability being fixed; it is a hardening/operational improvement to prevent logs from filling up the disk.
No urgent action. Treat as routine hardening. Users and packagers may verify that the new default disk usage (up to ~400 MB total: 200 MB old + 200 MB current) is acceptable for their environments.
Security signals we found
disk-space exhaustion mitigation
log rotation hardening
cleanup of stale log files
Evidence from the diff
The patch introduces LOGS_MAX_TOTAL_SIZE_BYTES (default 200 MB) in simple_config.py and uses it in logging.py. delete_old_logs now deletes oldest-first until both the number-of-files and total-size constraints are satisfied. The live log writer is changed from logging.FileHandler to logging.handlers.RotatingFileHandler with backupCount=4 and maxBytes set to roughly one-fifth of the configured max size. A glob pattern change from ‘electrum_log.log’ to ‘electrum_log_.log*’ now also matches rotated suffixes such as .log.1, .log.2, etc. The sort key was rewritten to handle those suffixes correctly.
Changed components
electrum/logging.pyelectrum/simple_config.pyInspect captured patch +57 / −11
diff --git a/electrum/logging.py b/electrum/logging.py
index ce67e84..7df975e 100644
--- a/electrum/logging.py
+++ b/electrum/logging.py
@@ -116,24 +116,52 @@ class TruncatingMemoryHandler(logging.handlers.MemoryHandler):
super().close()
-def _delete_old_logs(path, *, num_files_keep: int):
- files = sorted(list(pathlib.Path(path).glob("electrum_log_*.log")), reverse=True)
- for f in files[num_files_keep:]:
+def _delete_old_logs(path, *, num_files_keep: int, max_total_size: int):
+ """Delete old logfiles, only keeping the latest few."""
+ def sortkey_oldest_first(p: pathlib.PurePath):
+ fname = p.name
+ basename, ext, counter = str(fname).partition(".log")
+ # - each time electrum is launched, there will be a new basename, ordered by date
+ # - for any given basename, there might be multiple log files, differing by counter
+ # - empty counter is newest, then .1 is older, .2 is even older, etc
try:
- os.remove(str(f))
+ counter = int(counter[1:]) if counter else 0 # convert ".2" -> 2
+ except ValueError:
+ _logger.warning(f"failed to parse log file name: {fname}")
+ counter = 0
+ return basename, -counter
+ files = sorted(
+ list(pathlib.Path(path).glob("electrum_log_*.log*")),
+ key=sortkey_oldest_first,
+ )
+ total_size = sum(os.stat(f).st_size for f in files) # in bytes
+ num_files_remaining = len(files)
+ for f in files:
+ fsize = os.stat(f).st_size
+ if total_size < max_total_size and num_files_remaining <= num_files_keep:
+ break
+ total_size -= fsize
+ num_files_remaining -= 1
+ try:
+ os.remove(f)
except OSError as e:
_logger.warning(f"cannot delete old logfile: {e}")
_logfile_path = None
-def _configure_file_logging(log_directory: pathlib.Path, *, num_files_keep: int):
+def _configure_file_logging(
+ log_directory: pathlib.Path,
+ *,
+ num_files_keep: int,
+ max_total_size: int,
+):
from .util import os_chmod
global _logfile_path
assert _logfile_path is None, 'file logging already initialized'
log_directory.mkdir(exist_ok=True, mode=0o700)
- _delete_old_logs(log_directory, num_files_keep=num_files_keep)
+ _delete_old_logs(log_directory, num_files_keep=num_files_keep, max_total_size=max_total_size)
timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
PID = os.getpid()
@@ -142,7 +170,12 @@ def _configure_file_logging(log_directory: pathlib.Path, *, num_files_keep: int)
with open(_logfile_path, "w+") as f:
os_chmod(_logfile_path, 0o600)
- file_handler = logging.FileHandler(_logfile_path, encoding='utf-8')
+ logfile_backupcount = 4
+ file_handler = logging.handlers.RotatingFileHandler(
+ _logfile_path,
+ maxBytes=max_total_size // (logfile_backupcount+1),
+ backupCount=logfile_backupcount,
+ encoding='utf-8')
file_handler.setFormatter(file_formatter)
file_handler.setLevel(logging.DEBUG)
root_logger.addHandler(file_handler)
@@ -236,8 +269,9 @@ electrum_logger.setLevel(logging.DEBUG)
# --- External API
def get_logger(name: str) -> _CustomLogger:
- if name.startswith("electrum."):
- name = name[9:]
+ prefix = "electrum."
+ if name.startswith(prefix):
+ name = name[len(prefix):]
return electrum_logger.getChild(name)
@@ -283,7 +317,8 @@ def configure_logging(config: 'SimpleConfig', *, log_to_file: Optional[bool] = N
if log_to_file:
log_directory = pathlib.Path(config.path) / "logs"
num_files_keep = config.LOGS_NUM_FILES_KEEP
- _configure_file_logging(log_directory, num_files_keep=num_files_keep)
+ max_total_size = config.LOGS_MAX_TOTAL_SIZE_BYTES
+ _configure_file_logging(log_directory, num_files_keep=num_files_keep, max_total_size=max_total_size)
# clean up and delete in-memory logs
global _inmemory_startup_logs
diff --git a/electrum/simple_config.py b/electrum/simple_config.py
index aecec71..f356f26 100644
--- a/electrum/simple_config.py
+++ b/electrum/simple_config.py
@@ -892,7 +892,18 @@ Warning: setting this to too low will result in lots of payment failures."""),
short_desc=lambda: _("Write logs to file"),
long_desc=lambda: _('Debug logs can be persisted to disk. These are useful for troubleshooting.'),
)
- LOGS_NUM_FILES_KEEP = ConfigVar('logs_num_files_keep', default=30, type_=int)
+ LOGS_NUM_FILES_KEEP = ConfigVar(
+ 'logs_num_files_keep', default=30, type_=int,
+ long_desc=lambda: _("Old log files get deleted on startup, with only the newest few being kept."),
+ )
+ LOGS_MAX_TOTAL_SIZE_BYTES = ConfigVar(
+ 'logs_max_total_size', default=200_000_000, type_=int,
+ long_desc=lambda: _(
+ "Old log files get deleted on startup. "
+ "This value limits the max total size of the old log files kept, "
+ "and also separately the max size of the current log file. "
+ "Hence, the max disk usage will be twice this value."),
+ )
GUI_ENABLE_DEBUG_LOGS = ConfigVar('gui_enable_debug_logs', default=False, type_=bool)
LOCALIZATION_LANGUAGE = ConfigVar(
'language', default="", type_=str,
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.