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 不僅僅如此,它還支援以下功能:

  1. Bound Loggers 讓開發者可以把任意的 key, value 寫到 log 中,不用自己設定格式
  2. Context Variables 讓開發者可以把一些 context 相關的值丟進 Context Variables 中,例如紀錄使用者的 Id, IP 等等,都可以寫在 Context Variables 之中,如此一來就不用每筆 log 都要再拿一次這些資訊
  3. 更豐富的輸出格式,例如可以將 log 改為 JSON 格式輸出,更加適合與 ELK, Logstash, Graylog 等服務整合
  4. 支援 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 就會出現以下執行畫面:

structlog-context.png

上述 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-exceptionsrich 套件的話, 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-better-exceptions.png

總結

structlog 是相當好用的 Python logging 套件,使用上不僅簡單,也設計很多 Developer friendly 的功能,向下相容也做的很好,理論上可以無痛/微痛換掉 Python logging 模組。

而且它也提供整合 OpenTelemetry, Django, Flask, Pyramid, Celery, Twisted 的解決方案,算是相當成熟的一個套件,推薦給大家!

Enjoy!

References

structlog 23.1.0 documentation

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!