Unify Python logging for a Gunicorn/Uvicorn/FastAPI application 最近在写一个 fastapi,之后使用 uvicorn 去启动的 application,但是在日志格式这一块,我觉得不够统一,索性就记录一下使用 loguru 替换内置的日志模块 logging 的过程.
Gunicorn + Uvicorn version[¤] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 import osimport loggingimport sysfrom gunicorn.app.base import BaseApplicationfrom gunicorn.glogging import Loggerfrom loguru import loggerfrom my_app.app import appLOG_LEVEL = logging.getLevelName(os.environ.get("LOG_LEVEL" , "DEBUG" )) JSON_LOGS = True if os.environ.get("JSON_LOGS" , "0" ) == "1" else False WORKERS = int (os.environ.get("GUNICORN_WORKERS" , "5" )) class InterceptHandler (logging.Handler): def emit (self, record ): try : level = logger.level(record.levelname).name except ValueError: level = record.levelno frame, depth = sys._getframe(6 ), 6 while frame and frame.f_code.co_filename == logging.__file__: frame = frame.f_back depth += 1 logger.opt(depth=depth, exception=record.exc_info).log( level, record.getMessage() ) class StubbedGunicornLogger (Logger ): def setup (self, cfg ): handler = logging.NullHandler() self.error_logger = logging.getLogger("gunicorn.error" ) self.error_logger.addHandler(handler) self.access_logger = logging.getLogger("gunicorn.access" ) self.access_logger.addHandler(handler) self.error_logger.setLevel(LOG_LEVEL) self.access_logger.setLevel(LOG_LEVEL) class StandaloneApplication (BaseApplication ): """Our Gunicorn application.""" def __init__ (self, app, options=None ): self.options = options or {} self.application = app super ().__init__() def load_config (self ): config = { key: value for key, value in self.options.items() if key in self.cfg.settings and value is not None } for key, value in config.items(): self.cfg.set (key.lower(), value) def load (self ): return self.application if __name__ == "__main__" : intercept_handler = InterceptHandler() logging.root.setLevel(LOG_LEVEL) seen = set () for name in [ *logging.root.manager.loggerDict.keys(), "gunicorn" , "gunicorn.access" , "gunicorn.error" , "uvicorn" , "uvicorn.access" , "uvicorn.error" , ]: if name not in seen: seen.add(name.split("." )[0 ]) logging.getLogger(name).handlers = [intercept_handler] logger.configure(handlers=[{"sink" : sys.stdout, "serialize" : JSON_LOGS}]) options = { "bind" : "0.0.0.0" , "workers" : WORKERS, "accesslog" : "-" , "errorlog" : "-" , "worker_class" : "uvicorn.workers.UvicornWorker" , "logger_class" : StubbedGunicornLogger, } StandaloneApplication(app, options).run()
Uvicorn-only version[¤] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import osimport loggingimport sysfrom uvicorn import Config, Serverfrom loguru import loggerLOG_LEVEL = logging.getLevelName(os.environ.get("LOG_LEVEL" , "DEBUG" )) JSON_LOGS = True if os.environ.get("JSON_LOGS" , "0" ) == "1" else False class InterceptHandler (logging.Handler): def emit (self, record ): try : level = logger.level(record.levelname).name except ValueError: level = record.levelno frame, depth = sys._getframe(6 ), 6 while frame and frame.f_code.co_filename == logging.__file__: frame = frame.f_back depth += 1 logger.opt(depth=depth, exception=record.exc_info).log( level, record.getMessage() ) def setup_logging (): logging.root.handlers = [InterceptHandler()] logging.root.setLevel(LOG_LEVEL) for name in logging.root.manager.loggerDict.keys(): logging.getLogger(name).handlers = [] logging.getLogger(name).propagate = True logger.configure(handlers=[{"sink" : sys.stdout, "serialize" : JSON_LOGS}]) if __name__ == "__main__" : server = Server( Config( "my_app.app:app" , host="0.0.0.0" , log_level=LOG_LEVEL, ), ) setup_logging() server.run()
使用 loguru 替换内置的 logging 模块
loguru 的使用 建议直接参考 loguru官方文档 ,这里我就不多写了