【Python】ログファイルを『ファイルサイズでローテーション』するコード例 RotatingFileHandler

Python

Python の RotatingFileHandlerローテーティング ファイル ハンドラ で、ログファイルをローテーションして出力するコードれいです。

『ログ出力』と『ログのファイルサイズによるローテーション』は、RotatingFileHandler() を使うとできました。

(Python) class logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False, errors=None)

コード例です。

import logging, logging.handlers, pathlib
logger = logging.getLogger(__name__)

def main():
    root = logging.getLogger()
    root.setLevel(logging.DEBUG)

    log_txt = pathlib.Path(r'F:\apps\data\log.txt')
    rh = logging.handlers.RotatingFileHandler(
        log_txt, encoding='utf-8', mode='a',
        maxBytes=20, backupCount=5,
        )
    root.addHandler(rh)

    logger.info('あいうえお')
    logger.info('かきくけこ')
    logger.info('さしすせそ')
    return

if __name__ == '__main__':
    main()

実行結果です。

log.txt (ファイルサイズ 17 バイト)

さしすせそ

log.txt.1 (ファイルサイズ 17 バイト)

かきくけこ

log.txt.2 (ファイルサイズ 17 バイト)

あいうえお

ログファイルが自動的に分割されていきました。

以上です。

RotatingFileHandlerローテーティング ファイル ハンドラ の効果です。

ログファイルを自動で分割しながら、テキストファイルに出力することができました。

古いログも、自動でリネームして、ログファイルをローテーションして、残してくれました。

ログのファイルサイズ maxBytes を指定したら、だいたいそのくらいのサイズで、自動的に分割してくれました。

ログのファイルサイズは、文字コード(エンコーディング)によって変わりました。

ログのバックアップのかず backupCount を指定したら、その数だけ、分割後の古いログを残してくれました。

全体のログサイズは『ログサイズ×(最新ログ1個+バックアップN個)』くらいに収まりました。

古いログが『流れて消えていく仕組み』を作ることができました。

ログサイズの設定をデフォルトの maxBytes=0 にしたら、ログが分割されず、どこまでも大きくなっていきました。

ちょうど、普通の FileHandlerファイルハンドラ と同じような動作になりました。

以下、解説付きのコード例です。

コード例

ログファイルをローテーションしながら記録する Python コード例です。

RotatingFileHandler() を使用します。

OS は Windows 10 Pro (64 bit) で、Python 3.8.6 (64 bit) を使用しました。

"""RotatingFileHandler でログを記録する Python コード例です。"""
import logging, logging.handlers, pathlib

# (1/5) 名前付きロガーを取得します。
# 名前付きロガー取得時のロギングレベルは NOTSET (0) です。
logger = logging.getLogger(__name__)
# logger.setLevel(logging.NOTSET)

def main():
    """メイン関数です。"""
    # (2/5) ルートロガーを取得します。
    # ルートロガー取得時のロギングレベルは WARNING (30) です。
    root = logging.getLogger()
    root.setLevel(logging.DEBUG)
    # 最初は NOTSET / DEBUG / INFO あたりに設定しておきます。

    # (お好みで) ルートロガーにストリームハンドラを追加します。
    # どのハンドラも生成時のロギングレベルは NOTSET (0) です。
    sh = logging.StreamHandler()
    # sh.setLevel(logging.NOTSET)
    sh.setFormatter(logging.Formatter('%(name)s %(levelname)s: %(message)s'))
    root.addHandler(sh)

    # (3/5) ルートロガーにローテーティングファイルハンドラを追加します。
    # どのハンドラも生成時のロギングレベルは NOTSET (0) です。
    log_txt = pathlib.Path(r'F:\apps\data\log.txt')
    rh = logging.handlers.RotatingFileHandler(
        log_txt, encoding='utf-8', mode='a',
        maxBytes=100, backupCount=5,
        )
    # rh.setLevel(logging.NOTSET)
    rh.setFormatter(logging.Formatter('%(name)s %(levelname)s: %(message)s'))
    root.addHandler(rh)

    # (4/5) 自作関数を実行します。
    root.debug('start')
    root.info('%(name)s %(levelname)s: %(message)s')
    jisaku_func()
    root.debug('end')

    print('\n(デバッグ) ログのファイルサイズを取得してみます。')
    # ログファイルを列挙して、サイズを表示していきます。
    # p は Path の略です。
    for p in pathlib.Path(r'F:\apps\data').glob('*log.txt*'):
        if p.is_file():
            file_size = p.stat().st_size
            print(f'({p}) {file_size} [bytes]')

    print('\n(デバッグ) ロガーとハンドラのロギングレベルを確認します。')
    print(f'root.level: {logging.getLevelName(root.level)} ({root.level})')
    print(f'root.getEffectiveLevel(): {logging.getLevelName(root.getEffectiveLevel())} ({root.getEffectiveLevel()})')
    print(f'logger.level: {logging.getLevelName(logger.level)} ({logger.level})')
    print(f'logger.getEffectiveLevel(): {logging.getLevelName(logger.getEffectiveLevel())} ({logger.getEffectiveLevel()})')
    print(f'sh.level: {logging.getLevelName(sh.level)} ({sh.level})')
    print(f'rh.level: {logging.getLevelName(rh.level)} ({rh.level})')
    return

