一、背景
有人提出,message日志不能放我们自己的服务的日志,需要将该日志单独搞到一个地方。
二、先说结论
本文直接上结论,可能是全CSDN唯一的解释。哈哈哈哈哈哈哈哈。
在uvicorn启动的时候,传入log_config参数
那么,这个参数如何传呢?我先给个样例
LOGGING_CONFIG = {
"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',
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": "./log"
},
"access": {
"formatter": "access",
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": "./log"
},
},
"loggers": {
"": {"handlers": ["default"], "level": "INFO"},
"uvicorn.error": {"level": "INFO"},
"uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False},
},
}
这样的配置,会使unicorn 的日志,按照TimedRotatingFileHandler的默认切割方法,将日志写到我们配置的./log文件中
三、源码解析
首先,我们先在一个代码文件中,将服务跑起来,用官网的命令行方式也是一样,这里不过多介绍
from typing import Optional
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Optional[str] = None):
return {"item_id": item_id, "q": q}
if __name__ == "__main__":
uvicorn.run(app=app,
host="0.0.0.0",
port=12345,
)
运行结果如下:
遇到了问题,先去看了看官方文档。
但官方文档,就这么一点点,只说有个参数能配置,十分令人生气。(迅速调整情绪)开始读源码。
点住 run 方法,进去看看
再点开Config进去看看
原来,这里给了一个默认的日志配置,再往下点 LOGGING_CONFIG
就找到了默认的配置:
LOGGING_CONFIG = {
"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',
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
"access": {
"formatter": "access",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
},
"loggers": {
"": {"handlers": ["default"], "level": "INFO"},
"uvicorn.error": {"level": "INFO"},
"uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False},
},
}
再继续往下追,看看这个 配置使如何处理的呢
来到233行,对字典的配置的处理,继续往下点,来到了关键的代码:这一块对字典中的内容进行解析。
?
?
?来到了
self.configure_logger(name, loggers[name])
——————————————
再往里面进入,点一层
?来看这个函数,我们就看到了本质,本质上,还是给logger.addHandler方法。
def common_logger_config(self, logger, config, incremental=False):
"""
Perform configuration which is common to root and non-root loggers.
"""
level = config.get('level', None)
if level is not None:
logger.setLevel(logging._checkLevel(level))
if not incremental:
#Remove any existing handlers
for h in logger.handlers[:]:
logger.removeHandler(h)
handlers = config.get('handlers', None)
if handlers:
self.add_handlers(logger, handlers)
filters = config.get('filters', None)
if filters:
self.add_filters(logger, filters)
def add_handlers(self, logger, handlers):
"""Add handlers to a logger from a list of names."""
for h in handlers:
try:
logger.addHandler(self.config['handlers'][h])
except Exception as e:
raise ValueError('Unable to add handler %r' % h) from e
这段代码追到这里就可以停止了。回头追解析部分。
for name in sorted(handlers):
try:
handler = self.configure_handler(handlers[name])
?话不多说,直接把函数贴上来了。
def configure_handler(self, config):
"""Configure a handler from a dictionary."""
config_copy = dict(config) # for restoring in case of error
formatter = config.pop('formatter', None)
if formatter:
try:
formatter = self.config['formatters'][formatter]
except Exception as e:
raise ValueError('Unable to set formatter '
'%r' % formatter) from e
level = config.pop('level', None)
filters = config.pop('filters', None)
if '()' in config:
c = config.pop('()')
if not callable(c):
c = self.resolve(c)
factory = c
else:
cname = config.pop('class')
klass = self.resolve(cname)
#Special case for handler which refers to another handler
if issubclass(klass, logging.handlers.MemoryHandler) and\
'target' in config:
try:
th = self.config['handlers'][config['target']]
if not isinstance(th, logging.Handler):
config.update(config_copy) # restore for deferred cfg
raise TypeError('target not configured yet')
config['target'] = th
except Exception as e:
raise ValueError('Unable to set target handler '
'%r' % config['target']) from e
elif issubclass(klass, logging.handlers.SMTPHandler) and\
'mailhost' in config:
config['mailhost'] = self.as_tuple(config['mailhost'])
elif issubclass(klass, logging.handlers.SysLogHandler) and\
'address' in config:
config['address'] = self.as_tuple(config['address'])
factory = klass
props = config.pop('.', None)
kwargs = {k: config[k] for k in config if valid_ident(k)}
try:
result = factory(**kwargs)
except TypeError as te:
if "'stream'" not in str(te):
raise
#The argument name changed from strm to stream
#Retry with old name.
#This is so that code can be used with older Python versions
#(e.g. by Django)
kwargs['strm'] = kwargs.pop('stream')
result = factory(**kwargs)
if formatter:
result.setFormatter(formatter)
if level is not None:
result.setLevel(logging._checkLevel(level))
if filters:
self.add_filters(result, filters)
if props:
for name, value in props.items():
setattr(result, name, value)
return result
请注意以下两行 ?
klass = self.resolve(cname)
factory = klass
result = factory(**kwargs)
再结合resolve方法,发现,这就是个通过类名获取该类的方法。然后再把参数传给该类。
我理解,就是JAVA中的反射机制
源码就看到这里了。
四、实战解析
1.首先找到我们想要配置的Handler
可以参考我之前的文章
Python日志详解【两篇就够了系列】--第一篇logging_康雨城的博客-CSDN博客
2.修改已有配置为自己的配置
?
?不难发现,class为类名,后面的参数就是类的参数名。
五、启动验证
from typing import Optional
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Optional[str] = None):
return {"item_id": item_id, "q": q}
LOGGING_CONFIG = {
"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',
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": "./log"
},
"access": {
"formatter": "access",
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": "./log"
},
},
"loggers": {
"": {"handlers": ["default"], "level": "INFO"},
"uvicorn.error": {"level": "INFO"},
"uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False},
},
}
if __name__ == "__main__":
uvicorn.run(app=app,
host="0.0.0.0",
port=12345,
log_config=LOGGING_CONFIG,
)
可以发现,日志打到了文件里面
?
|