Add `add_log_level()` factory + register `IO`=21
Follow-up tocustom_log_levelsf595acc7(`supervise_run_process`) which called `log.io(...)` for std-stream relay assuming an `IO=21` level existed. Add the registration via a new factory + tests covering both the factory and the new level. `add_log_level()` factory, - One call wires the four (otherwise hand-synced) pieces: - `CUSTOM_LEVELS[NAME]` — drives the `stacklevel` bump in `StackLevelAdapter.log()` + `get_logger()`'s per-level audit. - `logging.addLevelName()` — stdlib name registration. - `STD_PALETTE[NAME]` + `BOLD_PALETTE['bold'][NAME]` — color entries consumed by `get_console_log()`'s `ColoredFormatter` build. - Same-named (lowercase) emit method bound on `StackLevelAdapter` so `log.<name>('msg')` works + `get_logger()`'s per-level method audit passes. - Idempotent: re-registering an existing name is a no-op-ish refresh that won't clobber an already-bound method. - Method binding uses a default-arg `_level=value` so the level int is captured (not late-bound across multiple registrations). `IO=21` level (first user), - Purple. Used by `tractor.trionics._subproc`'s std-stream relay (seef595acc7). - Value 21 picked to sit just ABOVE stdlib `INFO`=20 so it's SHOWN BY DEFAULT at usual `info`/`devx` console levels — a `runtime`=15 relay would be silently filtered (footgun for daemon supervisors whose whole point is visibility). Still distinctly labeled + filterable. Tests (`tests/test_log_sys.py`), - `test_io_custom_level_registered`: validates the IO level is fully wired (`CUSTOM_LEVELS`, `addLevelName`, both palettes, `StackLevelAdapter.io()` callable); emits a record + sanity-asserts `21 >= INFO(20)`. - `test_add_log_level_pluggable`: registers a fresh `XLVL=19` (cyan) via `add_log_level()`, asserts all four wires + the bound `xlog.xlvl()` emit, then try/finally cleans up the module-global mutations so later `get_logger()` audits don't trip on a half-removed level. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
parent
f595acc76c
commit
7bd7dd50c7
|
|
@ -162,6 +162,66 @@ def test_implicit_mod_name_applied_for_child(
|
||||||
assert submod.log.logger in sub_logs
|
assert submod.log.logger in sub_logs
|
||||||
|
|
||||||
|
|
||||||
|
def test_io_custom_level_registered():
|
||||||
|
'''
|
||||||
|
The `IO`(21) level (registered via `add_log_level()` at
|
||||||
|
import, for `tractor.trionics._subproc`'s std-stream relay)
|
||||||
|
is fully wired and SHOWN BY DEFAULT at `info`-level consoles
|
||||||
|
since `21 >= INFO(20)`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
import logging
|
||||||
|
assert log.CUSTOM_LEVELS.get('IO') == 21
|
||||||
|
assert logging.getLevelName(21) == 'IO'
|
||||||
|
assert log.STD_PALETTE.get('IO')
|
||||||
|
assert log.BOLD_PALETTE['bold'].get('IO')
|
||||||
|
|
||||||
|
iolog = log.get_logger('io_lvl_test')
|
||||||
|
assert callable(getattr(iolog, 'io', None))
|
||||||
|
# emit must not raise
|
||||||
|
iolog.io('hello from the IO level')
|
||||||
|
|
||||||
|
# 21 >= INFO(20) -> shown when console set to `info`
|
||||||
|
assert 21 >= logging.INFO
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_log_level_pluggable():
|
||||||
|
'''
|
||||||
|
`add_log_level()` is the single pluggable entry-point: one
|
||||||
|
call wires `CUSTOM_LEVELS` + `addLevelName` + both palettes +
|
||||||
|
a same-named `StackLevelAdapter` emit method (so
|
||||||
|
`get_logger()`'s per-level audit passes).
|
||||||
|
|
||||||
|
'''
|
||||||
|
import logging
|
||||||
|
name: str = 'XLVL'
|
||||||
|
val: int = 19
|
||||||
|
try:
|
||||||
|
log.add_log_level(name, val, 'cyan')
|
||||||
|
|
||||||
|
assert log.CUSTOM_LEVELS[name] == val
|
||||||
|
assert logging.getLevelName(val) == name
|
||||||
|
assert log.STD_PALETTE[name] == 'cyan'
|
||||||
|
assert log.BOLD_PALETTE['bold'][name] == 'bold_cyan'
|
||||||
|
|
||||||
|
# the audit in `get_logger()` (asserts a method per
|
||||||
|
# `CUSTOM_LEVELS` entry) must still pass.
|
||||||
|
xlog = log.get_logger('xlvl_test')
|
||||||
|
emit = getattr(xlog, name.lower(), None)
|
||||||
|
assert callable(emit)
|
||||||
|
emit('hello from a plugged-in level')
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# best-effort cleanup of our module-global mutations so
|
||||||
|
# later `get_logger()` audits don't see a half-removed
|
||||||
|
# level.
|
||||||
|
log.CUSTOM_LEVELS.pop(name, None)
|
||||||
|
log.STD_PALETTE.pop(name, None)
|
||||||
|
log.BOLD_PALETTE['bold'].pop(name, None)
|
||||||
|
if hasattr(log.StackLevelAdapter, name.lower()):
|
||||||
|
delattr(log.StackLevelAdapter, name.lower())
|
||||||
|
|
||||||
|
|
||||||
# TODO, moar tests against existing feats:
|
# TODO, moar tests against existing feats:
|
||||||
# ------ - ------
|
# ------ - ------
|
||||||
# - [ ] color settings?
|
# - [ ] color settings?
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,63 @@ class StackLevelAdapter(LoggerAdapter):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_log_level(
|
||||||
|
name: str,
|
||||||
|
value: int,
|
||||||
|
color: str = 'white',
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Register a new custom log level with `tractor`'s logging
|
||||||
|
machinery in ONE call — the single pluggable entry-point that
|
||||||
|
keeps the (otherwise hand-synced) pieces consistent:
|
||||||
|
|
||||||
|
- `CUSTOM_LEVELS[name]` (drives the `stacklevel` bump in
|
||||||
|
`StackLevelAdapter.log()` + the `get_logger()` audit).
|
||||||
|
- `logging.addLevelName()` registration.
|
||||||
|
- `STD_PALETTE`/`BOLD_PALETTE` color entries (consumed when
|
||||||
|
`get_console_log()` builds its `ColoredFormatter`).
|
||||||
|
- a same-named (lowercase) emit method bound on
|
||||||
|
`StackLevelAdapter` so `log.<name>('msg')` works (and so
|
||||||
|
`get_logger()`'s per-level method audit passes).
|
||||||
|
|
||||||
|
Idempotent: re-registering an existing name is a no-op-ish
|
||||||
|
refresh (won't clobber an already-bound method).
|
||||||
|
|
||||||
|
'''
|
||||||
|
name_up: str = name.upper()
|
||||||
|
name_lo: str = name.lower()
|
||||||
|
|
||||||
|
CUSTOM_LEVELS[name_up] = value
|
||||||
|
logging.addLevelName(value, name_up)
|
||||||
|
STD_PALETTE[name_up] = color
|
||||||
|
BOLD_PALETTE['bold'][name_up] = f'bold_{color}'
|
||||||
|
|
||||||
|
if not hasattr(StackLevelAdapter, name_lo):
|
||||||
|
# bind via default-arg so `value` is captured (not
|
||||||
|
# late-bound); delegates to `.log()` exactly like the
|
||||||
|
# hand-written level methods above.
|
||||||
|
def _emit(
|
||||||
|
self,
|
||||||
|
msg: str,
|
||||||
|
*,
|
||||||
|
_level: int = value,
|
||||||
|
) -> None:
|
||||||
|
return self.log(_level, msg)
|
||||||
|
|
||||||
|
_emit.__name__ = name_lo
|
||||||
|
_emit.__qualname__ = f'StackLevelAdapter.{name_lo}'
|
||||||
|
setattr(StackLevelAdapter, name_lo, _emit)
|
||||||
|
|
||||||
|
|
||||||
|
# `IO`: child-subproc std-stream relay (see
|
||||||
|
# `tractor.trionics._subproc`). Value 21 sits just ABOVE
|
||||||
|
# `INFO`(20) so it's SHOWN BY DEFAULT at the usual `info`/`devx`
|
||||||
|
# console levels (a `runtime`(15) relay would be silently
|
||||||
|
# filtered) yet still distinctly labelled/colored + separately
|
||||||
|
# filterable.
|
||||||
|
add_log_level('IO', 21, 'purple')
|
||||||
|
|
||||||
|
|
||||||
# TODO IDEAs:
|
# TODO IDEAs:
|
||||||
# -[ ] move to `.devx.pformat`?
|
# -[ ] move to `.devx.pformat`?
|
||||||
# -[ ] do per task-name and actor-name color coding
|
# -[ ] do per task-name and actor-name color coding
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue