Compare commits

...

2 Commits

Author SHA1 Message Date
Gud Boi 3a45dbd503 Hoist proc-title prefix to `_def_prefix` const
Make the sub-actor proc-title prefix a single
authoritative constant (`_proctitle._def_prefix`) so
the reap-recognition markers and `xontrib` banner pick
it up automatically — one place to flip the prefix
shape going fwd.

Deats,
- `_proctitle._def_prefix: str = '_subactor'`. New
  module-level const consumed by everything that needs
  to know the prefix.
- `set_actor_proctitle(actor, prefix=_def_prefix)`:
  takes an explicit `prefix` arg (default = the const)
  so callers can override per-spawn if they want.
- Default proc-title format:
  `'tractor[<reprol>]'` → `f'{prefix}[<reprol>]'`
  i.e. `_subactor[<reprol>]` by default.
- `_testing/_reap.py`: cmdline + comm markers source
  the prefix from `_proctitle._def_prefix` instead of
  the hardcoded `'tractor['`. So
  `_is_tractor_subactor()` tracks the const
  automatically.
- `xontrib/tractor_diag.xsh`: `acli.reap` orphan-mode
  banner now interpolates the
  `_TRACTOR_PROC_CMDLINE_MARKERS` tuple directly so
  the human-readable mode line stays in sync if the
  prefix shape changes again.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-06-01 20:26:22 -04:00
Gud Boi 7bd7dd50c7 Add `add_log_level()` factory + register `IO`=21
Follow-up to f595acc7 (`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 (see f595acc7).
- 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
2026-06-01 19:42:03 -04:00
5 changed files with 133 additions and 8 deletions

View File

@ -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?

View File

@ -90,7 +90,6 @@ keys are caller-defined).
''' '''
from __future__ import annotations from __future__ import annotations
import os import os
import pathlib import pathlib
import re import re
@ -99,6 +98,9 @@ import stat
import sys import sys
import time import time
from tractor.devx import _proctitle
# `/dev/shm` is the POSIX-shm filesystem on Linux + FreeBSD. # `/dev/shm` is the POSIX-shm filesystem on Linux + FreeBSD.
# macOS uses `shm_open` syscalls without a fs-visible path, # macOS uses `shm_open` syscalls without a fs-visible path,
# so the shm helpers refuse to run there. # so the shm helpers refuse to run there.
@ -230,9 +232,9 @@ def _read_comm(pid: int) -> str:
# while `cmdline` for zombies often reads as empty. # while `cmdline` for zombies often reads as empty.
_TRACTOR_PROC_CMDLINE_MARKERS: tuple[str, ...] = ( _TRACTOR_PROC_CMDLINE_MARKERS: tuple[str, ...] = (
'tractor._child', 'tractor._child',
'tractor[', _proctitle._def_prefix,
) )
_TRACTOR_PROC_COMM_MARKER: str = 'tractor[' _TRACTOR_PROC_COMM_MARKER: str = _proctitle._def_prefix
def _is_tractor_subactor(pid: int) -> bool: def _is_tractor_subactor(pid: int) -> bool:

View File

@ -52,7 +52,13 @@ except ImportError:
_stp = None _stp = None
def set_actor_proctitle(actor: 'Actor') -> str | None: _def_prefix: str = '_subactor'
def set_actor_proctitle(
actor: 'Actor',
prefix: str = _def_prefix,
) -> str | None:
''' '''
Set the calling process's proc-title to identify it as a Set the calling process's proc-title to identify it as a
tractor sub-actor. tractor sub-actor.
@ -69,6 +75,6 @@ def set_actor_proctitle(actor: 'Actor') -> str | None:
if _stp is None: if _stp is None:
return None return None
title: str = f'tractor[{actor.aid.reprol()}]' title: str = f'{prefix}[{actor.aid.reprol()}]'
_stp.setproctitle(title) _stp.setproctitle(title)
return title return title

View File

@ -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

View File

@ -488,6 +488,7 @@ def _tractor_reap(args):
reap, reap,
reap_shm, reap_shm,
reap_uds, reap_uds,
_TRACTOR_PROC_CMDLINE_MARKERS,
) )
rc: int = 0 rc: int = 0
@ -500,9 +501,8 @@ def _tractor_reap(args):
else: else:
pids = find_orphans() pids = find_orphans()
mode = ( mode = (
'orphans (PPid==1, intrinsic ' f'orphans (PPid==1, intrinsic '
'cmdline/comm match — `tractor[…]` or ' f'cmdline/comm match — {_TRACTOR_PROC_CMDLINE_MARKERS}'
'`tractor._child`)'
) )
if not pids: if not pids: