跳转至

Log

包括我在内的大多数人,当编写小型脚本时,习惯使用print来debug,肥肠方便,这没问题,但随着代码不断完善,日志功能一定是不可或缺的,极大程度方便问题溯源以及甩锅,也是每个工程师必备技能

Python自带的logging我个人不推介使用,不太Pythonic,而开源的Loguru库成为众多工程师及项目中首选,本期将同时对loggingLoguru进行使用对比,希望有所帮助。

快速示例

logging中,默认的日志功能输出的信息较为有限

Python
import logging

logger = logging.getLogger(__name__) 

def main(): 
	logger.debug("This is a debug message") 
	logger.info("This is an info message") 
	logger.warning("This is a warning message") 
	logger.error("This is an error message") 

if __name__ == "__main__": 
	main()

输出(logging默认日志等级为warning,故此处未输出info与debug等级的信息)

Bash
WARNING:root:This is a warning message 
ERROR:root:This is an error message

再来看看loguru,默认生成的信息就较为丰富了

Python
1
2
3
4
5
6
7
8
9
from loguru import logger 
def main(): 
	logger.debug("This is a debug message") 
	logger.info("This is an info message") 
	logger.warning("This is a warning message") 
	logger.error("This is an error message") 
	
if __name__ == "__main__": 
	main()

提供了执行时间、等级、在哪个函数调用、具体哪一行等信息

格式化日志

格式化日志允许我们向日志添加有用的信息,例如时间戳、日志级别、模块名称、函数名称和行号

logging中使用%达到格式化目的

Python
1
2
3
4
5
6
7
8
9
import logging 
# Create a logger and set the logging level 
logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)s | %(module)s:%(funcName)s:%(lineno)d - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) 
logger = logging.getLogger(__name__) 
def main(): 
	logger.debug("This is a debug message") 
	logger.info("This is an info message") 
	logger.warning("This is a warning message") 
	logger.error("This is an error message")

输出

Bash
1
2
3
2023-10-18 15:47:30 | INFO | tmp:<module>:186 - This is an info message 
2023-10-18 15:47:30 | WARNING | tmp:<module>:187 - This is a warning message 
2023-10-18 15:47:30 | ERROR | tmp:<module>:188 - This is an error message

loguru使用和f-string相同的{}格式,更方便

Python
from loguru import logger 
logger.add( sys.stdout, level="INFO", format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} - {message}", )

日志保存

Python
1
2
3
4
5
6
7
import logging 

logging.basicConfig( level=logging.DEBUG, format="%(asctime)s | %(levelname)s | %(module)s:%(funcName)s:%(lineno)d - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", handlers=[logging.FileHandler(filename="/your/save/path/info.log", level=logging.INFO), logging.StreamHandler(level=logging.DEBUG), ], ) 

logger = logging.getLogger(__name__) def main(): logging.debug("This is a debug message") logging.info("This is an info message") 

logging.warning("This is a warning message") logging.error("This is an error message") if __name__ == "__main__": main()

但是在loguru中,只需要使用add方法即可达到目的

Python
from loguru import logger 

logger.add( 'info.log', format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} - {message}", level="INFO", ) 

def main(): 
	logger.debug("This is a debug message") 
	logger.info("This is an info message") 
	logger.warning("This is a warning message") 
	logger.error("This is an error message") 

if __name__ == "__main__": 
	main()

日志轮换

日志轮换指通过定期创建新的日志文件并归档或删除旧的日志来防止日志变得过大

logging中,需要一个名为 TimedRotatingFileHandler 的附加类,以下代码示例代表每周切换到一个新的日志文件 ( when=“WO”, interval=1 ),并保留最多 4 周的日志文件 ( backupCount=4 )

Python
import logging 
from logging.handlers import TimedRotatingFileHandler 

logger = logging.getLogger(__name__) 
logger.setLevel(logging.DEBUG) 
# Create a formatter with the desired log format 

formatter = logging.Formatter( "%(asctime)s | %(levelname)-8s | %(module)s:%(funcName)s:%(lineno)d - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) 
file_handler = TimedRotatingFileHandler( filename="debug2.log", when="WO", interval=1, backupCount=4 ) 
file_handler.setLevel(logging.INFO) 
file_handler.setFormatter(formatter) 
logger.addHandler(file_handler) 

def main(): 
	logger.debug("This is a debug message") 
	logger.info("This is an info message") 
	logger.warning("This is a warning message") 
	logger.error("This is an error message") 
	
if __name__ == "__main__": 
	main()

loguru中,可以通过将 rotationretention 参数添加到 add 方法来达到目的,如下示例,同样肥肠方便

Python
from loguru import logger

logger.add("debug.log", level="INFO", rotation="1 week", retention="4 weeks") 

def main(): 
	logger.debug("This is a debug message") 
	logger.info("This is an info message") 
	logger.warning("This is a warning message") 
	logger.error("This is an error message") 

if __name__ == "__main__": 
	main()

日志筛选

日志筛选指根据特定条件有选择的控制应输出与保存哪些日志信息

logging中,实现该功能需要创建自定义日志过滤器类

Python
import logging

logging.basicConfig( filename="test.log", format="%(asctime)s | %(levelname)-8s | %(module)s:%(funcName)s:%(lineno)d - %(message)s", level=logging.INFO, ) 

class CustomFilter(logging.Filter): 

def filter(self, record):
	return "Cai Xukong" in record.msg 
# Create a custom logging filter 
custom_filter = CustomFilter() 
# Get the root logger and add the custom filter to it 
logger = logging.getLogger()
logger.addFilter(custom_filter)

def main(): 
	logger.info("Hello Cai Xukong") 
	logger.info("Bye Cai Xukong") 
if __name__ == "__main__": 
	main()

loguru中,可以简单地使用lambda函数来过滤日志

Python
1
2
3
4
5
6
7
8
from loguru import logger

logger.add("test.log", filter=lambda x: "Cai Xukong" in x["message"], level="INFO")
def main(): 
	logger.info("Hello Cai Xukong") 
	logger.info("Bye Cai Xukong") 
if __name__ == "__main__": 
	main()

捕获异常

logging中捕获异常较为不便且难以调试,如

Python
import logging 

logging.basicConfig( level=logging.DEBUG, format="%(asctime)s | %(levelname)s | %(module)s:%(funcName)s:%(lineno)d - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) 

def division(a, b): 
	return a / b 
	
def nested(c): 
	try: 
		division(1, c) 
	except ZeroDivisionError: 
		logging.exception("ZeroDivisionError") 

if __name__ == "__main__": 
	nested(0)
Python
Traceback (most recent call last): File "logging_example.py", line 16, in nested division(1, c) File "logging_example.py", line 11, in division return a / b ZeroDivisionError: division by zero

上面输出的信息未提供触发异常的c值信息,而在loguru中,通过显示包含变量值的完整堆栈跟踪来方便用户识别

Python
from loguru import logger

def division(a, b): 
	return a / b 

def nested(c): 
	try: 
		division(1, c)
	except ZeroDivisionError:
		logger.exception("ZeroDivisionError") 
		
if __name__ == "__main__": 
	nested(0)

值得一提的是,loguru中的catch装饰器允许用户捕获函数内任何错误,且还会标识发生错误的线程

Python
from loguru import logger 

def division(a, b): 
	return a / b 

@logger.catch 
def nested(c): 
	division(1, c) 
	
if __name__ == "__main__": 
	nested(0)

OK,作为普通玩家以上功能足以满足日常日志需求,通过对比loggingloguru应该让大家有了直观感受,哦对了,loguru如何安装?

Bash
pip install loguru