解决uvicorn两种启动方式,日志打印不一致的问题

问题描述:

《fastapi项目使用loguru,如何在uvicorn代码式启动时,也能正常打印出请求日志?》
https://segmentfault.com/q/10...

问题定位:

首先想到,uvicorn两种启动方式,uvicorn运行时的日志handler会不一致?为验证该想法,加入了调试代码。

def get_logger_info(msg:str):
    LOGGERS = (
        logging.getLogger(name)
        for name in logging.root.manager.loggerDict
        if name.startswith("uvicorn")
    ) 
    for uvicorn_logger in LOGGERS:
        print(msg, uvicorn_logger.name, uvicorn_logger.handlers, len(uvicorn_logger.handlers))

在main函数中,uvicorn配置前后,加入get_logger_info调试代码

if __name__ == "__main__":
    ......
    get_logger("Before init uvicorn: ")
    config = uvicorn.Config("main:app", host=SERVEICE_HOST_IP, port=int(SERVICE_HOST_PORT), access_log=True, workers=1)
    server = uvicorn.Server(config)
    get_logger("After init uvicorn: ")
    server.run()

使用python main.py方式启动,结果如下:

2022-07-20 14:38:46.376 | MainThread | INFO | main::224 - 日志级别: DEBUG
2022-07-20 14:38:46.380 | MainThread | INFO | main::225 - 数据存放目录: E:\vitural\envs\videorecord\VideoRecordProxy\data
Before init uvicorn:  uvicorn.error [] 0
Before init uvicorn:  uvicorn [] 1
Before init uvicorn:  uvicorn.access [] 1
After init uvicorn:  uvicorn.error [] 0
After init uvicorn:  uvicorn [ (NOTSET)>] 1
After init uvicorn:  uvicorn.access [ (NOTSET)>] 1
INFO:     Started server process [22648]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

果然,在运行 uvicorn.Config()前指定好的拦截handler,在 uvicorn.Config()后,被替换成其它handler。不难想到,uvicorn.Config()会配置一个默认的日志配置。

查看uvicorn源码,也的确如此。Config类若不传入log_config,会默认指定LOGGING_CONFIG。

class Config:
    def __init__(
        self,
        app: Union[ASGIApplication, Callable, str],
        host: str = "127.0.0.1",
        port: int = 8000,
        uds: Optional[str] = None,
        fd: Optional[int] = None,
        loop: LoopSetupType = "auto",
        http: Union[Type[asyncio.Protocol], HTTPProtocolType] = "auto",
        ws: Union[Type[asyncio.Protocol], WSProtocolType] = "auto",
        ws_max_size: int = 16 * 1024 * 1024,
        ws_ping_interval: Optional[float] = 20,
        ws_ping_timeout: Optional[float] = 20,
        lifespan: LifespanType = "auto",
        env_file: Optional[Union[str, os.PathLike]] = None,
        log_config: Optional[Union[dict, str]] = LOGGING_CONFIG,
        ....

再来看下LOGGING_CONFIG到底有什么:

LOGGING_CONFIG: dict = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "default": {
            "()": "uvicorn.logging.DefaultFormatter",
            "fmt": "%(levelprefix)s %(message)s",
            "use_colors": None,
        },
        "access": {
            "()": "uvicorn.logging.AccessFormatter",
            "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',  # noqa: E501
        },
    },
    "handlers": {
        "default": {
            "formatter": "default",
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stderr",
        },
        "access": {
            "formatter": "access",
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stdout",
        },
    },
    "loggers": {
        "uvicorn": {"handlers": ["default"], "level": "INFO"},
        "uvicorn.error": {"level": "INFO"},
        "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False},
    },
}

解决方式:

uvicorn代码启动的方式下,要将日志拦截器初始化的过程在uvicorn.Config()后执行:

if __name__ == "__main__":
    config = uvicorn.Config("main:app", host=SERVEICE_HOST_IP, port=int(SERVICE_HOST_PORT), access_log=True, workers=1)
    server = uvicorn.Server(config)
    init_logger()
    server.run()
def init_logger():
    LOGGER_NAMES = ("uvicorn","uvicorn.access",)
    for logger_name in LOGGER_NAMES:
        logging_logger = logging.getLogger(logger_name)
        logging_logger.handlers = [InterceptHandler()]
    logger.configure(**loguru_config)

日志按照预想的打印:

2022-07-20 15:00:03.137 | MainThread | INFO | server:serve:84 - Started server process [22244]
2022-07-20 15:00:03.141 | MainThread | INFO | on:startup:45 - Waiting for application startup.
2022-07-20 15:00:03.147 | MainThread | INFO | on:startup:59 - Application startup complete.
2022-07-20 15:00:03.156 | MainThread | INFO | server:_log_started_message:222 - Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
2022-07-20 15:00:13.635 | MainThread | INFO | httptools_impl:send:447 - 127.0.0.1:62664 - "GET /docs HTTP/1.1" 200
2022-07-20 15:00:25.161 | MainThread | INFO | httptools_impl:send:447 - 127.0.0.1:62665 - "GET /openapi.json HTTP/1.1" 200

注意重复打印的问题

分析日志重复打印的问题,首先要了解logger对象的两个特性:
1)logger对象有父子关系的,当没有父logger对象时,其父对象就是root。
2)logger对象打日志时,其所有的父对象也能同时收到日志。这个特性由propagate属性控制,官网对propagate解释:

propagate
如果这个属性为真,记录到这个记录器的事件除了会发送到此记录器的所有处理程序外,还会传递给更高级别(祖先)记录器的处理器,此外任何关联到这个记录器的处理器。消息会直接传递给祖先记录器的处理器 —— 不考虑祖先记录器的级别和过滤器。

理解了上述特性,再来看重复打印的问题就简单了。
LOGGING_CONFIG中与uvicorn相关的记录器有三个,"uvicorn"、"uvicorn.error"、 "uvicorn.access",同时还有root记录器,即""。

在我的例子中,只选择"uvicorn","uvicorn.access"两个logger指定handler, 而uvicorn.access的propagate默认是False,因此,不会出现重复日志。

但如果,我将"uvicorn.error"也指定handler:

def init_logger():
    LOGGER_NAMES = ("uvicorn","uvicorn.access","uvicorn.error",)
    for logger_name in LOGGER_NAMES:
        logging_logger = logging.getLogger(logger_name)
        logging_logger.handlers = [InterceptHandler()]
    logger.configure(**loguru_config)

LOGGING_CONFIG中uvicorn.error的propagate属性为True,其父对象"uvicorn"也会收到日志,就会重复打印。

2022-07-20 15:53:32.790 | MainThread | INFO | server:serve:84 - Started server process [22396]
2022-07-20 15:53:32.934 | MainThread | INFO | server:serve:84 - Started server process [22396]
2022-07-20 15:53:32.936 | MainThread | INFO | on:startup:45 - Waiting for application startup.
2022-07-20 15:53:32.937 | MainThread | INFO | on:startup:45 - Waiting for application startup.
2022-07-20 15:53:32.943 | MainThread | INFO | on:startup:59 - Application startup complete.   
2022-07-20 15:53:32.944 | MainThread | INFO | on:startup:59 - Application startup complete.
2022-07-20 15:53:32.948 | MainThread | INFO | server:_log_started_message:222 - Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
2022-07-20 15:53:32.950 | MainThread | INFO | server:_log_started_message:222 - Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

因此,解决重复打印的问题就要控制好父子logger之间的日志传递。

你可能感兴趣的:(fastapipython)