包括我在內(nèi)的大多數(shù)人,當(dāng)編寫小型腳本時,習(xí)慣使用print
來debug,肥腸方便,這沒問題,但隨著代碼不斷完善,日志功能一定是不可或缺的,極大程度方便問題溯源以及甩鍋,也是每個工程師必備技能
Python/ target=_blank class=infotextkey>Python自帶的logging
我個人不推介使用,不太Pythonic,而開源的Loguru
庫成為眾多工程師及項目中首選,本期將同時對logging
及Loguru
進(jìn)行使用對比,希望有所幫助
快速示例
在logging
中,默認(rèn)的日志功能輸出的信息較為有限
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默認(rèn)日志等級為warning
,故此處未輸出info與debug等級的信息)
WARNING:root:This is a warning message
ERROR:root:This is an error message
再來看看loguru
,默認(rèn)生成的信息就較為豐富了
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()
提供了執(zhí)行時間、等級、在哪個函數(shù)調(diào)用、具體哪一行等信息
格式化日志
格式化日志允許我們向日志添加有用的信息,例如時間戳、日志級別、模塊名稱、函數(shù)名稱和行號
在logging
中使用%
達(dá)到格式化目的
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")
輸出
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相同的{}
格式,更方便
from loguru import logger
logger.add(
sys.stdout,
level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} - {message}",
)
日志保存
在logging
中,實現(xiàn)日志保存與日志打印需要兩個額外的類,FileHandler
和 StreamHandler
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
方法即可達(dá)到目的
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()
日志輪換
日志輪換指通過定期創(chuàng)建新的日志文件并歸檔或刪除舊的日志來防止日志變得過大
在logging
中,需要一個名為 TimedRotatingFileHandler
的附加類,以下代碼示例代表每周切換到一個新的日志文件 ( when=“WO”, interval=1 ),并保留最多 4 周的日志文件 ( backupCount=4 )
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
中,可以通過將 rotation
和 retention
參數(shù)添加到 add
方法來達(dá)到目的,如下示例,同樣肥腸方便
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()
日志篩選
日志篩選指根據(jù)特定條件有選擇的控制應(yīng)輸出與保存哪些日志信息
在logging
中,實現(xiàn)該功能需要創(chuàng)建自定義日志過濾器類
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
函數(shù)來過濾日志
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
中捕獲異常較為不便且難以調(diào)試,如
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)
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
上面輸出的信息未提供觸發(fā)異常的c
值信息,而在loguru
中,通過顯示包含變量值的完整堆棧跟蹤來方便用戶識別
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
裝飾器允許用戶捕獲函數(shù)內(nèi)任何錯誤,且還會標(biāo)識發(fā)生錯誤的線程
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,作為普通玩家以上功能足以滿足日常日志需求,通過對比logging
與loguru
應(yīng)該讓大家有了直觀感受,哦對了,loguru
如何安裝?