【Python】ログ出力をちょっと良くするライブラリ3種類【logzero・loguru・pylogrus】


pythonのログ出力用のライブラリの紹介です。標準のlogging以上に簡単・便利にログ出力を管理できるライブラリlogzero、loguru、pylogrusの3種類のサンプルコードとその使い方を紹介します。

# サンプルコード

各ライブラリのサンプルコードを用意しました。後述の使い方と合わせて参考にしてください。

- logzero
    - [基本的なサンプル](https://github.com/k8shiro/PythonLogginLibraries/blob/master/src/logzero_sample/sample1.py)
    - [機能全般のサンプル](https://github.com/k8shiro/PythonLogginLibraries/blob/master/src/logzero_sample/sample2.py)
- loguru
    - [基本的なサンプル](https://github.com/k8shiro/PythonLogginLibraries/blob/master/src/loguru_sample/sample1.py)
    - [機能全般のサンプル](https://github.com/k8shiro/PythonLogginLibraries/blob/master/src/loguru_sample/sample2.py)
- pylogrus
    - [基本的なサンプル](https://github.com/k8shiro/PythonLogginLibraries/blob/master/src/pylogrus_sample/sample1.py)
    - [機能全般のサンプル](https://github.com/k8shiro/PythonLogginLibraries/blob/master/src/pylogrus_sample/sample2.py)

# logzero

ドキュメント: https://github.com/metachris/logzero, https://logzero.readthedocs.io/en/latest/

loggingの代わりに使える系のライブラリ。loggingを簡単に扱えるようにしたもの。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86684/86af30ab-fc7d-fa92-78e3-0eae3201007e.png)

## logzeroの使い方

### デフォルト設定で使う

```
from logzero import logger

logger.trace("sample trace level log message")
logger.debug("sample debug level log message")
logger.info("sample info level log message")
logger.warning("sample warn level log message")
logger.error("sample error level log message")
logger.critical("sample critical level log message")
```

### コンソール出力ログレベルの設定

```
logzero.loglevel(logging.INFO)
```

### ログファイル出力設定
```
# 出力設定
logzero.logfile(
    '/var/log/logzero.log',  # ログファイルpath
    loglevel=logging.ERROR,   # ファイルに出力されるログレベル
    maxBytes=1e6,            # 最大ファイルサイズ
    backupCount=3            # 保持する老ファイルの世代数
)

# 出力設定の無効化
logzero.logfile(None)
```

### ログのフォーマット

`%(color)s`から`%(end_color)s`までの部分にログレベルに合わせて色がつきます

```
log_format = '%(color)s[%(levelname)1.1s %(asctime)s %(name)s %(module)s %(funcName)s:%(lineno)d]%(end_color)s %(message)s'
formatter = logzero.LogFormatter(fmt=log_format)
logzero.formatter(formatter)
```

### logger instanceの作成

```
# logger instanceの作成
log_format = '%(color)s[%(levelname)1.1s %(asctime)s %(name)s %(module)s %(funcName)s:%(lineno)d]%(end_color)s %(message)s'
formatter = logzero.LogFormatter(fmt=log_format)
custom_logger = logzero.setup_logger(
    name=__name__,
    logfile="/var/log/logzero.log",
    formatter=formatter,
    maxBytes=1000000,
    backupCount=3,
    level=logging.INFO,
    fileLoglevel=logging.ERROR,
    disableStderrLogger=False,
)

# ログ出力
custom_logger.debug("sample class custom logger debug level log message")
custom_logger.info("sample class custom logger info level log message")
custom_logger.warning("sample class custom logger warn level log message")
custom_logger.error("sample class custom logger error level log message")
```

# loguru

ドキュメント: https://github.com/Delgan/loguru, https://loguru.readthedocs.io/en/stable/index.html

loggingとは使い勝手が異なるが簡単かつ直感的に扱える。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86684/5647000f-5057-1761-ba01-c6eedd9cb2fe.png)

## loguruの使い方
### ログ出力先を追加する
デフォルトではstderrにのみ出力される。ファイルへの出力も行う場合以下のように`logger.add`する。

```
logger.add("/var/log/loguru_sample2.log")
```

ログのローテーション等の設定は

```
logger.add("file_1.log", rotation="500 MB")    # ファイルサイズでローテーション
logger.add("file_2.log", rotation="12:00")     # 毎日指定時刻でローテーション
logger.add("file_3.log", rotation="1 week")    # 1週間ごとにローテーション
logger.add("file_A.log", retention=3)  # 3世代保持する
logger.add("file_B.log", retention="10 days")  # 10日保持する
logger.add("file_Y.log", compression="zip")    # zipで圧縮する
```

その他、設定に関してはhttps://loguru.readthedocs.io/en/stable/api/logger.html#file を参照してください。
この`logger.add`にはlogのlevel等いくつかの設定可能なパラメータがあります。詳細はhttps://loguru.readthedocs.io/en/stable/api/logger.html?highlight=logger.add#loguru._logger.Logger.add を参考にしてください。※ diagnose, backtraceはFalseの設定をおすすめします。

- logger.addの例

```
logger.add(
    "/var/log/sample.log",
    rotation="12:00",
    format=log_format,
    diagnose=False,
    backtrace=False,
    level=LOG_LEVEL
)
```

### フォーマット設定
`<green>～～～</green>`のように記述し、ログに色を付けられます。
`<level>`で深刻度により色を変えられます。
ログメッセージはrecordと関連付けられます。recordは辞書型でkeyを`{message}`のようにformatに埋め込むことで、
ログに埋め込めます。どのようなkeyを持っているかは
https://loguru.readthedocs.io/en/stable/api/logger.html
の「The record dict」を参照してください。

```
log_format = "<green>{time:YYYY-MM-DDTHH:mm:ss}</green> <level>{level} {message}</level>"

# コンソール出力を設定したい場合、デフォルトの設定を先に消す必要があります。
# 以前にlogger.addしたものも消えるので注意
logger.remove()
logger.add(sys.stderr, format=log_format)
```

### recordにない値をフォーマットに追加する
フォーマットで`{extra[extra_value]}`のようにextraのdictとして定義し、bindかメッセージの引数として渡せます。

```
logger.remove()
log_format = "<green>{time}</green>: <level>{level} {message}</level>: {extra[extra_value]}"
logger.add(
    sys.stdout,
    format=log_format,
    serialize=True
)

logger.bind(extra_value="some_extra_value").info("serialize message 01")
logger.info("serialize message 02", extra_value="some_extra_value")
```

### JSONで出力する
`serialize=True`として出力をjsonにすることができます。

```
logger.remove()
log_format = "<green>{time}</green>: <level>{level} {message}</level>"
logger.add(
    sys.stdout,
    format=log_format,
    serialize=True
)
```

### Exceptionのloglevelを変える

```python
try:
    raise ValueError("error!")
except ValueError as e:
    logger.opt(exception=True).critical("Exceptionのloglevelを変える")
```

### ログの一部に色を付ける

```python
logger.remove()
logger.add(sys.stderr, format="{time} {level} {message}", level="INFO")
logger.opt(colors=True).info("ログに色をつける <blue>colors</blue>")
```

### recordに格納されている情報をlogに付与する
ログメッセージは`record`と関連付けられます。`record`は辞書型でどういったkeyを持っているかは
https://loguru.readthedocs.io/en/stable/api/logger.html
の「The record dict」を参照してください。

```python
logger.opt(record=True).info("recordに格納されている情報をlogに付与する (eg. {record[thread]})")
```

### フォーマットを無視してログ出力

```
logger.opt(raw=True).info("フォーマットを無視してログ出力\n")
```

### 親(呼び出し元)の情報をログに表示する
ログメッセージで使用されるrecord情報が親(呼び出し元)のものになります。
以下の例の場合、`child_func()`内でログを出力していますが、`2020-05-30T18:54:32.505956+0000 INFO 親の情報をログに表示する parent_func`のように`{function}`にparent_funcが入ります。

```
logger.remove()
logger.add(sys.stderr, format="{time} {level} {message} {function}", level="INFO")
def child_func():
    logger.opt(depth=1).info("親の情報をログに表示する")

def parent_func():
    child_func()
parent_func()
```

### messageのみに変数を使用する
通常、ログメッセージに変数を使用した場合、その値は`record['extra']`に格納される。`capture=False`にした場合格納されなくなる。

```
logger.remove()
logger.add(sys.stderr, format="{time} {level} {message} {function}", level="INFO", serialize=True)
logger.opt(capture=False).info("{dest} messageのみに変数を使用する", dest="extra")
```

出力例

```
# capture=Falseの場合
{
    ...
    "record": {
        ...
        "extra": {},
        ...
        "message": "Keyword arguments not added to extra dict",
        ...
    }
}

# capture=Trueの場合
{
    ...
    "record": {
        ...
        "extra": {"dest": "extra"},
        ...
        "message": "Keyword arguments not added to extra dict",
        ...
    }
}
```

### lazy: 重い関数実行を制御する
適切なloglebelの時のみ関数を実行するようにします。

```
def test_lazy():
    print('exec test_razy')
    return 'exec test_razy'

logger.remove()
logger.add(sys.stderr, format="{time} {level} {message}", level="INFO")

# 表示するloglevelはINFOに設定している。
# この時lazy=False(デフォルト)、では以下のDEBUG levelのログは表示されないが、test_lazy()関数は実行されてしまう。
logger.opt(lazy=False).debug("DEBUG LEVEL LOG: {x}", x=test_lazy())

# lazy=Trueとし、以下のようにlambdaを使用する。
# この場合、ログは出力されず、test_lazy()も実行されない。
logger.opt(lazy=True).debug("DEBUG LEVEL LOG: {x}", x=lambda: test_lazy())

# DEBUGレベルに変更し、同じく以下のように実行した場合
# lambdaの実行結果がxに格納される
logger.remove()
logger.add(sys.stderr, format="{time} {level} {message}", level="DEBUG")
logger.opt(lazy=True).debug("DEBUG LEVEL LOG: {x}", x=lambda: test_lazy())
```

# pylogrus

ドキュメント: https://github.com/vmig/pylogrus

golangの[logrus](https://github.com/sirupsen/logrus)風に使えるようにするloggingを拡張するライブラリ。基本的な使い方はlggingと同じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86684/57beb1fc-217c-2971-9137-3b6d2046aaba.png)

## pylogrusの使い方
### 基本的な出力とファイルへの出力
`logging.setLoggerClass(PyLogrus)`を最初に実行することと、pylogrusのTextFormatterを使うこと以外はloggingと同じ。

```
import logging
from pylogrus import PyLogrus, TextFormatter

logging.setLoggerClass(PyLogrus)
logger = logging.getLogger(__name__)  # type: PyLogrus
logger.setLevel(logging.DEBUG)

formatter = TextFormatter(datefmt='Z', colorize=True)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(formatter)
logger.addHandler(ch)

ch = logging.FileHandler('/var/log/py_logrus_sample2.log')
ch.setLevel(logging.ERROR)
ch.setFormatter(formatter)
logger.addHandler(ch)

logger.debug("DEBUG MESSAGE")
logger.info("INFO MESSAGE")
logger.warning("WARNING MESSAGE")
logger.error("ERROR MESSAGE")
```

### メッセージにPrefixをつける

```
# [2020-05-31T13:12:14.428Z]    DEBUG [API] DEBUG MESSAGE
# のように出力される
logger = logger.withPrefix("[API]")
logger.debug("DEBUG MESSAGE")
```

### メッセージにフィールドを追加する

```
# [2020-05-31T13:12:14.428Z]     INFO INFO MESSAGE; error_code=404
# のように出力される
logger.withFields({'error_code': 404}).info("INFO MESSAGE")
```

### JSON形式で出力
有効化するフィールドをしJsonFormatterを使用する。

```
enabled_fields = [
    ('name', 'logger_name'),
    ('asctime', 'service_timestamp'),
    ('levelname', 'level'),
    ('threadName', 'thread_name'),
    'message',
    ('exception', 'exception_class'),
    ('stacktrace', 'stack_trace'),
    'module',
    ('funcName', 'function')
]
formatter = JsonFormatter(datefmt='Z', enabled_fields=enabled_fields, indent=2, sort_keys=True)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(formatter)
logger.addHandler(ch)

logger.debug("DEBUG MESSAGE")
```

# まとめ

logzero、loguru、pylogrusの3種類を紹介しました。pythonには標準のloggingがありますが、簡単にちょっといい感じのログ出力をしたい時にこれらを使うとよいと思います。個人的にはloguruが扱いやすいと感じましたが、loggingに慣れているようであればlogzero、pylogrusの使用がおすすめです。