# Observability / Python logging - standard library only, namespaced, plain text

**Purpose + scope.** Every Python module in this repository (data generation,
converters, benchmark runner, extrapolation, reporting, helper tools) MUST
use the standard-library `logging` module following these conventions. No
external logging dependency.

---

## 1. The rule

1. **Standard library `logging` only.** No `loguru`, no `structlog`, no
   `python-json-logger`. Stdlib is sufficient and keeps the dependency list
   closed (see [code-python-style.md](code-python-style.md)).
2. **Namespaced loggers.** Every module obtains its logger via
   `logging.getLogger(__name__)`. Never the root logger directly.
3. **`print()` is for the user-facing CLI protocol only.** The specs
   explicitly require certain stdout output: progress summaries, the real
   generated tree size (task 03), per-cell benchmark progress, final
   counts. That protocol output goes through `print()`. Everything
   diagnostic - internal flow, warnings, errors, per-row detail - goes
   through a logger.
4. **Log level from a CLI flag.** Every pipeline script accepts
   `--log-level {DEBUG,INFO,WARNING,ERROR}`, default `INFO`. There is no
   settings table and no `.env` in this project (see
   [code-config-yaml.md](code-config-yaml.md)).
5. **Plain-text format on stderr.** These are one-shot CLI scripts, not
   services; JSON logs are overkill. One line per record via
   `logging.basicConfig`. Keeping the logger on stderr leaves stdout clean
   for the protocol output of item 3.
6. **Never log secrets.** The only credential material in this project is
   the PostgreSQL DSN - never log it (a DSN may embed a password). Log the
   database name or host, never the full connection string.

## 2. API by severity

| Method | Use for |
|---|---|
| `logger.critical(...)` | The process cannot continue - before `sys.exit(1)` or an unrecoverable state. |
| `logger.error(...)` | Failed operation, exception caught and handled, broken invariant, a benchmark cell recorded as TIMEOUT/ERROR. Always include `exc_info=True` when an exception is in scope. |
| `logger.warning(...)` | Recoverable problem, retry triggered, suspicious state (e.g. a value near a validation bound), an optional step skipped (e.g. cold-cache pass unavailable). |
| `logger.info(...)` | Normal flow milestones - task start/finish, per-batch progress, row counts written, validation gates passed. |
| `logger.debug(...)` | Verbose tracing - per-coupon detail, generated value samples, SQL statements, per-run raw measurements. Hidden at default `INFO`. |

**Default level: `INFO`.** `DEBUG` is opt-in via `--log-level DEBUG` for a
diagnosis session.

## 3. Logger acquisition and setup

```python
# Top of every module that logs:
import logging
logger = logging.getLogger(__name__)

# Once, in each script's main():
logging.basicConfig(
	level=args.log_level,
	stream=sys.stderr,
	format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
)
```

Inside functions:

```python
def load_table(conn, name: str, rows) -> int:
	logger.info("loading %s", name)
	try:
		...
	except psycopg.Error:
		logger.error("loading %s failed", name, exc_info=True)
		raise
```

`__name__` gives the module path as the namespace tree used for filtering.
Never hardcode a logger name string. Clamp noisy third-party libraries
(`psycopg`, `matplotlib`) to `WARNING` in the same setup block if they
pollute the output.

## 4. Progress reporting in long loops

Generation and loading loops run over millions of rows. Report progress at
INFO on a coarse cadence (per batch / per wafer / per table, or every N
seconds), never per row. Per-row output belongs at DEBUG, and even there
prefer sampled output ("first row of each file") over full dumps.

## 5. Anti-patterns

- **`print()` for diagnostics** - forbidden; `print()` is only the
  user-facing CLI protocol (progress summaries, final counts, sizes).
- **Diagnostics on stdout** - stdout is the protocol channel; the logger
  writes to stderr.
- **Root logger calls** (`logging.info(...)`) - forbidden; always
  `logger = logging.getLogger(__name__)`.
- **Third-party logging frameworks** (`loguru`, `structlog`) - forbidden.
- **Hardcoded logger names** (`logging.getLogger("datagen")`) - forbidden;
  pass `__name__`.
- **Logging the PostgreSQL DSN / connection string** - forbidden; log
  host/database name only.
- **Log level from an environment variable or a config file** - the level
  is a per-run diagnostic choice; it comes from `--log-level`.
- **`logger.error(...)` without re-raise** when the function semantically
  cannot proceed - see [code-error-handling.md](code-error-handling.md).
- **Per-row INFO logging in bulk loops** - drowns the signal and slows the
  run; use coarse milestones.

## 6. Related documents

- [code-error-handling.md](code-error-handling.md) - `exc_info=True`
  requirement and the swallow ladder.
- [code-config-yaml.md](code-config-yaml.md) - configuration scope (no
  `.env`, no settings table).
- [code-python-style.md](code-python-style.md) - the closed dependency list.
