Logging Conventions

appenv uses stdlib logging — no structlog, no stogger, no third-party logging libraries. This is a hard constraint of the zero-dependency bootstrap model. Every pattern below works with logging.Logger, %-formatting, and the handlers configured by setup_logging().

Log Levels

Level

Purpose

Example

DEBUG

Internal diagnostics: discovery chains, fallback paths, binary probes

log.debug("uv binary: %s", uv.bin)

INFO

Operational milestones: init, venv creation, migrations, exec calls

log.info("creating-venv: python=%s path=%s", ...)

WARNING

Degraded state the user should know about but that appenv handled

log.warning("corrupted-venv: venv=%s ...", ...)

ERROR

Fatal problems preceding sys.exit()

log.error("pyproject-not-found: path=%s", ...)

Dual-Channel Error Reporting

Every error exit uses both print() and log.error():

  • print() reaches the operator on the console right now

  • log.error() persists to .appenv/logs/<command>.log for post-mortem

log.error("pyproject-not-found: path=%s", pyproject.path)
print(f"Error: No pyproject config file at {pyproject.path} found.")
print("appenv must be in the project root (next to pyproject.toml).")
sys.exit(EXIT_CODE_NOINPUT)

Every sys.exit() path must have a preceding log call. The log file must contain enough context to reconstruct what happened without the console output.

Message Format

All log messages use a topic prefix followed by context key-value pairs:

<topic>: key=value key=value

The topic identifies the event class (e.g., binary-not-found, uv-version-invalid, venv-health-check-failed). The key-value pairs carry the specific identifiers — paths, versions, commands — that make the message actionable.

Bad:

venv-health-check-failed: FileNotFoundError

Good:

venv-health-check-failed: venv=/path python=/path/bin/python error=FileNotFoundError

Exception Handling

Use log.exception() inside except blocks — it attaches the traceback automatically. Never use log.error() when an active exception exists:

except (OSError, subprocess.CalledProcessError) as e:
    log.exception("subprocess-failed: %s exit_code=%d output=%s", c, e.returncode, output)

For handled exceptions where the traceback is not useful (expected fallback paths), use log.debug() with the exception as context:

except (OSError, subprocess.CalledProcessError):
    log.debug("python3 version check failed, skipping bare python3 fallback")

Operational Milestones

Log at INFO when the operation reaches a point an operator would need during triage:

  • venv creation: log.info("creating-venv: python=%s path=%s", ...)

  • lockfile updates: log.info("updating-lockfile: path=%s", ...)

  • migrations: log.info("migrate-completed: base=%s", ...)

  • script creation: log.info("creating-appenv-script: path=%s", ...)

The log file should read as a chronological narrative of what happened during a run.

Pre-Execution Logging

Before any os.execv() call, log the binary path and full argv. If the exec fails or the system crashes, the operator knows what was attempted:

log.info("exec-command: binary=%s argv=%s", cmd_path, argv)
os.execv(str(cmd_path), argv)

What Not to Log

  • CLI user outputprint() calls for progress messages, help text, and version info are user-facing and do not need matching log calls

  • Normal flow trivia — every function entry/exit, every variable assignment. Reserve DEBUG for fallback paths and boundary transitions

Verbose Mode

APPENV_VERBOSE=1 adds a console handler with dimmed caller info (funcName:lineno). This is a development tool — not for production use.