Skip to content

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:

  1. 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.

  2. log → stderr only: 27 modules declare log = logging.getLogger(__name__), but logging.basicConfig() only configures a StreamHandler(stderr). Docker can temporarily view stderr via docker logs, but it is lost after container restart.

  3. No FileHandler: docker-compose.yml has a commented line # - ./logs:/app/logs that has never been enabled. There are no persistent log files.

  4. Inconsistent Style: core/committee_runner.py exclusively uses log.warning() (15 occurrences), while jobs/daily_report.py in the same layer exclusively uses print() (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 in scripts/skill_cmds/* (originally scripts/skill.py, refactored into the skill_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:

python
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 ​

LevelScenarioExample
log.info()Normal flow milestones"SOLVENCY=strong + TRIM → HOLD", job start/success
log.warning()Degraded but recoverablePrice fetch failure using historical values, backup_cny extraction failure defaulting to 0
log.error()Single asset/function failureA symbol committee failure (skipped)
log.exception()Unexpected exception requiring stacktraceEmail send failure, database connection loss

4. Module Logger Naming ​

python
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*.py are user-facing; print offers better readability

Migration Scope ​

Files requiring migration (print → log):

FilePrint CountPriority
jobs/daily_report.py12P0 — Daily core pipeline
core/committee.py (now core/committee/ package, print/log spread across 6 submodules, total unchanged)7P0 — Decision engine
scripts/skill_cmds/*7P1 — 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.py5P1 — Review pipeline
services/news.py6P1 — News collection
services/commsec_reader.py5P2 — CommSec sync
jobs/dreaming.py1P2
jobs/pnl_snapshot.py2P2
jobs/payday_check.py1P2
jobs/commsec_sync.py1P2
jobs/weekly_review.py1P2
scheduler/runner.py3P2 — Mostly already using log

Next Steps ​

  1. Approve this ADR
  2. PR-1: Add FileHandler to scheduler/runner.py + enable logs volume in docker-compose.yml
  3. PR-2: P0 files (daily_report + committee) print → log
  4. PR-3: P1 + P2 files

References ​