Skip to content

py(logging): Add structured logging with extra context across all modules#1017

Open
tony wants to merge 28 commits intomasterfrom
logging
Open

py(logging): Add structured logging with extra context across all modules#1017
tony wants to merge 28 commits intomasterfrom
logging

Conversation

@tony
Copy link
Member

@tony tony commented Mar 7, 2026

Summary

  • Add logging.getLogger(__name__) declarations to all 36 source modules for consistent logger hierarchy
  • Add NullHandler in library __init__.py to follow Python library logging best practice
  • Add TmuxpLoggerAdapter for persistent extra context (session/window/pane) without repeating kwargs
  • Replace print() calls with structured tmuxp_echo() or logger.*() in util.py, cli/load.py, and finders.py
  • Add structured extra keys (tmux_session, tmux_window, tmux_pane, tmux_config_path) to log calls across workspace builder, finders, freezer, importers, loader, and validation
  • Fix setup_logger() to default to the "tmuxp" logger (not root) and skip NullHandler when checking for existing handlers
  • Add stacklevel=2 to tmuxp_echo log calls so source locations point to callers
  • Remove catch-log-reraise anti-pattern in plugin version check
  • Add comprehensive caplog-based tests asserting on extra attributes across all instrumented modules

Changes by area

Core logging (src/tmuxp/log.py)

  • TmuxpLoggerAdapter: New LoggerAdapter subclass that merges extra dicts portably (Python < 3.13 compatible)
  • setup_logger(): Defaults to "tmuxp" logger, ignores NullHandler when deciding whether to add StreamHandler, selects DebugLogFormatter at DEBUG level
  • tmuxp_echo(): Adds stacklevel=2 for accurate caller attribution

Workspace modules

  • builder.py: Session/window/pane lifecycle logging via TmuxpLoggerAdapter with appropriate extra keys
  • finders.py: Structured DEBUG for local file search, WARNING for ambiguous multi-file directories (replaces raw tmuxp_echo with logger.warning + user-facing echo)
  • freezer.py: DEBUG logging for freeze session/window operations
  • importers.py: DEBUG logging for tmuxinator/teamocil import with session name context
  • loader.py: DEBUG logging for expand/trickle operations
  • validation.py: DEBUG logging for schema validation

CLI and utilities

  • cli/__init__.py: Fix setup_logger call (remove explicit logger arg, use default)
  • cli/load.py: Add missing log_level to CLILoadNamespace, use DebugLogFormatter for file handler at DEBUG level, replace print() with tmuxp_echo()
  • util.py: Replace print() in oh_my_zsh_auto_title with tmuxp_echo(log_level="WARNING"), replace print(e) in get_pane with structured logger.debug + proper re-raise

Module stubs (30 files)

  • Every module gets import logging + logger = logging.getLogger(__name__) for future instrumentation

Design decisions

  • Portable LoggerAdapter over merge_extra=True: Python 3.13 added merge_extra to LoggerAdapter.__init__, but tmuxp supports 3.10+, so TmuxpLoggerAdapter.process() manually merges — the override is forward-compatible
  • setup_logger defaults to "tmuxp" not root: Prevents tmuxp's handler setup from affecting unrelated libraries when used as an application entry point
  • Separate logger.warning + tmuxp_echo for ambiguity: The structured log record goes to aggregators; the tmuxp_echo output goes to the terminal for user visibility

Verification