def jisaku_func():
    """自作関数です。"""
    # (5/5) 名前付きロガーでログを記録してみます。
    # (ログはルートロガーに取り付けたハンドラで記録されます)
    temperatures = [20, 25, 30, 35, 40, 45] # 架空の気温
    for t in temperatures:
        if t < 24:
            logger.debug(f'気温 {t} ℃')
        elif 24 <= t < 28:
            logger.info(f'暑さに注意 {t} ℃')
        elif 28 <= t < 31:
            logger.warning(f'暑さに警戒 {t} ℃')
        elif 31 <= t < 36:
            logger.error(f'暑さに厳重警戒 {t} ℃')
        elif 36 <= t:
            logger.critical(f'危険な気温です {t} ℃')
    return

if __name__ == '__main__':
    main()

実行結果

画面表示

コード例を実行したときの画面表示です。StreamHandler()print() 関数による表示です。

root DEBUG: start
root INFO: %(name)s %(levelname)s: %(message)s
__main__ DEBUG: 気温 20 ℃
__main__ INFO: 暑さに注意 25 ℃
__main__ WARNING: 暑さに警戒 30 ℃
__main__ ERROR: 暑さに厳重警戒 35 ℃
__main__ CRITICAL: 危険な気温です 40 ℃
__main__ CRITICAL: 危険な気温です 45 ℃
root DEBUG: end

(デバッグ) ログのファイルサイズを取得してみます。        
(F:\apps\data\log.txt) 66 [bytes]
(F:\apps\data\log.txt.1) 95 [bytes]
(F:\apps\data\log.txt.2) 81 [bytes]
(F:\apps\data\log.txt.3) 98 [bytes]

(デバッグ) ロガーとハンドラのロギングレベルを確認します。
root.level: DEBUG (10)
root.getEffectiveLevel(): DEBUG (10)
logger.level: NOTSET (0)
logger.getEffectiveLevel(): DEBUG (10)
sh.level: NOTSET (0)
rh.level: NOTSET (0)

ログファイル

ログファイルの内容です。

サイズは Windows のファイルのプロパティで確認したファイルサイズです。

log.txt サイズ: 66 バイト (66 バイト)

__main__ CRITICAL: 危険な気温です 45 ℃
root DEBUG: end

log.txt.1 サイズ: 95 バイト (95 バイト)

__main__ ERROR: 暑さに厳重警戒 35 ℃
__main__ CRITICAL: 危険な気温です 40 ℃

log.txt.2 サイズ: 81 バイト (81 バイト)

__main__ INFO: 暑さに注意 25 ℃
__main__ WARNING: 暑さに警戒 30 ℃

log.txt.3 サイズ: 98 バイト (98 バイト)

root DEBUG: start
root INFO: %(name)s %(levelname)s: %(message)s
__main__ DEBUG: 気温 20 ℃

だいたいですが、指定したサイズ maxBytes=100 のところで分割されていました。

各ファイルの各行の末尾には、Windows 用の改行コード CR+LF ('\r\n') がくっついていました。改行コード CR+LF のサイズは、1行あたり2バイトでした。

maxBytes=0 の実行結果

ログサイズの設定を、デフォルトのゼロ maxBytes=0 にしてみました。

    log_txt = pathlib.Path(r'F:\apps\data\log.txt')
    rh = logging.handlers.RotatingFileHandler(
        log_txt, encoding='utf-8', mode='a',
        maxBytes=0, backupCount=5,
        )

画面表示

maxBytes=0 に設定した時の実行結果です。

root DEBUG: start
root INFO: %(name)s %(levelname)s: %(message)s
__main__ DEBUG: 気温 20 ℃
__main__ INFO: 暑さに注意 25 ℃
__main__ WARNING: 暑さに警戒 30 ℃
__main__ ERROR: 暑さに厳重警戒 35 ℃
__main__ CRITICAL: 危険な気温です 40 ℃
__main__ CRITICAL: 危険な気温です 45 ℃
root DEBUG: end

(デバッグ) ログのファイルサイズを取得してみます。        
(F:\apps\data\log.txt) 340 [bytes]

(デバッグ) ロガーとハンドラのロギングレベルを確認します。
root.level: DEBUG (10)
root.getEffectiveLevel(): DEBUG (10)
logger.level: NOTSET (0)
logger.getEffectiveLevel(): DEBUG (10)
sh.level: NOTSET (0)
rh.level: NOTSET (0)

ログファイル

maxBytes=0 に設定した時のログファイルの内容です。

log.txt サイズ: 340 バイト (340 バイト)

root DEBUG: start
root INFO: %(name)s %(levelname)s: %(message)s
__main__ DEBUG: 気温 20 ℃
__main__ INFO: 暑さに注意 25 ℃
__main__ WARNING: 暑さに警戒 30 ℃
__main__ ERROR: 暑さに厳重警戒 35 ℃
__main__ CRITICAL: 危険な気温です 40 ℃
__main__ CRITICAL: 危険な気温です 45 ℃
root DEBUG: end

ログファイルは、分割されなかったです。

古いログが流れて、消えることもなかったです。

1つのファイルに、すべてのログを残すことができました。

maxBytes=0 の代わりに backupCount=0 を設定した場合も、同じ結果になりました。

ローテート専用のロガーを作って使用するコード例

通常のログファイルとは別に、ローテート専用のログファイルを作って記録していくコード例です。

ここでは『temperature ロガー』のログだけ、ローテート記録してみました。

"""
ローテート専用のロガーを作って使用する Python コード例です。
『名前付きロガー』のログだけをローテート記録してみました。
"""
import logging, logging.handlers, pathlib

# (1/5) 名前付きロガーを取得します。
# 名前付きロガー取得時のロギングレベルは NOTSET (0) です。
logger = logging.getLogger('temperature') # temperature は気温の意味です。
logger.propagate = False # ルートロガーへの伝搬を止めます(ログの2重出力防止)。
# logger.setLevel(logging.NOTSET)

