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 |
|---|---|---|
|
Internal diagnostics: discovery chains, fallback paths, binary probes |
|
|
Operational milestones: init, venv creation, migrations, exec calls |
|
|
Degraded state the user should know about but that appenv handled |
|
|
Fatal problems preceding |
|
Dual-Channel Error Reporting¶
Every error exit uses both print() and log.error():
print()reaches the operator on the console right nowlog.error()persists to.appenv/logs/<command>.logfor 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 output —
print()calls for progress messages, help text, and version info are user-facing and do not need matching log callsNormal 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.