Verify no print() calls remain in util.py (outside of # NOQA exemptions):

$ rg 'print\(' src/tmuxp/util.py

Verify all source modules have logger declarations:

$ rg -L 'logger = logging.getLogger' src/tmuxp/ --glob '*.py' --files-without-match

Verify no f-strings in log calls:

$ rg 'logger\.\w+\(f"' src/tmuxp/

Test plan

  • test_builder_logs_session_created — verifies INFO record with tmux_session extra
  • test_builder_logs_window_and_pane_creation — verifies DEBUG records with tmux_window/tmux_pane extra and command logging
  • test_expand_logs_debug — verifies expand() DEBUG with tmux_session
  • test_trickle_logs_debug — verifies trickle() DEBUG with tmux_session
  • test_validate_schema_logs_debug — verifies validation DEBUG with tmux_session
  • test_find_workspace_file_logs_warning_on_multiple — verifies WARNING on ambiguous workspace dirs
  • test_find_local_workspace_files_logs_debug — verifies DEBUG with tmux_config_path
  • test_freeze_logs_debug — verifies freeze session/window DEBUG records
  • test_import_teamocil_logs_debug — verifies teamocil import DEBUG with session name
  • test_import_tmuxinator_logs_debug — verifies tmuxinator import DEBUG with session name
  • test_plugin_version_check_logs_debug — verifies plugin version check DEBUG
  • test_get_pane_logs_debug_on_failure — verifies structured DEBUG on pane lookup failure
  • test_oh_my_zsh_auto_title_logs_warning — verifies WARNING when DISABLE_AUTO_TITLE unset
  • uv run py.test — full test suite passes
  • uv run ruff check . — no lint issues
  • uv run mypy — no type errors

tony added 28 commits March 7, 2026 10:27
why: Logging standards require `logging.getLogger(__name__)` in every
module for consistent, hierarchical log propagation.
what:
- Add `import logging` and `logger = logging.getLogger(__name__)` to 27
  modules that lacked them
- Covers cli/, workspace/, _internal/, and top-level modules
- No behavioral changes; loggers are declared but unused until
  subsequent commits add log calls
…gger

why: Library __init__.py needs NullHandler per logging best practices.
setup_logger() targeted root logger instead of tmuxp, and its handler
guard failed when NullHandler was present.
what:
- Add NullHandler to tmuxp root __init__.py
- Add TmuxpLoggerAdapter with process() override for Python <3.13
  portable extra-merging
- Fix setup_logger() to target "tmuxp" logger by default
- Check for non-NullHandler handlers before adding StreamHandler
- Always call setLevel() regardless of existing handlers
why: CLI was passing a child logger to setup_logger instead of using
the tmuxp root default. Log-file handler hardcoded INFO level,
ignoring user's --log-level flag.
what:
- Simplify setup_logger() call to use default tmuxp root logger
- Use DebugLogFormatter for log-file when log_level is DEBUG
- Remove hardcoded setLevel(INFO) on tmuxp logger in log-file setup
- Add logger declaration to cli/load.py
why: print() calls in library code violate logging standards. The
get_pane() catch-reraise lost context, and oh_my_zsh_auto_title()
had no machine-readable signal.
what:
- Replace print(e) in get_pane() with logger.debug() using exc_info
  and tmux_pane extra; raise PaneNotFound explicitly
- Add logger.warning() in oh_my_zsh_auto_title() before the
  user-facing print block
why: Workspace build lifecycle had no logging, making debugging
session creation, window/pane setup, and config resolution opaque.
what:
- builder.py: Add TmuxpLoggerAdapter with tmux_session/window/pane
  extra for session created, workspace built, window/pane created,
  before-script, and send-command events
- finders.py: Replace tmuxp_echo() with logger.warning() for
  multiple-workspace-file detection; add DEBUG log for resolution
- loader.py: Add DEBUG logs for expand() and trickle() entry
- shell.py: Add DEBUG log for detected shell in detect_best_shell()
why: Log calls need test coverage to prevent regressions and verify
structured extra fields are emitted correctly.
what:
- test_util: Assert get_pane() emits DEBUG with tmux_pane extra on
  failure; assert oh_my_zsh_auto_title() emits WARNING
- test_builder: Assert build() emits INFO with tmux_session extra;
  assert window/pane creation emits DEBUG with tmux_window/tmux_pane
- test_finder: Assert find_workspace_file() emits WARNING when
  multiple workspace files found in same directory
…y modules

why: 5 library modules had logger stubs but zero log calls, leaving gaps
in observability for config loading, session freezing, imports, validation,
and plugin version checks.
what:
- Add DEBUG logs to freezer.freeze() (per-session + per-window) with tmux_session/tmux_window extra
- Add DEBUG logs to importers.import_tmuxinator() and import_teamocil()
- Add DEBUG log to validation.validate_schema()
- Add DEBUG log to config_reader._from_file() with tmux_config_path extra
- Add DEBUG + WARNING logs to plugin._version_check() with plugin name context
why: Existing log calls lacked structured extra for filtering/searching,
finders emitted two verbose warnings for one event, and builder logged
commands before execution (factually incorrect on failure).
what:
- Add tmux_session extra to loader.expand() and trickle() DEBUG calls
- Consolidate finders.py dual WARNING into single message with tmux_config_path extra
- Move builder command log after send_keys and change to past tense "sent command"
- Add tmux_config_path extra to builder "before script failed" catch-log-reraise
why: tmuxp_echo wraps _echo_logger.log(), so DebugLogFormatter showed
log.py as the source file instead of the actual CLI caller.
what:
- Add stacklevel=2 to both _echo_logger.log() calls in tmuxp_echo()
why: Two lifecycle events (session created detached, workspace saved) used
print() only, lacking observability via the logging system.
what:
- Replace cli/load.py print() with tmuxp_echo() for detached session message
- Add logger.info() with tmux_config_path extra after freeze file write
why: _compat.py was the only file in the codebase missing the standard
from __future__ import annotations that all other modules have.
what:
- Add from __future__ import annotations at top of _compat.py
why: New and modified log calls need test coverage to prevent regressions
and verify structured extra fields are emitted correctly.
what:
- Add test_expand_logs_debug and test_trickle_logs_debug (test_config.py)
- Add test_freeze_logs_debug with tmux_session and tmux_window assertions (test_freezer.py)
- Add test_import_teamocil_logs_debug (test_import_teamocil.py)
- Add test_import_tmuxinator_logs_debug (test_import_tmuxinator.py)
- Add test_plugin_version_check_logs_debug and warning test (test_plugin.py)
- Update test_builder assertion from "sending command" to "sent command"
- Update test_finder assertion for consolidated warning message
why: Enable dashboard filtering and caplog schema assertions for
importer debug logs.
what:
- Add tmux_session extra to import_tmuxinator log call
- Add tmux_session extra to import_teamocil log call via _inner peek
- Add tmux_session assertion to both importer caplog tests
why: Enable dashboard filtering on tmux_session for schema validation.
what:
- Add tmux_session extra to validate_schema debug log
- Guard with isinstance for non-dict inputs (param is t.Any)
why: Enable dashboard filtering on tmux_config_path for workspace discovery.
what:
- Add tmux_config_path extra to find_local_workspace_files debug log
…mpat

why: LoggerAdapter became subscriptable at runtime only in Python 3.11.
Base class expressions are always evaluated at runtime regardless of
`from __future__ import annotations`, so this crashes on 3.10.
what:
- Change `LoggerAdapter[logging.Logger]` to `LoggerAdapter`
- Add `# type: ignore[type-arg]` for mypy compatibility
why: After logging migration, the ambiguity warning only logged via
logger.warning() with no user-visible output. Users would silently get
the first file picked without knowing alternatives exist.
what:
- Add tmuxp_echo() after logger.warning() to show user-facing message
- Fix extra key: use workspace_file directly (already a directory path)
- Add tmuxp_echo import
why: before_script is a shell script path, not a workspace config path.
Using tmux_config_path for it violates the semantic contract of the key.
The adapter already provides tmux_session context.
what:
- Remove extra={"tmux_config_path": ...} from before_script log calls
why: logger.warning() immediately before raise duplicates the exception
info. The TmuxpPluginException already contains the incompatibility
details. Per logging standards, avoid catch-log-reraise without adding
new context.
what:
- Remove logger.warning() call before raise in _version_check()
why: args.log_level is accessed at line 597 but was not declared in
the typed Namespace class, causing type checking gaps.
what:
- Add log_level: str to CLILoadNamespace
…pter

why: TmuxpLoggerAdapter already has tmux_session and tmux_window in its
defaults. Passing the same keys in per-call extra is redundant since the
adapter's process() method merges them.
what:
- Remove extra={"tmux_session": ...} from "session created" log call
- Remove extra={"tmux_window": ...} from "window created" log call
why: print() bypasses the logging system. tmuxp_echo() provides both
logging and print output through a unified channel.
what:
- Replace logger.warning() + print() with single tmuxp_echo() call
- Add tmuxp_echo import
why: No tests existed for validate_schema or find_local_workspace_files
logging, leaving the structured extra fields unverified.
what:
- Add test_validate_schema_logs_debug asserting tmux_session extra
- Add test_find_local_workspace_files_logs_debug asserting tmux_config_path
why: stdlib imports (logging, sys) were split by the logger = ... line,
violating import grouping conventions.
what:
- Group stdlib imports together before logger assignment
- Remove stale flake8: NOQA comment
why: test_plugin_version_check_logs_warning_on_fail tested the warning
log that was removed in the catch-log-reraise fix. The version check
failure is already tested via exception assertions in other tests.
what:
- Remove test_plugin_version_check_logs_warning_on_fail
why: tmuxp_echo was both logging (stderr) and printing (stdout),
causing duplicate output. Separating these channels fixes the
regression and follows the print-for-users, logger-for-machines pattern.
what:
- Remove LOG_LEVELS dict, _echo_logger, unstyle import from log.py
- Simplify tmuxp_echo to pure print() wrapper (no log_level/style_log)
- Add explicit logger.*() calls at all operational call sites
- Add logger.info for workspace loading (supports --log-file)
- Fix test assertion to match lowercase log message convention
…ings

why: get_pane() caught exc.TmuxpException but window.panes.get() raises
ObjectDoesNotExist (not a TmuxpException subclass), making the handler
unreachable. get_session() and get_window() both catch Exception.
what:
- Change except exc.TmuxpException to except Exception in get_pane()
- Update test to raise Exception instead of TmuxpException
…uity warning

why: The logging branch replaced styled output with a plain tmuxp_echo
call, losing the red error styling and detailed guidance text that
helps users resolve multiple .tmuxp.{yaml,yml,json} files.
what:
- Replace tmuxp_echo with print() using Colors.error() for red styling
- Add guidance text about using distinct file names
- Replace tmuxp.log import with tmuxp._internal.colors import
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant