tractor/ai/tooling-todos/logspec_leaf_module_granula...

6.8 KiB
Raw Blame History

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 callers leaf-module filename (which the header already shows via {filename}).

Result:

  • devx.debug resolves to tractor.devx.debug, distinct from a bare devx -> tractor.devx (its parent). So the logging-spec can dial sub-package levels at any nesting depth (devx.debug:runtimedevx:cancel).
  • The get_logger(__name__) cosmetic (“dont 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 pkgs 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 root tractor logger, so a to_asyncio:<lvl> spec entry hits a phantom child -> no-op.

What “Route B” is

Make the loggers 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 formatters {name} rendering.

Net effect:

  • Real per-module Logger nodes 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 its “broad” — breaking changes / costs

The logger name is currently load-bearing well beyond display; changing it ripples:

  1. 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_debug warning logic inside get_logger() itself — the pkg_name in name, leaf_mod in pkg_path, “duplicate pkg-name” branches all key off the name shape and would need re-derivation.
  2. Formatter rewrite. LOG_FORMAT uses {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 a logging.Filter or a colorlog.ColoredFormatter subclass overriding .format(), then point LOG_FORMAT at that field. The {filename} vs {name} de-dup intent has to be re-implemented per-record rather than per-logger.

  3. 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=False trick 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.
  4. Level-inheritance semantics shift. Today setting a level on tractor.devx gates all devx emits (they share that logger). Post-Route-B, tractor.devx.debug._tty_lock is its own NOTSET logger that inherits the effective level from ancestors — functionally similar via inheritance, BUT any code that does log.setLevel(...) / reads log.level on a (previously collapsed) logger now only affects that exact node. All setLevel/.level = call sites need an audit (eg. get_logger()s own log.level = rlog.level line).

  5. Downstream contract churn. modden / piker call get_logger() / get_console_log() and may depend on current names — including modden.runtime.daemon.setup_tractor_logging() which asserts 'tractor' not in name on spec parts. The header {name} field is user-visible in everyones 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).

  6. 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_debug branches, dup-warnings, and the name=__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 name vs not; top-level module (__package__ == pkg_name).
  • Switch get_logger() to use it for identity; switch the formatter to use display_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 records source module -> its spec level. Set the base logger to the minimum level in the spec (so records arent 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 a pathname is fiddly; doesnt 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.debug vs devx).