[Feature] implement log channel separation and request log level system (#7190)

* feat: implement log channel separation and request log level system

* fix: log system improvements based on review

* add request_id to error logs, use RequestLogLevel enum, and unify logger implementation from utils to logger module
This commit is contained in:
zhouchong
2026-04-16 15:13:05 +08:00
committed by GitHub
parent 29495b2cf1
commit 6e16438a57
52 changed files with 1956 additions and 639 deletions
+107 -17
View File
@@ -20,21 +20,36 @@ This module provides the get_logger method to uniformly manage logging behavior
import logging
import os
import sys
import threading
from contextlib import contextmanager
from pathlib import Path
from fastdeploy import envs
from fastdeploy.logger.formatters import CustomFormatter
from fastdeploy.logger.config import resolve_log_level
from fastdeploy.logger.formatters import ColoredFormatter, CustomFormatter
from fastdeploy.logger.handlers import DailyRotatingFileHandler, LazyFileHandler
from fastdeploy.logger.setup_logging import setup_logging
# Standard log format
_LOG_FORMAT = "%(levelname)-8s %(asctime)s %(process)-5s %(filename)s[line:%(lineno)d] %(message)s"
class FastDeployLogger:
_instance = None
_initialized = False
_lock = threading.RLock()
# Channel to file mapping
_channel_files = {
"main": "fastdeploy.log",
"request": "request.log",
"console": "console.log",
}
# Cache for channel loggers that have been configured
_configured_channels = set()
def __new__(cls):
"""Singleton pattern implementation"""
if cls._instance is None:
@@ -50,7 +65,7 @@ class FastDeployLogger:
setup_logging()
self._initialized = True
def get_logger(self, name, file_name=None, without_formater=False, print_to_console=False):
def get_logger(self, name, file_name=None, without_formater=False, print_to_console=False, channel=None):
"""
Get logger (compatible with the original interface)
@@ -59,7 +74,14 @@ class FastDeployLogger:
file_name: Log file name (for compatibility)
without_formater: Whether to not use a formatter
print_to_console: Whether to print to console
channel: Log channel (main, request, console)
"""
# If channel is specified, use channel-based logging
if channel is not None:
if not self._initialized:
self._initialize()
return self._get_channel_logger(name, channel)
# If only one parameter is provided, use the new unified naming convention
if file_name is None and not without_formater and not print_to_console:
# Lazy initialization
@@ -74,27 +96,95 @@ class FastDeployLogger:
"""
New unified way to get logger
"""
if name is None:
return logging.getLogger("fastdeploy")
return self._get_channel_logger(name, "main")
# Handle __main__ special case
if name == "__main__":
def _get_channel_logger(self, name, channel):
"""
Get logger through channel with manual handler setup.
Uses manual addHandler instead of dictConfig for better performance.
Handlers are attached to the channel root logger (fastdeploy.{channel}),
and child loggers propagate to it.
Args:
name: logger name
channel: log channel (main, request, console)
"""
# Get or create the channel root logger (all handlers go here)
channel_root_name = f"fastdeploy.{channel}"
channel_logger = logging.getLogger(channel_root_name)
# Configure the channel root logger once
if channel not in self._configured_channels:
self._configured_channels.add(channel)
log_dir = envs.FD_LOG_DIR
os.makedirs(log_dir, exist_ok=True)
# Resolve log level (priority: FD_LOG_LEVEL > FD_DEBUG)
log_level = resolve_log_level()
channel_logger.setLevel(logging.DEBUG if log_level == "DEBUG" else logging.INFO)
# Create formatters
file_formatter = logging.Formatter(_LOG_FORMAT)
console_formatter = ColoredFormatter(_LOG_FORMAT)
# Clear existing handlers
for handler in channel_logger.handlers[:]:
channel_logger.removeHandler(handler)
# Create file handler for this channel
file_name = self._channel_files.get(channel, f"{channel}.log")
log_file = os.path.join(log_dir, file_name)
backup_count = int(envs.FD_LOG_BACKUP_COUNT)
file_handler = LazyFileHandler(log_file, backupCount=backup_count)
file_handler.setFormatter(file_formatter)
channel_logger.addHandler(file_handler)
# Error file handler (all channels write errors to error.log)
error_log_file = os.path.join(log_dir, "error.log")
error_file_handler = LazyFileHandler(
filename=error_log_file, backupCount=backup_count, level=logging.ERROR
)
error_file_handler.setFormatter(file_formatter)
channel_logger.addHandler(error_file_handler)
# Stderr handler for ERROR level (all channels output errors to stderr)
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setLevel(logging.ERROR)
stderr_handler.setFormatter(console_formatter)
channel_logger.addHandler(stderr_handler)
# Console stdout handler for console channel only
if channel == "console":
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(logging.DEBUG if log_level == "DEBUG" else logging.INFO)
stdout_handler.setFormatter(console_formatter)
# Filter to exclude ERROR and above (they go to stderr)
stdout_handler.addFilter(lambda record: record.levelno < logging.ERROR)
channel_logger.addHandler(stdout_handler)
channel_logger.propagate = False
# Determine the actual logger name and return the appropriate logger
if name is None or name == "fastdeploy":
return channel_logger
elif name == "__main__":
import __main__
# Get the __file__ attribute of the main module
if hasattr(__main__, "__file__"):
# Get the main module file name
base_name = Path(__main__.__file__).stem
# Create logger with prefix
return logging.getLogger(f"fastdeploy.main.{base_name}")
return logging.getLogger("fastdeploy.main")
# If already in fastdeploy namespace, use directly
if name.startswith("fastdeploy.") or name == "fastdeploy":
return logging.getLogger(name)
logger_name = f"{channel_root_name}.{base_name}"
else:
return channel_logger
elif name.startswith("fastdeploy."):
logger_name = name
else:
# Add fastdeploy prefix for other cases
return logging.getLogger(f"fastdeploy.{name}")
logger_name = f"{channel_root_name}.{name}"
# Child loggers propagate to channel_logger (which has handlers)
return logging.getLogger(logger_name)
def get_trace_logger(self, name, file_name, without_formater=False, print_to_console=False):
"""