Compare commits

...
2 Commits
Author SHA1 Message Date
LeoVasanko fa287eba20 Make buildutil logging use same emojis as the main fastapi-vue logging. 2026-09-18 02:52:52 +00:00
LeoVasanko 0f5b526df6 Add public setup_logging() for standalone pretty logging
Optionally usable early in CLI mains, devservers and scripts that log
before (or without) server.run().  Shares patch_log_config with the
server path (same log_config overrides), but installs no server-side
patches: Formatter gains an install flag gating the lifespan/error
middleware monkeypatches, and the NullHandler backdoor is skipped.
dev=None follows env.dev for the root level, True/False override it.
Pre-existing loggers survive the dictConfig (disable_existing_loggers
is set False).
2026-09-18 02:49:16 +00:00
3 changed files with 66 additions and 21 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
"""FastAPI Vue integration - serve Vue frontend from FastAPI."""
from .environ import env, teleport
from .logging import setup_logging
from .staticfiles import Frontend
__all__ = ["Frontend", "env", "teleport"]
__all__ = ["Frontend", "env", "setup_logging", "teleport"]
+40 -6
View File
@@ -98,12 +98,14 @@ class Formatter(logging.Formatter):
use_colors: bool | None = None, # noqa: FBT001 # mirrors logging.Formatter
*,
access: bool = False,
install: bool = True,
) -> None:
"""Load tracerite, optionally install the access log, detect color support."""
"""Load tracerite, optionally install server patches and access log."""
tracerite.load()
tracerite.load_suppressions(
extra={"starlette.routing": "until", "fastapi.routing": "until"}
)
if install:
patch_lifespan_logging()
patch_server_error_middleware()
if access:
@@ -309,7 +311,35 @@ def _merge_log_config(base: dict, overlay: dict) -> dict:
return base
def patch_log_config(log_config, *, access_log: bool = True): # noqa: ANN001, ANN201
def setup_logging(*, log_config: dict | None = None, dev: bool | None = None) -> None:
"""Set up pretty logging standalone, outside of ``server.run()``.
Optional helper for CLI mains, devservers and scripts that log before
(or without) starting the server. Loads tracerite directly and applies
the same patching as the server path (see ``patch_log_config``, a
private helper) with ``logging.config.dictConfig``: partial dicts merge
over uvicorn's default config, so only customizations are needed. The
server-side patches (error middleware, access log) are not installed —
there is no server here. The root logger level is INFO with *dev*
true, WARNING otherwise; *dev* of None follows ``env.dev``. An
explicit level in *log_config* always wins::
import fastapi_vue
fastapi_vue.setup_logging(log_config={"loggers": {"myapp": {"level": "DEBUG"}}})
"""
if log_config is not None and not isinstance(log_config, dict):
msg = f"setup_logging requires a dict log_config, got {type(log_config).__name__}"
raise TypeError(msg)
import logging.config
tracerite.load()
config = patch_log_config(log_config or {}, access_log=False, install=False, dev=dev)
# Standalone setup must not disable loggers created before this call.
config.setdefault("disable_existing_loggers", False)
logging.config.dictConfig(config)
def patch_log_config(log_config, *, access_log: bool = True, install: bool = True, dev: bool | None = None): # noqa: ANN001, ANN201
"""Patch a uvicorn log_config dict for our logging, best-effort.
A dict without a ``version`` key is treated as a partial config: it is
@@ -330,9 +360,11 @@ def patch_log_config(log_config, *, access_log: bool = True): # noqa: ANN001, A
``watchfiles.main`` logger is lifted to WARNING so its INFO "N changes
detected" line is dropped while the WARNING "Reloading..." line (logged
to ``uvicorn.error``) still shows; a user-supplied level wins.
With ``access_log``, additionally rewires the ``access`` formatter to
our Formatter and attaches its handler to our ``fastapi_vue.access``
logger. We must not
With ``install=False`` (standalone use via ``setup_logging``) the
NullHandler backdoor and the server-side patches in Formatter are
skipped. With ``access_log``, additionally rewires the ``access``
formatter to our Formatter and attaches its handler to our
``fastapi_vue.access`` logger. We must not
attach handlers to ``uvicorn.access``: uvicorn gates its own
protocol-level access logging on ``uvicorn.access.hasHandlers()``.
"""
@@ -342,6 +374,7 @@ def patch_log_config(log_config, *, access_log: bool = True): # noqa: ANN001, A
if "version" not in config:
config = _merge_log_config(deepcopy(LOGGING_CONFIG), config)
if install:
with suppress(Exception):
config["formatters"]["fastapi_vue"] = {"()": "fastapi_vue.logging.Formatter"}
config["handlers"]["fastapi_vue"] = {
@@ -367,6 +400,7 @@ def patch_log_config(log_config, *, access_log: bool = True): # noqa: ANN001, A
"()": "fastapi_vue.logging.Formatter",
"fmt": "%(message)s",
"use_colors": None,
"install": install,
}
# uvicorn's default config leaves the root logger handlerless, eating
@@ -375,7 +409,7 @@ def patch_log_config(log_config, *, access_log: bool = True): # noqa: ANN001, A
# with Python's default; dev keeps INFO. Subloggers can override.
with suppress(Exception):
root = config.setdefault("root", {})
root.setdefault("level", "INFO" if env.dev else "WARNING")
root.setdefault("level", "INFO" if (env.dev if dev is None else dev) else "WARNING")
if "default" in config.get("handlers", {}):
root_handlers = root.setdefault("handlers", [])
if "default" not in root_handlers:
+16 -6
View File
@@ -10,21 +10,31 @@ from pathlib import Path
MIN_NODE_VERSION = 20
# Duplicated from fastapi_vue.logging because build environment is isolated
_LEVEL_EMOJI = {
logging.DEBUG: "🐛",
logging.INFO: "🔷",
logging.WARNING: "",
logging.ERROR: "🛑",
logging.CRITICAL: "🚨",
}
class _PrefixFormatter(logging.Formatter):
"""Formatter that adds prefix based on log level."""
class _Formatter(logging.Formatter):
"""Emoji level prefix formatter, mirroring fastapi_vue.logging.Formatter."""
def format(self, record: logging.LogRecord) -> str:
if record.levelno >= logging.WARNING:
return f"⚠️ {record.getMessage()}"
return record.getMessage()
emoji = _LEVEL_EMOJI.get(record.levelno)
prefix = f"{emoji} " if emoji else f"{record.levelname}: "
return prefix + record.getMessage()
_handler = logging.StreamHandler()
_handler.setFormatter(_PrefixFormatter())
_handler.setFormatter(_Formatter())
logger = logging.getLogger("fastapi-vue")
logger.addHandler(_handler)
logger.setLevel(logging.INFO)
logger.propagate = False # own handler; do not double-print via a configured root
def _check_node_version(node_path: str) -> None: