6.8 KiB
Logging-spec leaf-module granularity — “Route B” (decouple
logger-identity from console-display)
Follow-up notes recording the breaking-changes / costs of the deeper fix that would give the tractor.log logging-spec (see LogSpec/apply_logspec()) true per-leaf-MODULE level control — deliberately not taken (for now) in favour of the smaller sub-PACKAGE fix already landed.
Status / what already shipped
The cheap, contained fix is done: get_logger()’s “strip #2” (log.py, the pkg_path = subpkg_path collapse) no longer eats a real sub-package component. It now strips the trailing token only when it duplicates the caller’s leaf-module filename (which the header already shows via {filename}).
Result:
devx.debugresolves totractor.devx.debug, distinct from a baredevx->tractor.devx(its parent). So the logging-spec can dial sub-package levels at any nesting depth (devx.debug:runtime≠devx:cancel).- The
get_logger(__name__)cosmetic (“don’t repeat the leaf module in{name}since{filename}shows it”) is preserved.
What is still NOT addressable after that fix:
- Per-leaf-MODULE levels. Every module in a (sub-)pkg shares that pkg’s logger, because
get_logger()drops the leaf module-name from the logger key by design. - Top-level lib modules (eg.
tractor.to_asyncio,__package__ == 'tractor') emit on the roottractorlogger, so ato_asyncio:<lvl>spec entry hits a phantom child -> no-op.
What “Route B” is
Make the logger’s identity the full dotted module path (incl. the leaf module + top-level modules), eg. tractor.devx.debug._tty_lock and tractor.to_asyncio, and move the cosmetic leaf-trim out of logger-naming and into the formatter’s {name} rendering.
Net effect:
- Real per-module
Loggernodes exist in the hierarchy -> the spec can target ANY module; stdlib level-inheritance and propagation “just work” top-down. - Console headers stay clean because the formatter computes a trimmed display string (drop the trailing token that equals
{filename}’s stem) instead of the logger doing it.
Why it’s “broad” — breaking changes / costs
The logger name is currently load-bearing well beyond display; changing it ripples:
Every logger name changes. Today (post sub-pkg fix) names collapse to the sub-package; Route B = full module path. This touches:
- handler attachment points + the
getChild()hierarchy, - any
logging.getLogger('tractor.X')string lookups, - any name-based filtering,
- the dedup /
_strict_debugwarning logic insideget_logger()itself — thepkg_name in name,leaf_mod in pkg_path, “duplicate pkg-name” branches all key off the name shape and would need re-derivation.
- handler attachment points + the
Formatter rewrite.
LOG_FORMATuses{name}==record.name(the full logger name). To keep headers clean we must compute a display name and inject it as a record attr (eg.record.pkg_ns) via alogging.Filteror acolorlog.ColoredFormattersubclass overriding.format(), then pointLOG_FORMATat that field. The{filename}vs{name}de-dup intent has to be re-implemented per-record rather than per-logger.Propagation / double-emit surface grows. Full-depth loggers mean more intermediate nodes (
...debug._tty_lock->.debug->.devx->tractor). If more than one level carries a handler (spec sub-handlers- a root console), records double-emit. The
propagate=Falsetrick we already use for filter-targeted sub-loggers (apply_logspec()) must be applied carefully across a deeper tree — more levels == more places to leak a dup.
- a root console), records double-emit. The
Level-inheritance semantics shift. Today setting a level on
tractor.devxgates all devx emits (they share that logger). Post-Route-B,tractor.devx.debug._tty_lockis its ownNOTSETlogger that inherits the effective level from ancestors — functionally similar via inheritance, BUT any code that doeslog.setLevel(...)/ readslog.levelon a (previously collapsed) logger now only affects that exact node. AllsetLevel/.level =call sites need an audit (eg.get_logger()’s ownlog.level = rlog.levelline).Downstream contract churn.
modden/pikercallget_logger()/get_console_log()and may depend on current names — includingmodden.runtime.daemon.setup_tractor_logging()which asserts'tractor' not in nameon spec parts. The header{name}field is user-visible in everyone’s logs + CI output. Changing the canonical names is a public-ish behavior change -> needs a version note + downstream coordination (or a formatter trim that keeps the displayed string byte-identical to today).get_logger()refactor risk. The fn tangles two concerns: compute logger identity and compute the display string. Route B forces splitting them inside a ~300-line fn with multiple_strict_debugbranches, dup-warnings, and thename=__name__convenience. High chance of subtle regressions without an exhaustive name-derivation test matrix.
Migration / test plan (if pursued)
- Extract a pure helper
_mk_logger_name(pkg_name, mod_name, mod_pkg) -> (logger_name, display_name)and cover it with an exhaustive unit matrix: auto vs explicit vs__name__; package-__init__vs leaf module; nested vs flat;pkg_name in namevs not; top-level module (__package__ == pkg_name). - Switch
get_logger()to use it for identity; switch the formatter to usedisplay_name(via a record attr). - Re-run the full suite + golden-diff a sample of rendered log headers to confirm zero cosmetic churn.
- Coordinate the name change with
modden/piker; bump + CHANGES note.
Cheaper alternative — “Route A” (record-filter)
If per-leaf control is wanted before committing to Route B: keep names collapsed, add a logging.Filter on the configured handler keyed on record.module / record.pathname that maps each record’s source module -> its spec level. Set the base logger to the minimum level in the spec (so records aren’t pre-dropped by the logger), and let the filter discriminate up/down within that floor.
- Pros: no name churn, no formatter change, fully contained next to
apply_logspec(). - Cons: a filter can only discriminate within what the logger admits -> base must be permissive, so
at_least_level()expensive-work guards over-admit; matching dotted spec names to apathnameis fiddly; doesn’t clean up the hierarchy itself.
Recommendation
- Defer Route B unless true per-module loggers are wanted as a first-class feature.
- If per-leaf control is needed soon, prefer Route A (filter) — lower risk.
- The shipped sub-PACKAGE fix already covers the common ask (
devx.debugvsdevx).