Python 好用套件介紹 - structlog
Posted on Aug 30, 2023 in Python 模組/套件推薦 , Python 程式設計 - 初階 by Amo Chen ‐ 4 min read
大家初次使用 Python 的 logging 模組時,應該都跟我同樣困惑過,例如下列程式碼:
import logging
log = logging.getLogger(__name__)
log.info('Hello')
我們會預期上述程式碼要列印 Hello
的字串,然而事實是不會。
這是由於 Python logging 模組預設的 log level 為 WARNING
, 所以只有 WARNING
, ERROR
以及 CRITICAL
才會顯示。
所以使用 logging 模組通常都要額外做一些設定才行,這不免需要讀一下 Python 官方文件。(這不是 Python 的問題,只是設計哲學不同)
那麼,有沒有什麼套件比起內建的 logging 模組更直覺好用呢?
答案是「有」,那就是本文要介紹的 structlog 套件。
本文環境
- Python 3
- structlog
- rich
- better_exceptions
- flask
$ pip install structlog rich better_exceptions flask
structlog
structlog 是訴求簡單、好用、快速的 logging 套件,因此 structlog 預設不管 logging level, 會將 log 輸出至標準輸出(standard output),而且連格式都已經自動設定好,還會按照 log 等級標上不同顏色,例如:
>>> import structlog
>>> log = structlog.get_logger()
>>> log.info('Hello World')
2023-08-30 13:20:28 [info ] Hello World
>>> log.info('Error')
2023-08-30 13:20:32 [error ] Error
此外,為了讓 structlog 可以無痛取代原本的 logging 模組,所以 structlog 也跟 logging 模組一樣支援 %-format string
, 例如下列範例:
>>> log.info('Hello %s', 'Amo')
2023-08-30 13:21:31 [info ] Hello Amo
但其實使用 structlog 時,可以直接使用 f-string
即可,例如:
>>> name = 'Amo'
>>> log.info(f'Hello {name}')
當然, structlog 不僅僅如此,它還支援以下功能:
- Bound Loggers 讓開發者可以把任意的 key, value 寫到 log 中,不用自己設定格式
- Context Variables 讓開發者可以把一些 context 相關的值丟進 Context Variables 中,例如紀錄使用者的 Id, IP 等等,都可以寫在 Context Variables 之中,如此一來就不用每筆 log 都要再拿一次這些資訊
- 更豐富的輸出格式,例如可以將 log 改為 JSON 格式輸出,更加適合與 ELK, Logstash, Graylog 等服務整合
- 支援 asyncio
Bound logger
Bound Loggers 讓開發者可以把任意的 key, value 寫到 log 中,不用自己設定格式,如下列範例,可以隨意指定 key, value 到 log 相關的方法之中, structlog 會自動把 key=value
格式的字串附加到 log 之中,而且會自動按照字母順序排序,相當方便:
>>> log.info('Hello %s', 'Amo', current_value='Amo', foo='bar')
>>> 2023-08-30 13:23:56 [info ] Hello Amo current_value=Amo foo=bar
Context Variables
Context Variables 讓開發者可以把一些 context 相關的值丟進 Context Variables 中,例如紀錄使用者的 Id, IP 等等,都可以寫在 Context Variables 之中,如此一來就不用每筆 log 都要再拿一次這些資訊,例如以下的 Flask 範例,就是使用 Context Variable 做到更方便的 log :
import uuid
import structlog
from structlog.contextvars import (
bind_contextvars,
clear_contextvars,
merge_contextvars,
)
import flask
logger = structlog.get_logger()
app = flask.Flask(__name__)
@app.before_request
def setup_log():
clear_contextvars()
bind_contextvars(
request_id=str(uuid.uuid4()),
ip=flask.request.access_route[0],
)
def do_job():
log = logger.bind()
log.warning('Doing the job', func=do_job.__name__)
@app.route('/', methods=['GET'])
def index():
log = logger.bind()
log.info('GET /')
do_job()
return {'ok': True}
if __name__ == "__main__":
app.run()
上述範例程式執行起來之後,直接打開瀏覽器存取網址 http://127.0.0.1:5000
就會出現以下執行畫面:
上述 Flask 範例程式,我們在 app.before_request
中註冊一個函式,用來建立一個 context, 這個 context 中存著 request_id
以及 ip
2 個值,所以後續的 index()
以及 do_job()
可以呼叫 logger.bind()
取得 Context ,所以 log 時就會一併把 context 中的值輸出,是否感覺相當方便呢?
Context Variables 也很適合用來追巢狀(nested)多層的程式碼,可以看到每一層有什麼變化。
更多 Context Variables 可以參考 structlog.contextvars 文件 。
將 log 輸出為 JSON 格式
如果要將 structlog 的 log 格式輸出為 JSON, 只要將設定 structlog 的 processors 即可, processors 是 log 訊息的處理管道(pipelines), 設定是一個 list, log 訊息按照順序一個接著一個進行處理,所以開發者也可以客製化 processors 。
JSON 格式的設定如下:
import structlog
structlog.configure(
processors=[
structlog.processors.dict_tracebacks,
structlog.processors.JSONRenderer(),
],
)
log = structlog.get_logger()
log.info('Hi', value_1=1, valu2='2')
上述範例執行結果如下:
{"value_1": 1, "valu2": "2", "event": "Hi"}
可以看到 log 訊息會統一存在 event
這個 key 中,所以千萬別再帶入一個 event
的參數,否則 structlog 會顯示 TypeError: meth() got multiple values for argument 'event'
,至於剩下值的都會按照 key, value 的配對放到 Python dictionary 之中。
p.s. 有人可能會疑問 structlog.processors.dict_tracebacks
的作用,它的作用是將例外錯誤(exception)轉為字典(dictionary)形式,如此才能夠讓 JSONRenderer
順利輸出為 JSON 。
asyncio
以下範例取自官方文件,可以看到 structlog 也支援 asyncio, 只要將 info
, debug
, warning
, error
等方法加上 a
當前綴即可使用 asyncio 版的 log 方法:
import asyncio
>>> logger = structlog.get_logger()
>>> async def f():
... await logger.ainfo("async hi!")
整合 better-exceptions, rich 套件
如果有安裝 better-exceptions 與 rich 套件的話, structlog 還會將例外錯誤訊息以更好理解的方式輸出,因爲 structlog 作者認為所有的 log 都是要給人閱讀的,所以越清楚越容易理解越好,例如下列程式故意將數字除以 0, 以查看例外錯誤的 log 訊息:
import structlog
log = structlog.get_logger()
value = 1
try:
value / 0
except Exception as e:
log.exception(e)
上述範例執行結果如下圖,可以看到錯誤訊息變得很漂亮之外,也有提供 locals
紀錄當時的變數值,變得更好理解與除錯:
總結
structlog 是相當好用的 Python logging 套件,使用上不僅簡單,也設計很多 Developer friendly 的功能,向下相容也做的很好,理論上可以無痛/微痛換掉 Python logging 模組。
而且它也提供整合 OpenTelemetry, Django, Flask, Pyramid, Celery, Twisted 的解決方案,算是相當成熟的一個套件,推薦給大家!
Enjoy!
References
structlog 23.1.0 documentation