mirror of
https://github.com/User0332/rewards-farmer.git
synced 2026-09-16 01:31:36 +00:00
switch the bot runtime from print to logging
Closes #14. The runtime modules now log through the stdlib logging module. A new log_utils.setup_logging is called once from main.py, and each module holds its own logging.getLogger(__name__) so every line says which module it came from. The [INFO] and [WARNING] prefixes are gone, since the level field carries that now. [OK], [SKIP] and [FAIL] stay in the message text: they are the per-task outcome summary from complete_all_tasks rather than severities, and folding them into the level would erase the run summary. They map to info, warning and error, which is the one thing print could not express, a real failure now sorts above a task the current UI variant simply does not ship. Two things fall out of having levels at all: - REWARDS_FARMER_LOG_LEVEL=DEBUG attaches the traceback to every [FAIL], which is the stack trace that bug reports keep having to be asked for. - REWARDS_FARMER_LOG_FILE writes the same output to a file, so an unattended run can be read after the fact. Both are off by default, so a normal run looks the same as before apart from the timestamp and level columns. The [FAIL] summary keeps only the first line of the exception message. A selenium exception carries the whole msedgedriver stacktrace inside str(), tens of lines of it, which would turn one task into one screenful and make the log file impossible to scan. The full detail is still there with the traceback on debug. The console stream is stdout rather than the StreamHandler default of stderr, so anyone already redirecting stdout keeps getting the output there, and its error handler is set to replace. Card descriptions are scraped from the page and are not ASCII outside the en-US market, and the Windows console encoding raises on them. check_selectors.py, fitts_law.py and analyze_keypresses.py are left on print. Their output is formatted report text, and prefixing every row of a diagnostic table with a timestamp and a level makes it harder to read.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""Central logging configuration.
|
||||
|
||||
`setup_logging` is called once from `main.py`. Every other module just does
|
||||
`logger = logging.getLogger(__name__)` at import time, which is safe to do
|
||||
before this runs, so import order does not matter.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
LEVEL_ENV_VAR = "REWARDS_FARMER_LOG_LEVEL"
|
||||
FILE_ENV_VAR = "REWARDS_FARMER_LOG_FILE"
|
||||
|
||||
DEFAULT_LEVEL = "INFO"
|
||||
|
||||
# Configuring the root logger switches on output for every library that logs,
|
||||
# not just ours. httpx emits an info line per ollama call, which buries the
|
||||
# task summary and puts the ollama endpoint in the log file. print never did
|
||||
# this because it never touched logging at all, so leaving these at their
|
||||
# default would make the output noisier than what it replaces.
|
||||
NOISY_LIBRARIES = ("httpx", "httpcore", "urllib3", "selenium")
|
||||
|
||||
# CRITICAL is the longest level name at 8 characters, so pad to that and the
|
||||
# message column stays aligned no matter what is being logged.
|
||||
LOG_FORMAT = "%(asctime)s %(levelname)-8s %(name)s: %(message)s"
|
||||
DATE_FORMAT = "%H:%M:%S"
|
||||
|
||||
_configured = False
|
||||
|
||||
|
||||
def _resolve_level(level: str | int | None) -> int:
|
||||
"""Turn a level name, a level number or None into a level number.
|
||||
|
||||
An unusable value falls back to the default rather than raising. A typo in
|
||||
an environment variable must not be able to take down an unattended run.
|
||||
"""
|
||||
if level is None:
|
||||
level = os.environ.get(LEVEL_ENV_VAR, DEFAULT_LEVEL)
|
||||
|
||||
if isinstance(level, int):
|
||||
return level
|
||||
|
||||
resolved = logging.getLevelNamesMapping().get(str(level).strip().upper())
|
||||
|
||||
if resolved is None:
|
||||
logging.getLogger(__name__).warning(
|
||||
"Unknown log level %r, falling back to %s", level, DEFAULT_LEVEL
|
||||
)
|
||||
|
||||
return logging.getLevelNamesMapping()[DEFAULT_LEVEL]
|
||||
|
||||
return resolved
|
||||
|
||||
|
||||
def setup_logging(level: str | int | None = None, log_file: str | None = None) -> None:
|
||||
"""Configure the root logger. Calling this more than once is a no-op.
|
||||
|
||||
`level` defaults to $REWARDS_FARMER_LOG_LEVEL, then to INFO.
|
||||
`log_file` defaults to $REWARDS_FARMER_LOG_FILE, and no file is written
|
||||
when neither is set.
|
||||
"""
|
||||
global _configured
|
||||
|
||||
if _configured:
|
||||
return
|
||||
|
||||
root = logging.getLogger()
|
||||
root.setLevel(_resolve_level(level))
|
||||
|
||||
formatter = logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT)
|
||||
|
||||
# Card descriptions are scraped from the page and are not ASCII outside the
|
||||
# en-US market, which the Windows console encoding cannot represent. Replace
|
||||
# those characters instead of letting the write raise.
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(errors="replace")
|
||||
|
||||
# stdout rather than the StreamHandler default of stderr, because this
|
||||
# replaces print and anyone already redirecting stdout to a file should
|
||||
# keep getting the same output there.
|
||||
console = logging.StreamHandler(sys.stdout)
|
||||
console.setFormatter(formatter)
|
||||
root.addHandler(console)
|
||||
|
||||
if log_file is None:
|
||||
log_file = os.environ.get(FILE_ENV_VAR)
|
||||
|
||||
if log_file:
|
||||
# utf-8 explicitly. Card descriptions are scraped from the page and are
|
||||
# not ASCII outside the en-US market, and the Windows default encoding
|
||||
# would raise on them.
|
||||
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
||||
file_handler.setFormatter(formatter)
|
||||
root.addHandler(file_handler)
|
||||
|
||||
for name in NOISY_LIBRARIES:
|
||||
logging.getLogger(name).setLevel(logging.WARNING)
|
||||
|
||||
_configured = True
|
||||
Reference in New Issue
Block a user