Logging, Config & CLI Ergonomics
- Replace print() debugging with the logging module and justify the swap
- Configure log levels, formats, and handlers, and toggle verbosity with a flag
- Build an argparse CLI with subcommands, typed arguments, and helpful --help text
- Load configuration with sane precedence: CLI flag > environment variable > default
- Return meaningful exit codes so scripts compose in shell pipelines
| Spaced-rep warm-up: due flashcards (incl. Day 15 hints) | 10 min |
| ELI5 + tech read: levels, handlers, argparse, exit codes | 20 min |
| Guided: printβlogging swap + subcommands & exit codes | 40 min |
| Practice: config precedence function with terminal proof | 20 min |
| Project: the analyzer cockpit | 20 min |
| Quiz + flashcards | 10 min |
Builds on: Day 5 β Files & errors Β· Day 6 β The terminal & Linux Β· Day 14 β Week 2 project β the log analyzer
Airliners carry a flight recorder β the orange "black box" β that continuously writes down altitude, speed, and every switch the crew flips. Nobody reads it during a normal flight. But when something goes wrong over the ocean at 3 a.m., investigators don't have to *reproduce* the incident; they replay the recording. Compare that to a pilot shouting observations out the window: loud, unrecorded, gone.
print() is shouting out the window. The logging module is the flight recorder: every message gets a timestamp, a severity (was this routine cruising data or an engine fire?), and a destination β screen, file, or both. Best of all, the recorder has a dial: in normal operation you record only WARNING and above; when investigating, you turn the dial to DEBUG and the same code suddenly narrates everything, no rewrites needed. Today you also give your programs a proper cockpit: a command-line interface with subcommands and --help, configuration that can come from flags or environment variables, and exit codes β the single number a program leaves behind that tells the rest of the system "landed fine" or "declare emergency."
Every production system you will run β the FastAPI service on Day 42, the RAG capstone from Day 119 β is debugged almost entirely through its logs, because you cannot attach a debugger to a crashed 3 a.m. process. LLM apps raise the stakes: on Day 142 you'll add tracing (the flight recorder's big sibling) to record every prompt, retrieval, and tool call. FDEs live in customer environments where logs are often the ONLY access you get β "send me the log file" is how remote debugging starts. Learn the recorder now, on a small plane.
Guided practice
From print to flight recorder
20 min- Create
recorder_lab.pyfrom the starter code β it processes fake log lines with print() calls sprinkled in. - Replace every print with the appropriate logger call: routine progress ->
logger.debug, notable events ->logger.info, recoverable problems ->logger.warning, failures ->logger.error. - Terminal: run
python recorder_lab.pyβ at the default INFO level the debug chatter is gone. - Add the
--verboseflag wiring from the starter (sets level to DEBUG) and runpython recorder_lab.py --verboseβ the full narration returns without touching the processing code. That is the dial. - Add a FileHandler so everything from DEBUG up also lands in
run.logwhile the console stays at your chosen level. Run once and inspectrun.logwithcat run.log.
Subcommands and exit codes
20 min- Create
cli_lab.pyfrom the starter: an argparse CLI with two subcommands,reportandtop-ips, each with its own options. - Terminal: run
python cli_lab.py --help, thenpython cli_lab.py top-ips --helpβ notice argparse wrote both help screens for you. - Run
python cli_lab.py report missing.logβ it should log an error and exit with code 1. Verify in the terminal withecho $?(PowerShell:echo $LASTEXITCODE) immediately after. - Run it on a real file and verify
echo $?prints 0. - Chain it:
python cli_lab.py report sample.log && echo "pipeline continues"β the echo only fires on success. This is why exit codes matter.
On your own
Config precedence, implemented honestly
20 minWrite get_log_level(cli_value) used by your CLI: it must return the level from the CLI flag if given, else from the environment variable ANALYZER_LOG_LEVEL if set, else default to WARNING β and it must reject invalid values with a clear error and exit code 2.
Verify all three tiers in the terminal: (1) no flag, no env var; (2) export ANALYZER_LOG_LEVEL=DEBUG then run (Windows PowerShell: $env:ANALYZER_LOG_LEVEL="DEBUG"); (3) env var set AND --log-level ERROR passed β the flag must win. Prove each with a debug line that only appears at the right tier.
Hints: os.environ.get(name) returns None when unset; logging.getLevelName("DEBUG") maps names to numbers; precedence is just ordered "first non-None wins" logic.
Give the analyzer a cockpit
Upgrade your Day 14/15 log analyzer into a proper CLI tool: (1) subcommands report, top-ips, and errors via argparse, each with --limit-style options and real help text; (2) all print-debugging replaced by a module logger with the timestamped format, --verbose dial, and a FileHandler writing analyzer.log; (3) config precedence for the log level (flag > ANALYZER_LOG_LEVEL > default); (4) exit codes: 0 on success, 1 on missing/unreadable input, 2 on bad arguments. Demonstrate the exit codes with a short shell session pasted into your journal. Commit the upgrade β tomorrow this tool gets packaged, and on Day 21 it ships.
Common mistakes & misconceptions
- Using print() for diagnostics because "it works." It has no levels, timestamps, destinations, or off-switch β the four things you need the night production breaks. Reserve print for the program's actual output.
- Logging at the wrong level: everything at INFO (can never turn the noise down) or everything at ERROR (real alarms drown). Ask: who needs this line, and when?
- Calling logging.basicConfig() deep inside library code. Configuration belongs to the application entry point; libraries should only getLogger(__name__) and emit.
- Building string messages eagerly: logger.debug("row " + str(huge)) pays the formatting cost even when DEBUG is off. Use lazy %-style: logger.debug("row %s", huge).
- Printing "ERROR: ..." but exiting 0. Shells, cron, and CI only see the exit code β a failing script that exits 0 silently corrupts every pipeline built on it.
- Committing .env files or secrets in config. Environment-based config exists precisely to keep secrets OUT of the repo; .env goes in .gitignore on Day 20 if it is not there already.
Q1. Your code is full of logger.debug(...) calls but the level is set to INFO. What happens?
Q2. A script prints "FATAL: could not open file" but ends with sys.exit(0). What does a CI pipeline that ran it conclude?
Q3. With config precedence "flag > env var > default", the env var says DEBUG and the user passes --log-level ERROR. Effective level?
Go deeper β curated resources
- Structured (JSON) logging β Production services often log one JSON object per line so machines can query logs. Try formatting a record as JSON with the extra= parameter β this becomes standard practice in the capstone (Day 143).
- recorder_lab.py shows the verbosity dial working (INFO vs --verbose) with run.log capturing everything
- echo $? verified for success, missing-file, and bad-argument cases
- Analyzer upgraded with subcommands, logging, config precedence β committed
- Quiz β₯ 2/3
β Back: Day 6 gave you the shell where exit codes and env vars live; Day 5 gave you the file handling these subcommands wrap. The analyzer itself is the Day 14 project, freshly cleaned on Day 15.
Forward β: Day 17 packages this CLI as an installable console script. The flight-recorder idea returns at production scale on Day 142 (tracing LLM apps) and the log-mining flywheel on Day 143; environment-based config becomes 12-factor discipline on Day 45.
Unlocks: D17 Packaging & Environments Β· D39 Operating Systems Essentials Β· D45 Web Service Architecture Β· D142 Tracing LLM Applications