blob: 6b9d91cc7cb0992e90b706d4ce6ad4345e0718d9 [file]
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import enum
import logging
import re
import threading
from dubbo.configs import LoggerConfig
__all__ = ["loggerFactory"]
class ColorFormatter(logging.Formatter):
"""
A formatter with color.
It will format the log message like this:
2024-06-24 16:39:57 | DEBUG | test_logger_factory:test_with_config:44 - [Dubbo] debug log
"""
@enum.unique
class Colors(enum.Enum):
"""
Colors for log messages.
"""
END = "\033[0m"
BOLD = "\033[1m"
BLUE = "\033[34m"
GREEN = "\033[32m"
PURPLE = "\033[35m"
CYAN = "\033[36m"
RED = "\033[31m"
YELLOW = "\033[33m"
GREY = "\033[38;5;240m"
COLOR_LEVEL_MAP = {
"DEBUG": Colors.BLUE.value,
"INFO": Colors.GREEN.value,
"WARNING": Colors.YELLOW.value,
"ERROR": Colors.RED.value,
"CRITICAL": Colors.RED.value + Colors.BOLD.value,
}
DATE_FORMAT: str = "%Y-%m-%d %H:%M:%S"
LOG_FORMAT: str = (
f"{Colors.GREEN.value}%(asctime)s{Colors.END.value}"
" | "
f"%(level_color)s%(levelname)s{Colors.END.value}"
" | "
f"{Colors.CYAN.value}%(module)s:%(funcName)s:%(lineno)d{Colors.END.value}"
" - "
f"{Colors.PURPLE.value}[Dubbo]{Colors.END.value} "
f"%(suffix)s"
f"%(msg_color)s%(message)s{Colors.END.value}"
)
def __init__(self, suffix: str = ""):
super().__init__(self.LOG_FORMAT, self.DATE_FORMAT)
self.suffix = f"{self.Colors.PURPLE.value}[{suffix}]{self.Colors.END.value} " if suffix else ""
def format(self, record) -> str:
levelname = record.levelname
record.level_color = record.msg_color = self.COLOR_LEVEL_MAP.get(levelname)
record.suffix = self.suffix
return super().format(record)
class NoColorFormatter(logging.Formatter):
"""
A formatter without color.
It will format the log message like this:
2024-06-24 16:39:57 | DEBUG | test_logger_factory:test_with_config:44 - [Dubbo] debug log
"""
def __init__(self, suffix: str = ""):
color_re = re.compile(r"\033\[[0-9;]*\w|%\((msg_color|level_color)\)s")
self.log_format = color_re.sub("", ColorFormatter.LOG_FORMAT)
self.suffix = f"[{suffix}] " if suffix else ""
super().__init__(self.log_format, ColorFormatter.DATE_FORMAT)
def format(self, record) -> str:
record.message = self.suffix + record.getMessage()
return super().format(record)
class _LoggerFactory:
"""
The logger factory.
"""
DEFAULT_LOGGER_NAME = "dubbo"
_logger_lock = threading.RLock()
_config: LoggerConfig = LoggerConfig()
_loggers = {}
@classmethod
def set_config(cls, config):
if not isinstance(config, LoggerConfig):
raise TypeError("config must be an instance of LoggerConfig")
cls._config = config
cls._refresh_config()
@classmethod
def _refresh_config(cls) -> None:
"""
Refresh the logger configuration.
"""
with cls._logger_lock:
# create logger if not exists
if not cls._loggers:
cls._loggers[cls.DEFAULT_LOGGER_NAME] = logging.getLogger(cls.DEFAULT_LOGGER_NAME)
# update all loggers
for name, logger in cls._loggers.items():
cls._update_logger(logger, name)
@classmethod
def _update_logger(cls, logger: logging.Logger, name: str) -> logging.Logger:
"""
Update the logger with the current configuration.
:param logger: The logger to update.
:type logger: logging.Logger
:param name: The logger name.
:type name: str
:return: The updated logger.
:rtype: logging.Logger
"""
# clean up handlers
logger.handlers.clear()
config = cls._config
# set logger level
logger.setLevel(config.level)
# add console handler if enabled
if config.is_console_enabled():
logger.addHandler(cls._get_console_handler(name))
# add file handler if enabled
if config.is_file_enabled():
logger.addHandler(cls._get_file_handler(name))
return logger
@classmethod
def _get_console_handler(cls, name: str) -> logging.StreamHandler:
"""
Get the console handler
:param name: The logger name.
:type name: str
:return: The console handler.
:rtype: logging.StreamHandler
"""
console_handler = logging.StreamHandler()
if not cls._config.console_config.formatter or cls._config.global_formatter:
# set default color formatter
console_handler.setFormatter(ColorFormatter(name if name != cls.DEFAULT_LOGGER_NAME else ""))
else:
console_handler.setFormatter(
logging.Formatter(cls._config.console_config.formatter or cls._config.global_formatter)
)
return console_handler
@classmethod
def _get_file_handler(cls, name: str) -> logging.FileHandler:
"""
Get the file handler
:param name: The logger name.
:type name: str
:return: The file handler.
:rtype: logging.FileHandler
"""
file_handler = logging.FileHandler(
filename=cls._config.file_config.file_name,
mode="a",
encoding="utf-8",
)
if not cls._config.file_config.file_formatter or cls._config.global_formatter:
# set default no color formatter
file_handler.setFormatter(NoColorFormatter(name if name != cls.DEFAULT_LOGGER_NAME else ""))
else:
file_handler.setFormatter(
logging.Formatter(cls._config.file_config.file_formatter or cls._config.global_formatter)
)
return file_handler
@classmethod
def get_logger(cls, name=DEFAULT_LOGGER_NAME) -> logging.Logger:
"""
Get the logger. class method.
:return: The logger.
:rtype: logging.Logger
"""
logger = cls._loggers.get(name)
if logger is not None:
return logger
with cls._logger_lock:
logger = cls._loggers.get(name)
# double check
if logger is not None:
return logger
logger = cls._update_logger(logging.getLogger(name), name)
cls._loggers[name] = logger
return logger
# expose loggerFactory
loggerFactory = _LoggerFactory