Fix `add_log_level()` re-registration value drift

The bound `StackLevelAdapter.<name>` emit method captured its
level via a `_level: int = value` default-arg at bind time, so
a later `add_log_level(name, <new-value>)` re-registration left
the method emitting at the *stale* original level. Read
`CUSTOM_LEVELS[name_up]` at call time instead — the method binds
once but always tracks the current registered value.

Also,
- correct the `add_log_level()` docstring's "Idempotent" note
  to describe the call-time value lookup (was the misleading
  "no-op-ish refresh (won't clobber an already-bound method)").
- sync stale `_reap.py` doc/comment markers `tractor[` ->
  `_subactor[` to match the actual `_proctitle._def_prefix`
  proc-title prefix (doc-only drift; the code markers already
  referenced `_proctitle._def_prefix`).

Review: PR #467 (copilot-pull-request-reviewer)
https://github.com/goodboy/tractor/pull/467#pullrequestreview-4562945515

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
custom_log_levels_api
Gud Boi 2026-06-24 18:03:28 -04:00
parent 3727ce9e6f
commit 88cc68edd3
2 changed files with 20 additions and 15 deletions

View File

@ -216,18 +216,18 @@ def _read_comm(pid: int) -> str:
# regardless of cwd / venv path / launch context. Used by
# `_is_tractor_subactor()` below.
#
# - cmdline `tractor[`: matches the `setproctitle`-set form
# (`tractor[<aid.reprol()>]`) — set in
# `_actor_child_main` for ALL backends, mutates argv via
# libc so visible in `/proc/<pid>/cmdline`.
# - cmdline `_subactor[` (`_proctitle._def_prefix`): matches
# the `setproctitle`-set form (`_subactor[<aid.reprol()>]`)
# — set in `_actor_child_main` for ALL backends, mutates
# argv via libc so visible in `/proc/<pid>/cmdline`.
# - cmdline `tractor._child`: matches the legacy
# `python -m tractor._child --uid (...)` form. Catches
# procs that died before `_actor_child_main` got to call
# `setproctitle()` — argv from exec is still kernel-
# visible at that point.
# - comm `tractor[`: same proctitle-set form, but visible
# - comm `_subactor[`: same proctitle-set form, but visible
# via `/proc/<pid>/comm` (kernel-truncated to ~15 bytes,
# `tractor[doggy:`). Critical for ZOMBIES — kernel
# `_subactor[doggy:`). Critical for ZOMBIES — kernel
# preserves `comm` past task-exit until parent reaps,
# while `cmdline` for zombies often reads as empty.
_TRACTOR_PROC_CMDLINE_MARKERS: tuple[str, ...] = (
@ -253,7 +253,7 @@ def _is_tractor_subactor(pid: int) -> bool:
'''
# 1. cmdline match — catches both `setproctitle`-set
# `tractor[<aid>]` (live) AND legacy `python -m
# `_subactor[<aid>]` (live) AND legacy `python -m
# tractor._child` (any) form.
cmdline: str = _read_cmdline(pid)
if any(m in cmdline for m in _TRACTOR_PROC_CMDLINE_MARKERS):

View File

@ -281,8 +281,11 @@ def add_log_level(
`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).
Idempotent: re-registering an existing name refreshes its
value + color in place. The bound `StackLevelAdapter.<name>`
method reads the current `CUSTOM_LEVELS` value at call time, so
it tracks the new level across re-registration (the method
object is bound once, never re-bound/clobbered).
'''
name_up: str = name.upper()
@ -294,16 +297,18 @@ def add_log_level(
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.
# read the level from `CUSTOM_LEVELS[name_up]` at CALL time
# (NOT captured at bind time) so a later
# `add_log_level(name, <new-value>)` re-registration stays
# consistent with this once-bound method. The closure binds
# *this* call's `name_up` local (stable, never reassigned),
# so there's no late-binding hazard. 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)
return self.log(CUSTOM_LEVELS[name_up], msg)
_emit.__name__ = name_lo
_emit.__qualname__ = f'StackLevelAdapter.{name_lo}'