如何使用 loguru 接管程式的所有日誌輸出?

ponponon發表於2022-05-02

背景和痛點——日誌的要求:輸出格式統一

loguru 是多麼的優秀就不用我介紹了,我們自己的業務程式碼,可以輕鬆的 import loguru 來列印日誌,疑惑的一個統一的輸出

但是很多第三方庫整合的日誌模組是標準庫中的 logging,其 format 多種多樣

我希望可以實現:使用 loguru 接管所有庫的 logging,統一使用 loguru 的格式輸出

原因?因為方便日誌採集和解析儲存呀!比如對接 lokislselk 等等。

最佳實踐——我理想的日誌輸出格式:

我希望每條日誌輸出都長這樣

{
    "text": "2022-04-25 18:12:40.179 | ERROR    | __main__:<module>:18 - An error has been caught in function '<module>', process 'MainProcess' (60788), thread 'MainThread' (139849590506112):\nTraceback (most recent call last):\n\n> File \"/home/bot/Desktop/ideaboom/test_logger/loguru_contextualize.py\", line 18, in <module>\n    div(1,0)\n    └ <function div at 0x7f3143edf490>\n\n  File \"/home/bot/Desktop/ideaboom/test_logger/loguru_contextualize.py\", line 8, in div\n    return a/b\n           │ └ 0\n           └ 1\n\nZeroDivisionError: division by zero\n",
    "record": {
        "elapsed": { "repr": "0:00:00.007519", "seconds": 0.007519 },
        "exception": {
            "type": "ZeroDivisionError",
            "value": "division by zero",
            "traceback": true
        },
        "extra": {
            "context_id": "fd77b1d159224d939c1cc0d7a566d09b",
            "message_id": "8ad2c351ef3240998fc10a5015c591c1"
        },
        "file": {
            "name": "loguru_contextualize.py",
            "path": "/home/bot/Desktop/ideaboom/test_logger/loguru_contextualize.py"
        },
        "function": "<module>",
        "level": { "icon": "❌", "name": "DEUBG", "no": 40 },
        "line": 18,
        "message": "An error has been caught in function '<module>', process 'MainProcess' (60788), thread 'MainThread' (139849590506112):",
        "module": "loguru_contextualize",
        "name": "__main__",
        "process": { "id": 60788, "name": "MainProcess" },
        "thread": { "id": 139849590506112, "name": "MainThread" },
        "time": {
            "repr": "2022-04-25 18:12:40.179070+08:00",
            "timestamp": 1650881560.17907
        }
    }
}

對應的程式碼就是這樣:

from loguru import logger
from mark import BASE_DIR
import logging

logger.add(BASE_DIR/'run.log', serialize='json')

我希望 loguru 的日誌輸出到檔案,然後日誌收集程式收集日誌檔案。而不是標準輸出,我希望的標準輸出是給人類看的,不是給日誌收集程式的。

也就是說日誌輸出兩份:

  • 一個輸出到 stdoutstderror,可以通過 taildocker-compose logskubectl logs 等等命令直接檢視,給人類看(格式不需要統一,反正不統一人類也看的懂,並且這個就是簡單的 text,不是 josn 之類的);
  • 另一份輸出到 run.log 檔案,logtailpromtail 等等日誌收集程式就收集 run.log 中的日誌記錄。(擁有統一的日誌格式,並且一行就是一個 json)

以 json 輸出日誌的優點:

  • 方便採集,不存在因為格式問題導致的採集錯誤!也不用焦慮堆疊換行而需要使用行首正則匹配表示式等等
  • 方便解析,對接 esmongodb 很方便

缺點呢?

  • 體積膨脹,json 相比直接的 text,體積大了 n 倍

程式碼實現—— loguru 偷天換日,魚目混珠

建立一個 loggers.py 檔案,內容如下:

from loguru import logger
from mark import BASE_DIR
import logging

logger.add(BASE_DIR/'run.log', serialize='json')


class InterceptHandler(logging.Handler):
    def emit(self, record):
        # Retrieve context where the logging call occurred, this happens to be in the 6th frame upward
        logger_opt = logger.opt(depth=6, exception=record.exc_info)
        logger_opt.log(record.levelno, record.getMessage())


# logging.getLogger("uvicorn").setLevel(10)
# logging.getLogger("uvicorn").handlers = []
# logging.getLogger("uvicorn").addHandler(InterceptHandler())

# logging.getLogger("uvicorn.error").setLevel(10)
# logging.getLogger("uvicorn.error").handlers = []
# logging.getLogger("uvicorn.error").addHandler(InterceptHandler())

# logging.getLogger("fawn.error").setLevel(10)
# logging.getLogger("fawn.error").handlers = []
# logging.getLogger("fawn.error").addHandler(InterceptHandler())

logger_name_list = [name for name in logging.root.manager.loggerDict]
# logger_name_list = [name for name in logging.root.manager.loggerDict if '.' not in name]

for logger_name in logger_name_list:
    logging.getLogger(logger_name).setLevel(10)
    logging.getLogger(logger_name).handlers = []
    if '.' not in logger_name:
        logging.getLogger(logger_name).addHandler(InterceptHandler())


print(logger_name_list)
print(logging.root.manager.loggerDict)

現實中,比如 uvicornpeewee 都有自己的輸出格式,如下:

─➤  python api.py
INFO:     Started server process [2382519]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

使用上面的程式碼之後,就變成了:

─➤  uvicorn api:app                                                                                                   1 ↵
2022-04-30 23:09:11.283 | INFO     | uvicorn.server:serve:75 - Started server process [2395776]
2022-04-30 23:09:11.283 | INFO     | uvicorn.lifespan.on:startup:45 - Waiting for application startup.
2022-04-30 23:09:11.283 | INFO     | uvicorn.lifespan.on:startup:59 - Application startup complete.
2022-04-30 23:09:11.284 | INFO     | uvicorn.server:_log_started_message:206 - Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

解釋一下程式碼

上面這段程式碼你知道是什麼意思嗎?

todo

參考文章:
過濾器物件
how can i logging peewee with loguru
How To Override Uvicorn Logger in FastAPI using Loguru

相關文章