ADR-014: Production Code Logging Convention
Status: Proposed Date: 2026-05-28 Author: Code Review
Background
The system is self-looping: scheduler/runner.py drives all jobs via APScheduler's BackgroundScheduler, running inside a Docker container with no human interaction.
Current Issues:
print → Black Hole: 51
print()calls across 16 production files output to stdout. Docker does not persist stdout by default, and the scheduler does not capture stdout. These diagnostic messages have never actually been seen by anyone.log → stderr only: 27 modules declare
log = logging.getLogger(__name__), butlogging.basicConfig()only configures a StreamHandler(stderr). Docker can temporarily view stderr viadocker logs, but it is lost after container restart.No FileHandler:
docker-compose.ymlhas a commented line# - ./logs:/app/logsthat has never been enabled. There are no persistent log files.Inconsistent Style:
core/committee_runner.pyexclusively useslog.warning()(15 occurrences), whilejobs/daily_report.pyin the same layer exclusively usesprint()(12 occurrences). No unified convention.
Impact:
- Sanity check 4 forces HOLD (print) → No record, unable to review
- Price fetch failure (print) → No record, unable to debug data issues
- backup_cny extraction failure (recently added print) → No record
Decision
1. Prohibit print() for Diagnostics in Production Code
print() is only allowed in two scenarios:
- CLI script final output:
cmd_*()functions inscripts/skill_cmds/*(originallyscripts/skill.py, refactored into theskill_cmds/subpackage) for JSON/text directly shown to the user - Interactive entry points with
if __name__ == "__main__": e.g.,print(run())
All diagnostics, warnings, and error messages must use log.*().
2. Unified Logging Configuration
Add a FileHandler to logging.basicConfig() in scheduler/runner.py:
LOG_DIR = ROOT / "logs"
LOG_DIR.mkdir(exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[
logging.StreamHandler(sys.stderr),
logging.handlers.RotatingFileHandler(
LOG_DIR / "invest.log",
maxBytes=10 * 1024 * 1024, # 10 MB
backupCount=5,
encoding="utf-8",
),
],
)Also uncomment ./logs:/app/logs in docker-compose.yml.
3. Log Level Usage Convention
| Level | Scenario | Example |
|---|---|---|
log.info() | Normal flow milestones | "SOLVENCY=strong + TRIM → HOLD", job start/success |
log.warning() | Degraded but recoverable | Price fetch failure using historical values, backup_cny extraction failure defaulting to 0 |
log.error() | Single asset/function failure | A symbol committee failure (skipped) |
log.exception() | Unexpected exception requiring stacktrace | Email send failure, database connection loss |
4. Module Logger Naming
import logging
log = logging.getLogger(__name__)Follow the existing convention of 27 modules. Declare once at the top of each module and reuse throughout the file.
What We Are Not Doing
- Not introducing structlog / loguru: The project already has 27 modules using stdlib logging; migration cost is high with little benefit
- Not modifying the experiments/ directory: Experiment scripts are interactive; print is reasonable
- Not modifying sandbox scripts: Evaluation scripts like
run_sandbox_v*.pyare user-facing; print offers better readability
Migration Scope
Files requiring migration (print → log):
| File | Print Count | Priority |
|---|---|---|
jobs/daily_report.py | 12 | P0 — Daily core pipeline |
core/committee.py (now core/committee/ package, print/log spread across 6 submodules, total unchanged) | 7 | P0 — Decision engine |
scripts/skill_cmds/* | 7 | P1 — Skill entry points (refactored from scripts/skill.py into skill_cmds/ package: analysis_cmds.py 2 occurrences / lifecycle_cmds.py remaining; some print calls are CLI output, to be kept) |
jobs/verdict_review.py | 5 | P1 — Review pipeline |
services/news.py | 6 | P1 — News collection |
services/commsec_reader.py | 5 | P2 — CommSec sync |
jobs/dreaming.py | 1 | P2 |
jobs/pnl_snapshot.py | 2 | P2 |
jobs/payday_check.py | 1 | P2 |
jobs/commsec_sync.py | 1 | P2 |
jobs/weekly_review.py | 1 | P2 |
scheduler/runner.py | 3 | P2 — Mostly already using log |
Next Steps
- Approve this ADR
- PR-1: Add FileHandler to
scheduler/runner.py+ enable logs volume indocker-compose.yml - PR-2: P0 files (daily_report + committee) print → log
- PR-3: P1 + P2 files
References
core/committee_runner.py— Reference implementation fully using log- Python logging HOWTO