Python の RotatingFileHandler で、ログファイルをローテーションして出力するコード例です。
『ログ出力』と『ログのファイルサイズによるローテーション』は、RotatingFileHandler()
を使うとできました。
コード例です。
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 でした。
もちろん、ローカル変数に入れて使ってもいいと思いますし、logging.getLogger(name=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) __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 文字列リテラル
以上です。