Logging conventions for src/unihan_db/**. These rules guide future
logging changes; existing code may not yet conform. Everything else about
how this package is written — docstrings, comments, commits — is in
../../.github/WRITING.md; this file only covers
log calls.
- Use
logging.getLogger(__name__)in every module. - Add
NullHandlerin library__init__.pyfiles. - Never configure handlers, levels, or formatters in library code — that is the application's job.
Pass structured data on every log call where useful for filtering, searching, or test assertions.
Core keys (stable, scalar, safe at any log level):
| Key | Type | Context |
|---|---|---|
unihan_field |
str |
UNIHAN field name |
unihan_source_file |
str |
source data file path |
unihan_record_count |
int |
records processed |
unihan_db_table |
str |
database table name |
unihan_db_rows |
int |
rows affected |
Heavy/optional keys (DEBUG only, potentially large):
| Key | Type | Context |
|---|---|---|
unihan_stdout |
list[str] |
subprocess stdout lines (truncate or cap; %(unihan_stdout)s produces repr) |
unihan_stderr |
list[str] |
subprocess stderr lines (same caveats) |
Treat established keys as compatibility-sensitive — downstream users may build dashboards and alerts on them. Change deliberately.
snake_case, not dotted;unihan_prefix.- Prefer stable scalars; avoid ad-hoc objects.
- Heavy keys (
unihan_stdout,unihan_stderr) are DEBUG-only; consider companionunihan_stdout_lenfields or hard truncation (e.g.stdout[:100]).
logger.debug("msg %s", val), not f-strings. Two rationales:
- Deferred string interpolation: skipped entirely when the level is filtered.
- Aggregator message template grouping:
"Running %s"is one signature grouped ×10,000; f-strings make each line unique.
When computing val itself is expensive, guard with
if logger.isEnabledFor(logging.DEBUG).
Increment for each wrapper layer so %(filename)s:%(lineno)d and OTel
code.filepath point to the real caller. Verify whenever call depth
changes.
For objects with stable identity (Dataset, Reader, Exporter), use
LoggerAdapter to avoid repeating the same extra on every call. Lead with
the portable pattern (override process() to merge); merge_extra=True
simplifies this on Python 3.13+.
| Level | Use for | Examples |
|---|---|---|
DEBUG |
Internal mechanics, data I/O | Field parsing, record transformation steps |
INFO |
Data lifecycle, user-visible operations | Download completed, export finished, database bootstrapped |
WARNING |
Recoverable issues, deprecation, user-actionable config | Missing optional field, deprecated data format |
ERROR |
Failures that stop an operation | Download failed, parse error, database write failed |
Config discovery noise belongs in DEBUG; only surprising/user-actionable
config issues go to WARNING.
- Lowercase, past tense for events:
"download completed","parse error". - No trailing punctuation.
- Keep messages short; put details in
extra, not the message string.
- Use
logger.exception()only insideexceptblocks when you are not re-raising. - Use
logger.error(..., exc_info=True)when you need the traceback outside anexceptblock. - Avoid
logger.exception()followed byraise— this duplicates the traceback. Either add context viaextrathat would otherwise be lost, or let the exception propagate.
Assert on caplog.records attributes, not string matching on
caplog.text:
- Scope capture:
caplog.at_level(logging.DEBUG, logger="unihan_db.bootstrap"). - Filter records rather than index by position:
[r for r in caplog.records if hasattr(r, "unihan_field")]. - Assert on schema:
record.unihan_record_count == 100, not"100 records" in caplog.text. caplog.record_tuplescannot access extra fields — always usecaplog.records.
- f-strings/
.format()in log calls. - Unguarded logging in hot loops (guard with
isEnabledFor()). - Catch-log-reraise without adding new context.
print()for diagnostics.- Logging secret env var values (log key names only).
- Non-scalar ad-hoc objects in
extra. - Requiring custom
extrafields in format strings without safe defaults (missing keys raiseKeyError).