Loguru migration notes
This repository has migrated from the standard library logging module to loguru for all generated project logging.
What changed
template/config.py— replacedloggingboilerplate with a single-file loguru setup (console sink atINFO, colorised output)template/config_dotenv.py— new env-driven variant for--dotenvprojectstemplate/logging_setup.py— new shared setup module for workspace projectstemplate/logging_setup_dotenv.py— dotenv variant for workspace projectssrc/uv_start/dev_deps.py—loguruis now always added as a runtime dependency;python-dotenvis added only when--dotenvis passedsrc/uv_start/parse_docs.py—_init_versionnow wiresfrom . import configin__init__.pyfor single-package projects; workspace members getsetup_logging_onceat theirmain()entry point
Why loguru
The standard logging module requires significant boilerplate (handlers, formatters, getLogger calls) and produces plain text output. Loguru provides:
- Zero config:
from loguru import loggerand you are ready to log - Colourised console output out of the box
- Structured tracebacks with full context and variable values (
diagnose=True) - Rotating file sinks with compression and retention in a single call
- One global logger: no per-module
getLogger(__name__)factory needed
How logging works in new projects
Single-package projects
The package __init__.py imports config on package load:
# src/my_package/__init__.py
from . import config # noqa: F401config.py removes loguru’s default handler and installs a colourised console sink:
# src/my_package/config.py
import sys
from loguru import logger
logger.remove()
logger.add(sys.stderr, level="INFO", colorize=True)After this runs (which happens the moment you import my_package), the global logger is ready. In any module within the package:
from loguru import logger
logger.info("Running analysis")
logger.debug("x = {}", x)Workspace projects
In a workspace, configuring loguru in each package’s __init__.py would cause last-one-wins clobbering — whichever package is imported last silently resets all the sinks.
The solution is to configure logging exactly once, at the application entry point, using common-utils:
workspace-root/
└── packages/
├── common_utils/ ← always created; hosts shared logging setup
│ └── src/common_utils/
│ └── logging_setup.py
└── my-app/
└── src/my_app/
└── __init__.py ← calls setup_logging_once() in main()
common_utils/logging_setup.py provides two functions:
def setup_logging(*, level: str = "INFO", log_file: str | None = None) -> None:
"""Configure loguru's sinks. Call ONCE at entry point, never on import."""
...
def setup_logging_once(...) -> None:
"""Idempotent variant — first call configures, later calls are no-ops."""
...Library packages in the workspace (common_utils itself, shared utilities) only emit log messages — they never configure sinks:
# In any library package
from loguru import logger
logger.debug("Processing item {}", item)Application packages call setup_logging_once() inside main():
# src/my_app/__init__.py
from common_utils.logging_setup import setup_logging_once
from loguru import logger
def main() -> None:
setup_logging_once()
logger.info("Hello from my-app!")This guarantees that sinks are configured exactly once, regardless of how many packages are imported or in what order.
Adding a rotating file sink
In any project, uncomment (or add) a file sink after the console sink in config.py:
logger.add(
"logs/app.log",
level="DEBUG",
rotation="1 MB",
retention=5,
compression="zip",
backtrace=True,
diagnose=True,
)In a workspace, pass log_file to setup_logging_once() at startup:
def main() -> None:
setup_logging_once(level="DEBUG", log_file="logs/app.log")Environment-driven log level (--dotenv)
Scaffold with --dotenv to get env-var-aware config. Copy .env.example to .env and set:
LOG_LEVEL=DEBUG
LOG_FILE=logs/app.logRecognised variables:
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
INFO |
Minimum level for all sinks |
LOG_FILE |
(unset) | Path for rotating file sink |
ENV |
development |
Loads .env.<ENV> overrides if present |
Never commit .env — it is gitignored by default. Use .env.example as the committed template.