未經允許,禁止轉載。
本文只是列出來我自己在實踐中總結出來的幾點,並不一定最佳。當然,我也不認爲有統一的最佳。
記錄日誌是程序中尤其是 web 服務中的重要一環,恰到好處的日誌記錄可以幫助我們瞭解程序運行情況以及
方便排(shuai)錯(guo)。
logger 和 handler
如果使用 logging 不多,可能對 logger
和 handler
這兩個概念不熟,大多數還是直接使用 logging.info()
來記錄日誌。
Python 官方給了一個流程圖來說明日誌消息(LogRecord)在 logger 和 handler 之間的流動情況:
總體來說,我們創建一個 logger,通過 logger.info()
來通知 handler,讓 handler(如在終端輸出日誌的 StreamHandler,完整 handler 見 Useful Handlers)來執行真正的記錄操作。
打個比方,logger 好比領導,handler 好比員工。默認情況下你都用 logging.info()
來記錄日誌,相當於 CEO 直接命令所有員工做事。而有了 logger,通常我們會配置模塊級的 logger(下文會細說),再使用 logger.info()
來記錄日誌,這就相當於公司拆分成若干個部門,每個部門領導(模塊級 logger)只管自己部門內的員工做事。這樣每個事情誰做的更爲明確,也可分別對不同部門進行不同的管理(配置)。
格式化日誌
logging 的默認日誌格式是這樣的:%(levelname)s:%(name)s
。完整的屬性列表見 LogRecord attributes。
例如:
>>> logging.warning("這是一條 WARNING 消息。")
WARNING:root:這是一條 WARNING 消息。
然而這樣的格式是完全不夠的。除此之外你可能還想知道時間,那麼你可以加上 %(asctime)s
,時間格式默認爲 %Y-%m-%d %H:%M:%S,uuu
,通常這已經能夠滿足要求,如果不能,那你可以通過自定義 datefmt
來定義自己的 logging.Formatter
。
例如:
>>> logging.basicConfig(format="%(asctime)s - %(levelname)s: %(message)s")
>>> logging.warning("這是一條 WARNING 消息。")
2019-12-17 14:22:07,639 - WARNING: 這是一條 WARNING 消息。
>>> logging.basicConfig(format="%(asctime)s - %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S") # 和默認的區別在於不會輸出毫秒
>>> logging.warning("這是一條 WARNING 消息。")
2019-12-17 14:24:37 - WARNING: 這是一條 WARNING 消息。
如果要將全部的屬性全部記錄,那麼就是如下的樣子:
# logging_test.py
import logging
logging.basicConfig(
format="%(asctime)s - %(created)f - %(filename)s - %(funcName)s - %(levelname)s - %(levelno)s - %(lineno)d - %(module)s - %(msecs)d - %(name)s - %(pathname)s - %(process)d - %(processName)s - %(relativeCreated)d - %(thread)d - %(threadName)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
def main():
logging.warning("這是一條 WARNING 消息。")
if __name__ == "__main__":
main()
運行 logging_test.py
就會看到:
2019-12-17 14:40:21 - 1576564821.762118 - test_logging.py - main - WARNING - 30 - 9 - test_logging - 762 - root - /data/liyajun/projects/jindu_v3_dev/test_logging.py - 36431 - MainProcess - 0 - 139905813096192 - MainThread: 這是一條 WARNING 消息。
設置恰當的日誌等級
logging 共有 5 種日誌等級,每種等級都有等級名稱和對應的數值,嚴重性由高到低分別爲:
等級 | 數值 |
---|---|
CRITICAL |
50 |
ERROR |
40 |
WARNING |
30 |
INFO |
20 |
DEBUG |
10 |
嚴格上來說,還有第六種等級:NOTSET,數值爲 0,即所有消息都會輸出。但這並不是一種有效等級。
這些等級決定了會輸出哪些日誌,例如如果你設置了等級爲 INFO
,那麼比 INFO
更「安全的」DEBUG
日誌便不會輸出。這通常表示了你希望該日誌有多詳細。
有兩個地方需要設置等級:logger 和 handler。logger 和 handler 的默認等級都是 NOTSET,但是需要注意的是,如果發現當前 logger 的等級是 NOTSET,那麼會自動往上尋找其父級 logger 的等級,直到尋找到一個等級不是 NOTSET 的 logger。
更刺激的來了,root logger 的默認等級是 WARNING。😁
也就是說,默認情況下,只有 WARNING 等級往上的消息纔會輸出:
import logging
logging.debug("這是一條 DEBUG 消息。")
logging.info("這是一條 INFO 消息。")
logging.warning("這是一條 WARNING 消息。")
logging.error("這是一條 ERROR 消息。")
logging.critical("這是一條 CRITICAL 消息。")
#################### 輸出 ####################
# WARNING:root:這是一條 WARNING 消息。
# ERROR:root:這是一條 ERROR 消息。
# CRITICAL:root:這是一條 CRITICAL 消息。
如果你只想顯示 INFO
以上的消息,那麼只需使用 basicConfig
設置即可:
import logging
logging.basicConfig(level=logging.INFO)
logging.debug("這是一條 DEBUG 消息。")
logging.info("這是一條 INFO 消息。")
logging.warning("這是一條 WARNING 消息。")
logging.error("這是一條 ERROR 消息。")
logging.critical("這是一條 CRITICAL 消息。")
#################### 輸出 ####################
# INFO:root:這是一條 INFO 消息。
# WARNING:root:這是一條 WARNING 消息。
# ERROR:root:這是一條 ERROR 消息。
# CRITICAL:root:這是一條 CRITICAL 消息。
使用恰當的日誌等級
不要一味使用 logger.info()
來記錄所有日誌。
- 只是便於 debug 的詳細日誌使用
debug()
,如某變量的值 - 常規通知性信息使用
info()
,如某接口有新請求 - 有可能非正常的程序行爲(如不推薦的輸入)但是又不至於程序崩潰報錯,使用
warning()
,如某變量的值可能不正確時,以便後期排查 - 有非正常的程序行爲而且會導致操作不能正確執行或不能正確返回結果時,使用
error()
或者exception()
,如除零錯誤 - 非常嚴重的錯誤,嚴重到服務或應用崩潰,如未能捕獲的異常,通常此時服務已經崩潰了。這個用的比較少
記錄 traceback
什麼是 traceback?
如果你還不知道,它就是程序出錯時你看到的報錯信息,類似下面這種你估計很熟悉了:
Traceback (most recent call last):
File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'tensorflow'
根據文檔:
Traceback objects represent a stack trace of an exception. A traceback object is implicitly created when an exception occurs, and may also be explicitly created by calling
types.TracebackType
.
即表示了異常的堆棧追蹤信息。這在程序出錯時是非常重要的,不然你都不知道哪裏出錯了。同樣在日誌中也是必須要記錄的。
通常我們會使用 try/except
來捕獲異常,並在 except
中記錄一下,但如果我們只是使用 logging.error(msg)
來記錄,那麼 traceback 是不會輸出的,這樣一來就很不方便排錯了。
解決方法是我們可以加上 exc_info=True
來輸出異常:
logging.error(msg, exc_info=True)
此外,還有個更爲方便的函數:loging.exception(msg)
,會默認輸出 traceback。
如有必要,把日誌輸出到文件
如果沒有進行任何配置,或者使用 basicConfig
進行配置,那麼就只有一個 StreamHandler
,即日誌消息默認輸出到終端。
日誌的意義就在於查詢和排錯,那麼只輸出到終端很明顯不滿足這個需求,我們需要將其持久化保存,保存到文件中。
我們可以使用 FileHandler
、RotatingFileHandler
或 TimedRotatingFileHandler
來將日誌輸出到文件。
其實除此此之外,還有其他 handler,例如常見的
StreamHandler
、NullHandler
和WatchedFileHandler
等。才疏學淺,在此就不再誤人子弟了。
這三個的一個重要區別是:FileHandler
是將日誌全部輸出到一個文件中,這會造成日誌文件越來越大的問題。而後兩個是將日誌按照某種規則輸出到多個文件中,可以緩解單個日誌文件過大的問題。
以 TimedRotatingFileHandler
爲例,該 handler 可以在指定時間點創建新日誌文件。有如下參數可供配置:
filename
:日誌文件名when
、interval
和atTime
:[共同](https://docs.python.org/3/libr ary/logging.handlers.html#logging.handlers.TimedRotatingFileHandler)決定何時創建新日誌文件backupCount
:保留多少份舊日誌文件encoding
:編碼delay
:是否在第一次往文件中寫日誌時纔打開文件,默認False
utc
:是否使用 UTC 時間,默認False
例如我們想要每天的凌晨 0 點創建新日誌文件,文件名爲 app.log
,UTF-8 編碼,保留最新的 7 份舊日誌文件:
import logging
import logging.handlers
fh = logging.handlers.TimedRotatingFileHandler('app.log', when='midnight', backupCount=7, encoding='utf8')
logging.basicConfig(
format="%(asctime)s %(levelname)s %(message)s",
level=logging.DEBUG,
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[fh],
)
logging.info("這是一條 INFO 消息。")
使用模塊級的 logger
如果項目文件很多,有時我們可能希望在輸出日誌的同時,能夠直到每條日誌是由哪個文件(模塊)記錄的。我們可以通過模塊級的日誌來實現,只需要在每個模塊的最上方引入如下語句創建一個 logger,並在 logging format 中加上 %(name)s
即可:
logger = logging.getLogger(__name__)
關於 __name__
,根據官方文檔:
A module is a file containing Python definitions and statements. The file name is the module name with the suffix
.py
appended. Within a module, the module’s name (as a string) is available as the value of the global variable__name__
.
這樣就會創建一個以該模塊名命名的 logger。當然你也可以自己隨便取名,只不過使用 __name__
更方便。
然後在接下來記錄日誌時使用 logger.info()
等方法即可。
例如,有如下文件結構:
|app.py
|package_a
| module_a.py
# app.py
import logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s:%(message)s')
from package_a import module_a
logger = logging.getLogger(__name__)
logger.warning('來自 app 的日誌。')
# module_a.py
import logging
logger = logging.getLogger(__name__)
logger.warning('來自 module_a 的日誌')
$ python app.py
2019-12-24 21:53:21,915 - package_a.module_a - WARNING:來自 module_a 的日誌
2019-12-24 21:53:21,916 - __main__ - WARNING:來自 app 的日誌。
使用配置文件
根據官方文檔,開發者有三種方式來配置 logging:
- 使用 Python 代碼顯式創建 logger、handler 和 formatter,如
logging.basicConfig()
。 - 創建一個 logging 配置文件,然後用
logging.config.fileConfig()
讀取。 - 創建一個字典形式的 logging 配置,然後傳給
logging.config.dictConfig()
。
使用配置文件的方法(2 和 3)相比直接在程序中顯示創建(1)有一些優勢,例如配置和代碼分離,非開發者也可以很容易地修改配置,也方便後人維護。
logging.config.fileConfig()
只能接受 configparser 格式的 .ini
文件,例如:
[loggers]
keys=root,simpleExample
[handlers]
keys=consoleHandler
[formatters]
keys=simpleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler
[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
Python 3.2 加入的 logging.config.dictConfig()
使得配置 logging 的方式更加靈活,只需傳入 dict 即可,而以什麼文件格式定義這個 dict 不受限制,.py
、.json
或者 .yml
均可。其中 .yml
格式可讀性較強,像 Python 一樣沒有那麼多符號累贅,修改起來也容易。缺點是讀取 .yml
文件需要第三方的 PyYAML
,而 .json
則有內置的標準庫 json
。
此處以 .yml
爲例,說明如何用 logging.config.dictConfig()
來配置 logging。
假設我們有如下需求:
- 日誌同時輸出到終端和文件,終端採用 StreamHandler,志等級爲 INFO。文件採用 TimedRotatingFileHandler,每天凌晨 rollover,UTF8 編碼,文件名爲 app.log,日誌等級爲 DEBUG
- 日誌格式爲 %(asctime)s - %(name)s - %(levelname)s: %(message)s
- 使用模塊級 logger
那可以編寫如下配置文件:
# logging_config.yml
version: 1
formatters:
default:
format: "%(asctime)s - %(name)s - %(levelname)s: %(message)s"
handlers:
console:
class: logging.StreamHandler
level: INFO
formatter: default
file:
class: logging.handlers.TimedRotatingFileHandler
level: DEBUG
filename: log/app.log
when: midnight
encoding: utf-8
formatter: default
loggers:
app:
handlers: [console, file]
level: DEBUG
package_a.module_a:
handlers: [console, file]
level: DEBUG
disable_existing_loggers: false
下面我分別解釋下:
version
:int 類型,必有參數,目前的可用值只有1
formatters
:指定日誌格式handlers
:可定義多個 handler,每個 handler 都有一個名字,可以在下文使用,如例子中的console
和file
。每個 handler 都有自己的類型,使用class
指定,例如logging.StreamHandler
,每種類型的參數可以直接用key: value
的方式直接指定loggers
:和handlers
結構類似,指定多個 logger。這裏需要注意的是,logger 是有層級的,以.
分隔,與 Python 的 import 機制類似。所以會出現package_a.module_a
這種名字,但是實際上我們在代碼種創建 logger 時是不必這麼寫的,直接用__name__
就行disable_existing_loggers
:默認爲true
,禁用已存在的 logger。fileConfig
和dictConfig
默認都會如此,如果你發現有的日誌應該出現但是沒有出現,可將此設爲false
,如 gunicorn 就會默認被禁用,詳情可參見 gunicorn accesslog 爲空的一種可能解決辦法
然後在 app.py
程序中使用如下語句讀入配置,並刪除 logging.basicConfig()
語句,然後在各個模塊創建 logger 即可(完整代碼見 GitHub):
爲了保持簡潔,這裏就不放完整代碼了,完整代碼放在 GitHub。
with open("logging_config.yml", "r", encoding="utf8") as f:
logging_config = yaml.safe_load(f)
logging.config.dictConfig(logging_config)
logger = logging.getLogger('app')
注意如果使用 gunicorn 來啓動 flask 服務,那麼在
app.py
種使用logger = logging.getLogger(__name__)
即可,不必寫成app
。此處寫成app
是假設直接運行python app.py
來啓動程序,此時__name__
爲__main__
,不是app
。
那麼運行 python app.py
可看到終端只輸出了 INFO 等級以上的日誌:
$ python app.py
2019-12-29 20:20:51,231 - app - WARNING: 來自 app 的 WARNING 日誌。
2019-12-29 20:20:51,232 - package_a.module_a - WARNING: 來自 module_a 的 WARNING 日誌
log/app.log
輸出了 DEBUG 等級以上的日誌:
2019-12-29 20:20:51,231 - app - DEBUG: 來自 app 的 DEBUG 日誌。
2019-12-29 20:20:51,231 - app - WARNING: 來自 app 的 WARNING 日誌。
2019-12-29 20:20:51,232 - package_a.module_a - DEBUG: 來自 module_a 的 DEBUG 日誌
2019-12-29 20:20:51,232 - package_a.module_a - WARNING: 來自 module_a 的 WARNING 日誌
如果程序運行時間超過一天,便可以看到 log/
下會有類似 app.log.2019-12-28
的日誌文件,後面會以日期結尾。
總結
總結起來,就是以下幾點:
- 按照自己的要求格式化日誌
- 設置恰當的日誌等級
- 使用恰當的日誌等級
- 記錄 traceback
- 日誌輸出到文件,且使用
RotatingFileHandler
或者TimedRotatingFileHandler
- 使用模塊級的 logger
- 使用配置文件來設置 logging
Reference
- logging — Logging facility for Python — Python 3.8.1rc1 documentation
- Logging HOWTO — Python 3.8.1rc1 documentation
- python - What is the default handler for the child logger? - Stack Overflow
- gunicorn accesslog 爲空的一種可能解決辦法_Alan Lee-CSDN博客
- logging - When to use the different log levels - Stack Overflow