update logging module

This commit is contained in:
Mose Müller 2023-10-16 15:51:52 +02:00
parent 1241d7a128
commit 5d7a7c6bdb

View File

@ -1,82 +1,111 @@
import logging import logging
import sys import sys
from types import FrameType from copy import copy
from typing import Optional from typing import Optional
import loguru import uvicorn.logging
import rpyc
from uvicorn.config import LOGGING_CONFIG from uvicorn.config import LOGGING_CONFIG
import pydase.config import pydase.config
ALLOWED_LOG_LEVELS = ["DEBUG", "INFO", "ERROR"]
class DefaultFormatter(uvicorn.logging.ColourizedFormatter):
"""
A custom log formatter class that:
* Outputs the LOG_LEVEL with an appropriate color.
* If a log call includes an `extras={"color_message": ...}` it will be used
for formatting the output, instead of the plain text message.
"""
def formatMessage(self, record: logging.LogRecord) -> str:
recordcopy = copy(record)
levelname = recordcopy.levelname
seperator = " " * (8 - len(recordcopy.levelname))
if self.use_colors:
levelname = self.color_level_name(levelname, recordcopy.levelno)
if "color_message" in recordcopy.__dict__:
recordcopy.msg = recordcopy.__dict__["color_message"]
recordcopy.__dict__["message"] = recordcopy.getMessage()
recordcopy.__dict__["levelprefix"] = levelname + seperator
return logging.Formatter.formatMessage(self, recordcopy)
def should_use_colors(self) -> bool:
return sys.stderr.isatty() # pragma: no cover
# from: https://github.com/Delgan/loguru section def setup_logging(level: Optional[str | int] = None) -> None:
# "Entirely compatible with standard logging" """
class InterceptHandler(logging.Handler): Configures the logging settings for the application.
def emit(self, record: logging.LogRecord) -> None:
# Ignore "asyncio.CancelledError" raised by uvicorn
if record.name == "uvicorn.error" and "CancelledError" in record.msg:
return
# Get corresponding Loguru level if it exists. This function sets up logging with specific formatting and colorization of log
level: int | str messages. The log level is determined based on the application's operation mode,
try: with an option to override the level. By default, in a development environment, the
level = loguru.logger.level(record.levelname).name log level is set to DEBUG, whereas in other environments, it is set to INFO.
except ValueError:
level = record.levelno
# Find caller from where originated the logged message. Parameters:
frame: Optional[FrameType] = sys._getframe(6) level (Optional[str | int]):
depth = 6 A specific log level to set for the application. If None, the log level is
while frame and frame.f_code.co_filename == logging.__file__: determined based on the application's operation mode. Accepts standard log
frame = frame.f_back level names ('DEBUG', 'INFO', etc.) and corresponding numerical values.
depth += 1
try: Example:
msg = record.getMessage()
except TypeError:
# A `TypeError` is raised when the `msg` string expects more arguments
# than are provided by `args`. This can happen when intercepting log
# messages with a certain format, like
# > logger.debug("call: %s%r", method_name, *args) # in tiqi_rpc
# where `*args` unpacks a sequence of values that should replace
# placeholders in the string.
msg = record.msg % (record.args[0], record.args[2:]) # type: ignore
loguru.logger.opt(depth=depth, exception=record.exc_info).log(level, msg) ```python
>>> import logging
>>> setup_logging(logging.DEBUG)
>>> setup_logging("INFO")
```
"""
logger = logging.getLogger()
def setup_logging(level: Optional[str] = None) -> None:
loguru.logger.debug("Configuring service logging.")
if pydase.config.OperationMode().environment == "development": if pydase.config.OperationMode().environment == "development":
log_level = "DEBUG" log_level = logging.DEBUG
else: else:
log_level = "INFO" log_level = logging.INFO
if level is not None and level in ALLOWED_LOG_LEVELS: # If a level is specified, check whether it's a string or an integer.
log_level = level if level is not None:
if isinstance(level, str):
# Convert known log level strings directly to their corresponding logging
# module constants.
level_name = level.upper() # Ensure level names are uppercase
if hasattr(logging, level_name):
log_level = getattr(logging, level_name)
else:
raise ValueError(
f"Invalid log level: {level}. Must be one of 'DEBUG', 'INFO', "
"'WARNING', 'ERROR', etc."
)
elif isinstance(level, int):
log_level = level # Directly use integer levels
else:
raise ValueError("Log level must be a string or an integer.")
loguru.logger.remove() # Set the logger's level.
loguru.logger.add(sys.stderr, level=log_level) logger.setLevel(log_level)
# set up the rpyc logger *before* adding the InterceptHandler to the logging module # create console handler and set level to debug
rpyc.setup_logger(quiet=True) # type: ignore ch = logging.StreamHandler()
logging.basicConfig(handlers=[InterceptHandler()], level=0) # add formatter to ch
ch.setFormatter(
DefaultFormatter(
fmt="%(asctime)s.%(msecs)03d | %(levelprefix)s | %(name)s:%(funcName)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
)
# add ch to logger
logger.addHandler(ch)
logger.debug("Configuring service logging.")
logging.getLogger("asyncio").setLevel(logging.INFO) logging.getLogger("asyncio").setLevel(logging.INFO)
logging.getLogger("urllib3").setLevel(logging.INFO) logging.getLogger("urllib3").setLevel(logging.INFO)
# overwriting the uvicorn logging config to use the loguru intercept handler # configuring uvicorn logger
LOGGING_CONFIG["handlers"] = { LOGGING_CONFIG["formatters"]["default"][
"default": { "fmt"
"()": InterceptHandler, ] = "%(asctime)s.%(msecs)03d | %(levelprefix)s %(message)s"
"formatter": "default", LOGGING_CONFIG["formatters"]["default"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
},
"access": {
"()": InterceptHandler,
"formatter": "access",
},
}