Source code for logcore.interop
"""Interoperability between LogCore and the standard library's ``logging``.
``get_logger`` only formats records you emit yourself. Everything a third-party
library logs — uvicorn, gunicorn, sqlalchemy, requests, celery, botocore — goes
through the stdlib root logger and comes out in whatever format that logger
happens to have. In a JSON log pipeline that means half the output is
unparseable.
:func:`configure_stdlib` closes that gap by putting LogCore's formatters on the
root logger, so *every* record in the process is formatted consistently.
"""
import logging
from typing import Dict, Iterable, List, Optional, Set, Tuple, Union
from .config import LogLevel, create_config
from .handlers import create_handlers
__all__ = ["configure_stdlib", "reset_stdlib", "dict_config_formatter"]
# Handlers this module installed, so reset_stdlib can remove exactly those.
_installed: List[logging.Handler] = []
[docs]
def configure_stdlib(
level: Union[str, LogLevel] = "INFO",
json: bool = True,
file: Optional[str] = None,
replace_existing: bool = True,
redact_fields: Optional[Set[str]] = None,
quiet: Optional[Iterable[Union[str, Tuple[str, str]]]] = None,
async_logging: bool = False,
) -> logging.Logger:
"""Route the stdlib root logger through LogCore's formatters.
Call this once during application startup, before the libraries you want to
capture emit anything.
Args:
level: Root log level.
json: Emit JSON (the usual choice when this is worth doing) or
human-readable text.
file: Optional path to also write a rotating log file.
replace_existing: Remove handlers already on the root logger. Leave
this True to override an earlier ``logging.basicConfig()``, which
would otherwise double every line.
redact_fields: Field names to mask. Defaults to LogCore's built-in set.
quiet: Loggers to turn down. Either a name (raised to WARNING) or a
``(name, level)`` pair.
async_logging: Move handler I/O onto a background thread.
Returns:
The configured root logger.
Example:
>>> import logcore
>>> logcore.configure_stdlib(level="INFO", json=True,
... quiet=["urllib3", ("botocore", "ERROR")])
"""
level_value = level.value if isinstance(level, LogLevel) else str(level)
config = create_config(
name="root",
level=level_value,
json=json,
file=file,
redact_fields=redact_fields,
async_logging=async_logging,
)
root = logging.getLogger()
if replace_existing:
for handler in list(root.handlers):
root.removeHandler(handler)
try:
handler.close()
except Exception: # pragma: no cover - close is best effort
pass
_installed.clear()
numeric_level = getattr(logging, config.level.value)
root.setLevel(numeric_level)
for handler in create_handlers(config):
handler.setLevel(numeric_level)
root.addHandler(handler)
_installed.append(handler)
for entry in quiet or ():
if isinstance(entry, tuple):
name, quiet_level = entry
else:
name, quiet_level = entry, "WARNING"
logging.getLogger(name).setLevel(
getattr(logging, LogLevel.from_string(quiet_level).value)
)
return root
[docs]
def reset_stdlib() -> None:
"""Remove the handlers :func:`configure_stdlib` installed on the root logger.
Intended for tests and for applications that reconfigure logging at
runtime. Handlers installed by anything else are left alone.
"""
root = logging.getLogger()
for handler in _installed:
root.removeHandler(handler)
try:
handler.close()
except Exception: # pragma: no cover - close is best effort
pass
_installed.clear()
[docs]
def dict_config_formatter(
json: bool = True, redact_fields: Optional[Set[str]] = None
) -> Dict[str, object]:
"""Return a ``logging.config.dictConfig`` formatter entry for LogCore.
For applications that configure logging declaratively rather than by
calling :func:`configure_stdlib`.
Example:
>>> import logging.config, logcore
>>> logging.config.dictConfig({
... "version": 1,
... "formatters": {"logcore": logcore.dict_config_formatter()},
... "handlers": {"console": {"class": "logging.StreamHandler",
... "formatter": "logcore"}},
... "root": {"handlers": ["console"], "level": "INFO"},
... })
"""
entry: Dict[str, object] = {
"()": (
"logcore.formatters.JSONFormatter"
if json
else "logcore.formatters.TextFormatter"
)
}
if redact_fields is not None:
entry["redact_fields"] = redact_fields
return entry