GRM-13: feat: replace print() with stdlib logging module

- Create logging_config.py with get_logger() providing dual handlers:
  - Console handler (stderr) controlled by GRM_LOG_LEVEL env var (default INFO)
  - File handler (~/.local/state/grm/logs/grm.log) capturing everything at DEBUG
- Replace all print() calls in executor.py and report.py with logger.info()/error()
- Add error logging before raising AnsibleError in executor.run()
- Add GRM_LOG_LEVEL to README configuration table and logging documentation
- Update all unit tests to mock logger instead of using capsys
- 114 tests, 100% coverage, pyright clean, ruff clean
This commit is contained in:
Emil Simeonov
2026-06-19 01:24:35 +02:00
parent 815b59c537
commit 3b3e002f7f
7 changed files with 287 additions and 103 deletions
+12 -10
View File
@@ -10,6 +10,7 @@ from pathlib import Path
from .exceptions import AnsibleError
from .i18n import _
from .logging_config import get_logger
class AnsibleExecutor:
@@ -17,31 +18,32 @@ class AnsibleExecutor:
def __init__(self, log_dir: Path | None = None) -> None:
self._log_dir = log_dir or Path.home() / ".local" / "state" / "grm" / "logs"
self._logger = get_logger()
def run(self, cmd: list[str], description: str = "") -> None:
"""Run an Ansible command, redirecting output to a log file."""
log_file = self._prepare_log(cmd)
desc = description or _("Running Ansible playbook")
print(f"[GRM] {desc}")
print(f"[GRM] {_('Full log: {log_file}', log_file=log_file)}")
self._logger.info(desc)
self._logger.info(_("Full log: {log_file}", log_file=log_file))
returncode = self._stream(cmd, log_file)
if returncode != 0:
raise AnsibleError(
_(
"Ansible failed with exit code {code}. See full log: {log_file}",
code=returncode,
log_file=log_file,
)
msg = _(
"Ansible failed with exit code {code}. See full log: {log_file}",
code=returncode,
log_file=log_file,
)
self._logger.error(msg)
raise AnsibleError(msg)
status = self._extract_status(log_file)
if status:
print(f"[GRM] {status}")
self._logger.info(status)
print(f"[GRM] {_('Done. See full log: {log_file}', log_file=log_file)}")
self._logger.info(_("Done. See full log: {log_file}", log_file=log_file))
def _prepare_log(self, cmd: list[str]) -> Path:
"""Create a log file with header."""