问题描述:
《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之间的日志传递。