02.11.04 — Loggers: PrintLogger, FileLogger, PrintAndFileLogger, MutedLogger#
Source folder: src/ocarina/opinionated/loggers/
Four canonical ILogger implementations + a create_matching_logger factory. All opt-in.
create_matching_logger#
LOGGERS_CHOICES = ("terminal", "file", "terminal+file", "muted")
def create_matching_logger(mode: SupportedLogger) -> ILogger:
if mode == "terminal":
return PrintLogger()
if mode == "file":
return FileLogger(base_dir=get_default_log_dir())
if mode == "terminal+file":
return PrintAndFileLogger(base_dir=get_default_log_dir())
if mode == "muted":
return MutedLogger()
raise ValueError(f"Unsupported logger mode: {mode}")
And:
def get_default_log_dir() -> Path:
return Path.cwd() / ".ocarina_logs"
→ By default, logs land in .ocarina_logs/ at the user project root. Gitignored by convention.
PrintLogger#
class PrintLogger(ILogger):
def __init__(self) -> None:
self._prefix_thunk: Thunk[str] = lambda: ""
self._domain_taxonomy: tuple[str, ...] = ("",)
def set_prefix(self, prefix_thunk: Thunk[str]) -> Self:
self._prefix_thunk = prefix_thunk
return self
def set_domain_taxonomy(self, taxonomy: tuple[str, ...]) -> Self:
self._domain_taxonomy = taxonomy
return self
def raw(self, *args, stream=None, **kwargs) -> None:
if stream is None:
stream = sys.stdout
print(*args, file=stream, **kwargs)
def info(self, msg, *args, exc=None, **kwargs):
self._print_log("[INFO]", msg, *args, exc=exc, stream=sys.stdout)
# ... id. for critical / error / warning / debug / success / test_name ...
- State:
_prefix_thunk (Thunk) + _domain_taxonomy (tuple). - Output:
sys.stdout (info, debug, success, test_name) or sys.stderr (critical, error, warning, exception). - Format: typically
<prefix> [LEVEL] <message>, with ANSI colors if terminal. cleanup(): no-op.
FileLogger#
@final
class FileLogger(PrintLogger):
def __init__(
self, *,
base_dir: Path,
with_flush_effect: bool = True,
with_fallback_on_print_logger_when_no_taxonomy_effect: bool = True,
) -> None:
self._base_dir = base_dir
self._with_flush_effect = with_flush_effect
self._with_fallback_on_print_logger_when_no_taxonomy_effect = (
with_fallback_on_print_logger_when_no_taxonomy_effect
)
super().__init__()
@property
def _log_file_path(self) -> Path:
if len(self._domain_taxonomy) == 1:
folder_path = self._base_dir
file_name = f"{self._domain_taxonomy[0]}.log"
else:
folder_path = self._base_dir.joinpath(*self._domain_taxonomy[:-1])
file_name = f"{self._domain_taxonomy[-1]}.log"
folder_path.mkdir(parents=True, exist_ok=True)
return folder_path / file_name
| Taxonomy | Folder | File |
|---|
("") | — | → Fallback to PrintLogger if enabled |
("annoncements",) | base_dir | annoncements.log |
("e2e", "Login") | base_dir/e2e | Login.log |
("e2e", "Login", "happy paths", "Login without OTP") | base_dir/e2e/Login/happy paths | Login without OTP.log |
The taxonomy is the folder tree. The DOCX reports (see 05-plugins-reports.md) walk it recursively and emit one .docx per test case.