日志是記錄軟件運行時發生事件的一種手段。事件有由一個開發者定義的重要程度;這個重要程度也可以叫做等級或者嚴重性。
何時使用日志
一些常見任務的最佳工具
任務 | 最佳工具 |
---|---|
展示普通用途的命令行腳本或程序的控制台輸出 | print() |
報告出現在程序正常操作中的事件(比如用於狀態監控或錯誤偵查) | logging.info(或者 logging.debug 用於診斷目的的詳細輸出) |
發布一個特定運行事件的警告 | logging.warning() |
報告一個特定運行事件的錯誤 | 拋出異常 |
報告一個抑制錯誤而不拋出異常(比如長期運行的服務進程的錯誤處理器) | logging.error(), logging.exception() 或者 logging.critical() |
日志的級別和適用情況
級別 | 適用情況 |
---|---|
DEBUG | 詳細信息,通常只在診斷問題時對其感興趣 |
INFO | 確認工作正常 |
WARNING | 表示發生了意料之外的事或者在不遠的將來會有問題(比如磁盤空間低)。軟件依然正常工作 |
ERROR | 由於一個更加嚴重的問題,軟件不能執行某些功能 |
CRITICAL | 嚴重的錯誤,表示程序可能不能繼續運行 |
組件
logging 庫提供了以下組件:日志記錄器、處理器、過濾器和格式化器。
- 日志記錄器暴露應用程序代碼可以直接使用的接口
- 處理器發送日志(由日志記錄器創建)到對應的目的地
- 過濾器篩選日志
- 格式化器決定最終輸出的日志的格式
通過調用 Logger 類的實例來記錄日志。每個實例都有一個名字,並且它們使用以點號作為分隔符的命名空間等級制度。比如,名字為 scan
的記錄器是 scan.text
、scan.html
、scan.pdf
的父親。記錄器的名字可以是任意值並且指明了日志信息產生的區域。
命名記錄器的一個好的慣例是使用模塊級別的記錄器,在每個要記錄日志的模塊中,以一下方式命名:
logger = logging.getLogger(__name__)
這意味着記錄器的名字追蹤了 包/模塊 的等級制度,並且可以很容易地從記錄器名字中發現日志的來源。
記錄器等級制度中的根叫做根記錄器。那是 logging.debug()
、logging.info()
等函數使用的記錄器。
目的地
可以將信息記錄到不同的目的地。目的地由處理器提供。在 logging 庫中支持將信息記錄到文件、HTTP GET/POST 地址、基於 SMTP 的 email等,詳細見 Useful Handlers。你也可以自定義日志目的地如果自帶的處理器類不能滿足你的特定需求。
日志記錄默認是沒有目的地(處理器)的。當你調用 logging
的 debug()
等函數時,它們會檢查處理器是否設置了目的地;如果沒有設置,它會自動調用 logging.basicConfig()
來設置。
basicConfig
logging.basicConfig
只有第一次設置才會生效,即第一次之后的設置不會覆蓋第一次設置。
logging.basicConfig
默認情況下給 root logger 添加一個默認格式的、目的地為控制台的處理器。
import logging
root = logging.getLogger()
root.handlers
[]
logging.basicConfig()
root.handlers
[<logging.StreamHandler object at 0x1010b68d0>]
流程圖
記錄器
記錄器有三種工作:
- 暴露函數給應用代碼
- 基於記錄器級別和記錄器上的過濾器決定哪些日志有效
- 將日志信息傳遞到相應的處理器
如果提供了名字,getLogger()
返回對應名字的記錄器示例的引用,否則,返回 root
。每個實例都有一個名字,並且它們使用以點號作為分隔符的命名空間等級制度。
import logging
foo = logging.getLogger('foo')
foo_bar = logging.getLogger('foo.bar')
foo_bar.parent.name == 'foo'
True
使用相同的名字多次調用 getLogger()
會返回同樣的記錄器對象:
logging.getLogger('foo') == foo
True
記錄器有一個有效級別的概念。如果一個記錄器沒有設置級別,那么記錄器使用它父親的有效級別。如果父親沒有設置級別,父親的父親會被測試,以此類推 -- 所有的祖先都會被搜索直到獲取一個級別。根記錄器總是有一個級別(默認是 WARNING
)。只有大於等於有效級別的日志才會被放行。
孩子記錄器會將信息傳遞給它父親的處理器。所以沒有必要為一個應用的所有記錄器設置處理器。設置一個最高級別的記錄器然后創建需要的孩子處理器就足夠了。也可以通過設置記錄器的 propagate
屬性來關閉傳播。
處理器
處理器負責分發日志到對應的目的地。記錄器對象可以有 0 或多個處理器對象。比如,一個應用可能想要發送所有的日志到一個日志文件,所有錯誤或以上級別的日志到標准輸出,所有 critical 的日志到郵件。這個場景需要三個單獨的處理器,每個處理器負責發送特定級別的信息到特定的目的地。
import logging
logger = logging.getLogger('example')
logger.setLevel(logging.DEBUG)
file_handler = logging.FileHandler('message.log')
file_handler.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.ERROR)
email_handler = logging.handlers.SMTPHandler(('smtp.163.com', 255), '發件人名稱', ['username@163.com', ], '主題', ('username@163.com', '授權碼'))
email_handler.setLevel(logging.CRITICAL)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
logger.addHandler(email_handler)
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')
流程是
- 日志級別是否大於等於日志器級別,如果是,那么進入下一步,否則,停止
- 依次將
LogRecord
傳遞到file_handler
、console_handler
、email_handler
,如果日志級別大於等於記錄器的日志級別,那么由記錄器將其送達目的地。
由於記錄器的級別設置為 DEBUG 級別,所以所有日志都能進入記錄器。文件處理器的級別設置為 DEBUG 級別,所以所有日志都會記錄到文件。控制台處理器的級別設置為 ERROR,所以只會顯示 ERROR 及以上級別的日志。郵件處理器也是同理。
其他
拋出異常與記錄日志
拋出異常:報告一個關於特定運行事件的錯誤
記錄日志:報告錯誤但不拋出異常(比如在一個長期運行的服務進程的錯誤處理器)
什么時候記錄日志,什么時候拋出異常?
Setry vs Logging
來自 https://sentry.io/vs/logging/
Logging 大部分是 info,之后少部分是 errors。Sentry 專注於異常 -- 捕捉應用的崩潰。Sentry 並不能代替 logs,並且 sentry 應該只儲存錯誤或者崩潰。
Logging 想要捕捉你所有的數據,並且給你最大的可審核性。當 computing rollups and transformations,sentry 會丟失很多數據。比如,sentry 不會儲存已存在錯誤的詳細情況。這意味着 sentry 不能保證讓你能精確地定位一個歷史錯誤。因為這個原因,當你想要追蹤事件時,你應該將它們儲存在你的 logging 設施中。
單例模式
在 logging 中用到的有:
basicConfig
,只有第一次設置才會生效getLogger()
使用相同的name
會返回同樣的實例
整個系統只需要一個對象進行操作。比如系統在任意時間都只需要一個 logger,如果有多個 logger 實例,那可能會出現日志重復等情況。
library logger
Configuring Logging for a Library 中認為應用的開發者知道他們的目標觀眾以及什么日志處理器最適合他們的應用:如果你在應用層之下添加了日志處理器,你很大程度上干涉了應用開發者分發適合他們需求的日志的能力。
通過給你的庫增加 NullHandler
(自從 Python 3.1),你可以阻止你的庫的日志輸出。
import logging
logging.getLogger('foo').addHandler(logging.NullHandler())
當你自己需要在你的庫中顯示日志時,可以使用一個函數來臨時使日志生效,比如 requests 中的
# env/lib/python2.7/site-packages/pip/_vendor/requests/packages/urllib3/__init__.py:57
def add_stderr_logger(level=logging.DEBUG):
"""
Helper for quickly adding a StreamHandler to the logger. Useful for
debugging.
Returns the handler after adding it.
"""
# This method needs to be in this __init__.py to get the __name__ correct
# even if urllib3 is vendored within another package.
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
logger.addHandler(handler)
logger.setLevel(level)
logger.debug('Added a stderr logging handler to logger: %s', __name__)
return handler
Logger.error or Logger.exception
Logger.exception() creates a log message similar to Logger.error(). The difference is that Logger.exception() dumps a stack trace along with it. Call this method only from an exception handler.
Exception handler 的意思是要在 except
中調用:
try:
1 / 0
except:
logging.exception('msg')
ERROR:root:msg
Traceback (most recent call last):
File "<ipython-input-9-63e73c36224b>", line 2, in <module>
1 / 0
ZeroDivisionError: integer division or modulo by zero
如果不在 except
中調用會 raise Empty
logging.exception('msg')
ERROR:root:msg
Traceback (most recent call last):
File "/Applications/PyCharm.app/Contents/helpers/pydev/pydevconsole.py", line 198, in process_exec_queue
code_fragment = interpreter.exec_queue.get(block=True, timeout=1/20.) # 20 calls/second
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/Queue.py", line 176, in get
raise Empty
Empty