Instruction file imported from prakosd/rag-playground (
.github/instructions/log4py.instructions.md). Copyright stays with the author.
log4py
The lowest layer of rag-playground: a tiny logging toolkit that every other
package (artifact_store, crawl4md, vector_indexer, rag_engine) and the
Streamlit app may import. It exists so libraries only emit records while the
application decides threshold, format, and destinations in one place.
Constraints
- Zero dependencies, pure stdlib.
log4pymust not import any project package or any third-party library — standard library only. It is the base layer; nothing may make it depend upward. A boundary test enforces this. - Libraries stay silent by default.
get_logger(name)returns a standardlogging.Loggerand attaches a singleNullHandlerto the record's top-level package logger, so importing a library produces no output until an app callsconfigure_logging. Never calllogging.basicConfig, add output handlers, or set levels from a library. - Single format.
LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s %(message)s"andDATE_FORMAT = "%Y-%m-%d %H:%M:%S"are the one source of truth. Do not add per-call format overrides. - Threshold model.
configure_logging(level=...)sets one minimum level (DEBUG < INFO < WARNING < ERROR;"WARN"is accepted as an alias via stdlibgetLevelName). Records below it are dropped. - Idempotent (Streamlit reruns).
configure_loggingmust be safe to call repeatedly. It first detaches (and closes file handlers of) any handler a prior call installed — tracked by the_log4py_managedmarker attribute and the_configured_logger_namesset — then attaches fresh handlers, so a rerun replaces rather than stacks handlers. Any new handler type that owns OS resources must be closed in_detach_managed_handlers. - Target selection. A non-empty
logger_namesisolates the named top-level loggers from third-party noise and stops propagation below them; an empty selection configures the root logger (captures everything). Keep this branch intact. - Per-context file routing.
log_file_router(aCallable[[], Path | None]) takes precedence over the fixedlog_file. Its_RoutingFileHandlerpicks a destination per record: it calls the router, skips the file when it returnsNone, and otherwise appends via a per-path cachedRotatingFileHandler(parents created, rotation applied). Router exceptions must be swallowed so a logging call never raises into caller code;close()must close every cached child handler. Callers set the routing context (e.g. acontextvars.ContextVar) and, because worker threads do not inherit contextvars, must set it inside each thread.
Tests
- Live in
tests/test_log4py.pyandtests/test_log4py_boundary.py(run bypython -m pytest tests/ -q, linted byruff check src/ tests/). - Restore logging state between tests (detach managed handlers, reset
propagate) socaplogand other suites are unaffected. - Cover:
get_loggerattaches exactly oneNullHandler;configure_loggingsets level + handlers; idempotency (no duplicate handlers after repeated calls); routing to per-path files andNone-skip; the zero-dependency boundary. - Use
tmp_pathfor any file assertions — never write outside the fixture.