def main():
    """メイン関数です。"""
    # (2/5) ルートロガーを取得します。
    # ルートロガー取得時のロギングレベルは WARNING (30) です。
    root = logging.getLogger()
    root.setLevel(logging.DEBUG)
    # 最初は NOTSET / DEBUG / INFO あたりに設定しておきます。

    # (お好みで) ルートロガーにストリームハンドラを追加します。
    # どのハンドラも生成時のロギングレベルは NOTSET (0) です。
    sh = logging.StreamHandler()
    # sh.setLevel(logging.NOTSET)
    sh.setFormatter(logging.Formatter('%(name)s %(levelname)s: %(message)s'))
    root.addHandler(sh)
    # (お好みで) ルートロガーに普通のファイルハンドラを追加します。
    # どのハンドラも生成時のロギングレベルは NOTSET (0) です。
    root_log_txt = pathlib.Path(r'F:\apps\data\root_log.txt')
    fh = logging.FileHandler(root_log_txt, mode='a', encoding='utf-8')
    # fh.setLevel(logging.NOTSET)
    fh.setFormatter(logging.Formatter('%(name)s %(levelname)s: %(message)s'))
    root.addHandler(fh)

    # (お好みで) 名前付きロガーにルートロガーと同じストリームハンドラを追加します。
    logger.addHandler(sh)
    # (3/5) 名前付きロガーにローテーティングファイルハンドラを追加します。
    # どのハンドラも生成時のロギングレベルは NOTSET です。
    rotate_log_txt = pathlib.Path(rf'F:\apps\data\{logger.name}_log.txt')
    rh = logging.handlers.RotatingFileHandler(
        rotate_log_txt, encoding='utf-8', mode='a',
        maxBytes=100, backupCount=5,
        )
    # rh.setLevel(logging.NOTSET)
    rh.setFormatter(logging.Formatter('%(name)s %(levelname)s: %(message)s'))
    logger.addHandler(rh)

    # (4/5) 自作関数を実行します。
    root.debug('start')
    root.info('%(name)s %(levelname)s: %(message)s')
    root.info(f'気温は『{logger.name} ロガー』に取り付けたハンドラで記録中...')
    root.info(f'({logger.name}) logger.propagate: {logger.propagate}')
    jisaku_func()
    root.debug('end')

    print('\n(デバッグ) ログのファイルサイズを取得してみます。')
    # ログファイルを列挙して、サイズを表示していきます。
    # p は Path の略です。
    for p in pathlib.Path(r'F:\apps\data').glob('*log.txt*'):
        if p.is_file():
            file_size = p.stat().st_size
            print(f'({p}) {file_size} [bytes]')

    print('\n(デバッグ) ロガーとハンドラのロギングレベルを確認します。')
    print(f'root.level: {logging.getLevelName(root.level)} ({root.level})')
    print(f'root.getEffectiveLevel(): {logging.getLevelName(root.getEffectiveLevel())} ({root.getEffectiveLevel()})')
    print(f'logger.level: {logging.getLevelName(logger.level)} ({logger.level})')
    print(f'logger.getEffectiveLevel(): {logging.getLevelName(logger.getEffectiveLevel())} ({logger.getEffectiveLevel()})')
    print(f'sh.level: {logging.getLevelName(sh.level)} ({sh.level})')
    print(f'fh.level: {logging.getLevelName(fh.level)} ({fh.level})')
    print(f'rh.level: {logging.getLevelName(rh.level)} ({rh.level})')
    return

def jisaku_func():
    """自作関数です。"""
    # (5/5) 名前付きロガーでログを記録してみます。
    # logger.propagate 属性を False に設定したので、
    # ログは 名前付きロガー に取り付けたハンドラだけで記録されます。
    temperatures = [15, 18, 20, 25, 28, 30, 35, 40, 45] # 架空の気温
    for t in temperatures:
        if t < 24:
            logger.debug(f'気温 {t} ℃')
        elif 24 <= t < 28:
            logger.info(f'暑さに注意 {t} ℃')
        elif 28 <= t < 31:
            logger.warning(f'暑さに警戒 {t} ℃')
        elif 31 <= t < 36:
            logger.error(f'暑さに厳重警戒 {t} ℃')
        elif 36 <= t:
            logger.critical(f'危険な気温です {t} ℃')
    return

if __name__ == '__main__':
    main()

実行結果

画面表示

コード例を実行したときの画面表示です。StreamHandler()print() 関数による表示です。

root DEBUG: start
root INFO: %(name)s %(levelname)s: %(message)s
root INFO: 気温は『temperature ロガー』に取り付けたハンドラで記録中...
root INFO: (temperature) logger.propagate: False
temperature DEBUG: 気温 15 ℃
temperature DEBUG: 気温 18 ℃
temperature DEBUG: 気温 20 ℃
temperature INFO: 暑さに注意 25 ℃
temperature WARNING: 暑さに警戒 28 ℃
temperature WARNING: 暑さに警戒 30 ℃
temperature ERROR: 暑さに厳重警戒 35 ℃
temperature CRITICAL: 危険な気温です 40 ℃
temperature CRITICAL: 危険な気温です 45 ℃
root DEBUG: end

