trader.logging_setup module¶
Application logging (requirements §11).
Deliberately narrow: one rotating file handler, attached to the trader logger and to every third-party logger this app is known to trigger, and one helper that renders a config safely.
Why third-party loggers get the same handler explicitly, rather than relying on propagation. yfinance logs through logging.getLogger (‘yfinance’) — a sibling of “trader”, not a child of it, so it never inherited the handler attached here. With no handler anywhere in ITS ancestor chain either (the root logger has none by default), those records fell through to Python’s built-in logging.lastResort handler: a bare StreamHandler(sys.stderr) with no formatter, printing just the raw message. Confirmed live, 2026-08-21: daemon-error.log filled with lines like HTTP Error 404: {“quoteSummary”: …} — a real yfinance-logged ERROR (an ETF with no fundamentals, an already-handled, expected case — see marketdata/analysts.py) with none of the %(asctime)s %(levelname)s %(name)s context every line in trader.log carries, making a deploy’s “check the error log” step impossible to read at a glance. _THIRD_PARTY_ LOGGER_NAMES is deliberately a short, explicit list — not the root logger — so a future noisy DEBUG-level library does not silently start writing to this file just because it happens to log through the standard module.
The helper is not a convenience. EffectiveTradingConfig marks its credential fields repr=False, so repr(config) is safe — but dataclasses.asdict() walks the fields directly and returns the secrets in clear text. Logging is the first thing in this codebase that writes to a durable file, so the first logger.debug(“config=%s”, asdict(config)) would put a live Alpaca key on disk. Log safe_config_summary(config) instead, never the config.
- trader.logging_setup.configure_logging(level=None, log_path=None)[source]¶
Attach a rotating file handler to the trader logger.
Idempotent: calling twice does not double every log line.
level=None means “read LOG_LEVEL from the environment”. An explicit level still wins, which is what keeps the tests in this module independent of whatever the developer has in their own .env.
Deliberately does not create path.parent here. The handler is built with delay=True, so nothing is opened or created on disk until the first record is actually emitted — a command that configures logging and then does nothing else (every CLI command’s –help, via the app callback) creates no logs/ directory as a side effect of printing usage text.
Raises ConfigError, not pydantic’s ValidationError, when LOG_LEVEL is unrecognised. This runs from the Typer app callback, before any command’s own try/except TraderError block, so an uncaught ValidationError would print a raw traceback instead of the one-line Error: … every other bad config value in this app produces — the valid values were still named, just buried in ten lines of rich formatting a typo does not deserve. Wrapping it here means the CLI only ever has to handle TraderError, not pydantic’s error type as well.
Settings() validates every field at once, not just log_level — so a bad TRADING_MODE raises the same ValidationError this function is watching for. Relabelling unconditionally previously turned that into “Invalid LOG_LEVEL: … trading_mode …”, which sends whoever is debugging a safety-relevant typo to the wrong line of .env. Only relabel when every error pydantic reports is on log_level; anything else re-raises the original ValidationError untouched, exactly as it propagated before this wrapping existed.
- Parameters:
level (int | None)
log_path (Path | None)
- Return type:
None
- trader.logging_setup.safe_config_summary(config)[source]¶
Render a config for logging with the credentials removed.
Returns only the operational fields. api_key and api_secret are absent by construction rather than masked, so a future field added to the config cannot leak through this function by default.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
dict[str, str]