(デバッグ) ログのファイルサイズを取得してみます。
(F:\apps\data\root_log.txt) 228 [bytes]
(F:\apps\data\temperature_log.txt) 104 [bytes]
(F:\apps\data\temperature_log.txt.1) 94 [bytes]
(F:\apps\data\temperature_log.txt.2) 87 [bytes]
(F:\apps\data\temperature_log.txt.3) 102 [bytes]

(デバッグ) ロガーとハンドラのロギングレベルを確認します。
root.level: DEBUG (10)
root.getEffectiveLevel(): DEBUG (10)
logger.level: NOTSET (0)
logger.getEffectiveLevel(): DEBUG (10)
sh.level: NOTSET (0)
fh.level: NOTSET (0)
rh.level: NOTSET (0)

ログファイル

ログファイルの内容です。

サイズは Windows のファイルのプロパティで確認したファイルサイズです。

root_log.txt サイズ: 228 バイト (228 バイト)

root DEBUG: start
root INFO: %(name)s %(levelname)s: %(message)s
root INFO: 気温は『temperature ロガー』に取り付けたハンドラで記録中...
root INFO: (temperature) logger.propagate: False
root DEBUG: end

temperature_log.txt サイズ: 104 バイト (104 バイト)

temperature CRITICAL: 危険な気温です 40 ℃
temperature CRITICAL: 危険な気温です 45 ℃

temperature_log.txt.1 サイズ: 94 バイト (94 バイト)

temperature WARNING: 暑さに警戒 30 ℃
temperature ERROR: 暑さに厳重警戒 35 ℃

temperature_log.txt.2 サイズ: 87 バイト (87 バイト)

temperature INFO: 暑さに注意 25 ℃
temperature WARNING: 暑さに警戒 28 ℃

temperature_log.txt.3 サイズ: 102 バイト (102 バイト)

temperature DEBUG: 気温 15 ℃
temperature DEBUG: 気温 18 ℃
temperature DEBUG: 気温 20 ℃

1文字は何バイトか?(文字によって変わりました)

UTF-8 では、英数字なら1文字1バイト、日本語ならたいてい1文字3バイトの計算になりました。

Python の文字列 'a' を UTF-8 形式のバイト列にエンコードするときは、たとえば str.encode() メソッドを使用します。

(Python) str.encode(encoding='utf-8', errors='strict')

それから、Python マニュアルによると、1 バイトは 8-bit とのことでした。

bytes オブジェクトは不変な配列です。要素は 8-bit バイトで、 0 <= x < 256 の範囲の整数で表現されます。

(Python) bytes(Python 言語リファレンス ⇒ 標準型の階層 ⇒ シーケンス型 ⇒ 変更不能なシーケンス)

英数字(UTF-8 で 1 バイトの文字)

英数字のバイト数の例です。

>>> 'a'.encode('utf-8')
b'a'
>>> len('a'.encode('utf-8'))
1

(ファイルサイズ)実際にテキストファイルに1文字だけの a を書いて、UTF-8(BOM 無し)で保存してみました。その時のファイルサイズです。Windows のファイルのプロパティでは、

サイズ: 1 バイト (1 バイト)

と表示されていました。

日本語(UTF-8 で 3 バイトの文字)

日本語のバイト数の例です。

>>> 'あ'.encode('utf-8')
b'\xe3\x81\x82'
>>> len('あ'.encode('utf-8'))
3

(ファイルサイズ)実際にテキストファイルに1文字だけの を書いて、UTF-8(BOM 無し)で保存してみました。その時のファイルサイズです。Windows のファイルのプロパティでは、

サイズ: 3 バイト (3 バイト)

と表示されていました。

日本語の UTF-8 でのバイト数は、たいてい1文字3バイトでした。

UTF-8 で 4 バイトの文字

UTF-8 で 1 文字 4 バイトになるような文字の例です。

漢字であれば、たとえば、『(ja.wikipedia.org) CJK統合漢字拡張B (20000-215FF)』にあるようなものが、UTF-8 で 1 文字あたり 4 バイトになりました。

(2022年時点の)日本語の文章では、まず使わない漢字たちだと思います。

漢字以外にも、UTF-8 で1文字 4 バイトになった文字はたくさんありました(古代文字とか何かの記号とか)。

絵文字の 😊 なども、UTF-8 で1文字 4 バイトでした。

ロギングの記事

loggingロギング モジュールでロガーを使う方法を書きました。

concurrent.futures でログを記録するコード例。

multiprocessing でログを記録するコード例。

ロガーで自動的に付加できる情報の表示例です。

エラーを記録するときは、自作例外でエラー名をつけてあげると便利でした。

Python マニュアル

コード例に関係する Python マニュアルの場所です。

モジュールレベルの関数

(Python) logging.getLevelName(level)

ロガー

(Python) logging.getLogger(name=None)

(Python) ロガー名の付け方(上級ロギングチュートリアル)logger = logging.getLogger(__name__)

(Python) logging.Logger.propagate 属性

(Python) logging.Logger.setLevel(level) 『名前付きロガー』取得時のロギングレベルは NOTSET で、『ルートロガー』取得時のロギングレベルは WARNING である旨の説明がありました。

(Python) ロギングレベル (..., logging.DEBUG, logging.INFO, ...)

(Python) logging.Logger.addHandler(hdlr)

(Python) logging.Logger.removeHandler(hdlr)

(Python) logging.Logger.debug(msg, *args, **kwargs)

(Python) logging.Logger.info(msg, *args, **kwargs)

(Python) logging.Logger.warning(msg, *args, **kwargs)

(Python) logging.Logger.error(msg, *args, **kwargs)

(Python) logging.Logger.critical(msg, *args, **kwargs)

ロガーはグローバル変数に入れて使うのが便利でした。普通にグローバルのところで代入しても OK でしたし、global 文グローバル ぶんを使用してから代入しても OK でした。

(Python) global

もちろん、ローカル変数に入れて使ってもいいと思いますし、logging.getLogger(name=None) で都度取得して使用してもいいと思います。お好みで。

ハンドラ

ローテーティングファイルハンドラ

(Python) class logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False, errors=None)

(Python) 便利なハンドラ(各種ハンドラの日本語の説明)

(Python) class logging.StreamHandler(stream=None)

(Python) class logging.FileHandler(filename, mode='a', encoding=None, delay=False, errors=None)

(Python) logging.Handler.setLevel(level) 『ハンドラ』生成時のロギングレベルは NOTSET(すべてのメッセージが処理される)である旨の説明がありました。

(Python) logging.Handler.setFormatter(fmt)

フォーマッタ

(Python) class logging.Formatter(fmt=None, datefmt=None, style='%', validate=True, *, defaults=None)

(Python) LogRecord 属性 %(asctime)s, %(levelname)s, %(levelno)s, %(message)s, %(name)s, ...

パスリブ

(Python) class pathlib.Path(*pathsegments)

(Python) Path.stat(*, follow_symlinks=True)

(Python) os.stat_result.st_size

(Python) Path.glob(pattern)

組み込み機能

(Python) __main__(トップレベルのスクリプト環境)

(Python) __name__(インポート関連のモジュール属性)

引用符いんようふの前に『アール r 』を付けた文字列もじれつ

(Python) r''raw stringsロウ ストリングス、raw 文字列)(チュートリアル)

(Python) r''raw stringsロウ ストリングス、raw 文字列)(言語リファレンス)

引用符いんようふの前に『エフ f 』を付けた文字列もじれつ

(Python) f''f-stringエフ ストリング、formatted string literal、フォーマット済み文字列リテラル)(言語リファレンス)

r''f'' を組み合わせた文字列もじれつ rf''

(Python) rf'' フォーマット済みの raw 文字列リテラル

以上です。

スポンサーリンク
シェアする(押すとSNS投稿用の『編集ページ』に移動します)
フォローする(RSSフィードに移動します)
スポンサーリンク
シラベルノート
タイトルとURLをコピーしました