本ページはプロモーションが含まれています

Python

Python loggingの使い方|27本の定期ジョブで使う設定例

CATEGORY
REV.
DRAWN BY
TOM
SHEETS
8枚
logging(Pythonのログ出力と設定)

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

ジョブの一覧には「成功」と出ている。なのに、そのジョブは実は失敗していた。

2026年10月、Macで毎朝7時に動かしているニュース集めのジョブ(news_bot)で、そんなことが起きました。原因はPythonのloggingの設定です。

Python loggingは、getLogger(__name__) でロガーを取り、レベル・出力先・書式の3つを決めれば使えます。

私はlaunchdで動かしている27本のPythonジョブすべてで、自作の共通ロガー setup_logger() を使っています。基本の書き方から、その中身と実際に踏んだ4つの落とし穴まで、実物のコードでまとめました。

この記事でわかること

  • Python loggingとprintの違い、最小コード
  • ログレベル5段階の使い分けと、INFOが出ない理由
  • Logger・Handler・Formatter・Filterの役割
  • ファイル出力とローテーション(サイズ方式と日次方式の選び方)
  • 27本の定期ジョブで使っている共通ロガーの実物と、そこで踏んだ落とし穴

動作確認はPython 3.11.14(macOS)で行いました。コードはすべて標準ライブラリだけで動きます。

PARTS LIST /目次

Python loggingとは?printとの違いと最小コード

Python loggingは、print()の代わりに使う「記録係」です。時刻や重要度を自動で付け、画面とファイルの両方へ同時に書き出せます。最初の3行さえ覚えれば、残りは必要になったときに足していけば十分です。

loggingはPython標準のログ記録の仕組み

Python loggingとは、プログラムの動作記録(ログ)を、重要度・出力先・書式に分けて残すための標準ライブラリです。pip install は要りません。

printでも記録は残せます。違いが出るのは、本番で動かし始めてからです。

項目print()logging
時刻自分で書き足す書式に %(asctime)s を入れるだけ
重要度区別なしDEBUG〜CRITICALの5段階
出力先標準出力のみ画面・ファイルなど複数に同時
本番で減らす1行ずつ消して回るレベルを1か所上げるだけ
どこから出たか分からないロガー名で区別できる

自動実行のスクリプトで効くのは、下の2行です。誰も見ていない時間に失敗したジョブを後から調べるとき、手がかりは時刻と重要度の付いたログファイルしかありません。

basicConfigとgetLogger(name)の3行で始める

一番短い書き方は、basicConfig で全体の設定を1回だけ行い、モジュールごとに getLogger(__name__) でロガーを取る形です。

import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)

logger.info("処理を開始します")
logger.warning("設定ファイルが見つからないので既定値を使います")
2026-11-11 20:55:50,648 - INFO - 処理を開始します
2026-11-11 20:55:50,648 - WARNING - 設定ファイルが見つからないので既定値を使います

__name__ にはモジュール名が入ります。同じ名前で getLogger を呼ぶと同じロガーが返るので、ファイルごとに1回取れば十分です。

logging.info() のように直接呼んでも動きます。ただ、それだとどのモジュールのログか区別できなくなるため、名前付きのロガーを使うほうが後で楽です。

Python loggingのログレベル5段階と使い分け

ログレベルは、ログの重要度を表す5段階の目盛りです。設定したレベル以上のログだけが出力され、それより下は捨てられます。初心者がつまずく「ログが出ない」の大半は、この目盛りの初期値が原因です。

DEBUG〜CRITICALの意味と数値

レベルには数値が割り当てられていて、数値が大きいほど深刻なログです。

レベル数値使う場面
DEBUG10開発中に変数の中身を見たいとき
INFO20処理の開始・終了、件数など正常な記録
WARNING30想定内だが気にしておきたいこと
ERROR40処理の一部が失敗したとき
CRITICAL50処理を続けられないとき

迷ったら「人が後で対応する必要があるか」で決めます。対応が要るならERROR以上、要らないならINFOかWARNINGです。

私の共通ロガーは、レベルをINFOに固定しています。定期ジョブでは、開始・件数・終了がINFOで残っていれば、あとから何が起きたかを追えます。

INFOが出ないのは初期値がWARNINGだから

設定を何もしないと、ロガーの大元(ルートロガー)のレベルはWARNING(30)です。INFOやDEBUGは黙って捨てられます。

ハマりポイント
basicConfigは2回目以降が無視される

basicConfigは、ルートロガーにまだ出力先がないときだけ設定します。先にどこかで呼ばれていたり、ログを1回でも出していたりすると、あとから level=logging.DEBUG を渡しても何も変わりません。設定し直したいときは force=True(Python 3.8以降)を付けます。

手元で試すと、初期状態のルートロガーのレベルは 30 WARNING と表示されました。basicConfig(level=logging.INFO) のあとに level=logging.DEBUG で呼び直しても、DEBUGのログは出ませんでした。

logger.exceptionで例外の詳細を残す

例外を捕まえたら、logger.error ではなく logger.exception を使います。レベルはERRORのまま、トレースバック(どの行で何が起きたか)まで書き残してくれます。

try:
    1 / 0
except ZeroDivisionError:
    logger.exception("割り算に失敗")
ERROR:__main__:割り算に失敗
Traceback (most recent call last):
  File "t.py", line 2, in <module>
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero

except の外で呼ぶと、付けるトレースバックがありません。except ブロックの中で使うものと覚えておけば迷いません。

loggingの4つの部品:Logger・Handler・Formatter・Filter

loggingの設定が分かりにくいのは、部品が4つに分かれているからです。順番に並べると、「誰が書くか → 何を通すか → どこへ書くか → どんな形で書くか」の流れです。

1本のログが出力されるまでの流れを図にしました。

flowchart LR
    A["Logger<br>logger.info()を受け取る"] --> B["Filter<br>通す・書き換える"]
    B --> C["Handler<br>出力先を決める"]
    C --> D["Formatter<br>1行の形に整える"]
    D --> E["画面やファイル"]

Handlerは1つのLoggerに何個でも付けられます。「画面にも出して、ファイルにも残す」は、Handlerを2つ付けるだけです。

getLoggerの名前とロガーの親子関係

ロガー名はドットで階層になります。"app.db" は "app" の子で、すべてのロガーはルートロガーの子孫です。

子で出たログは、自分のHandlerで出力されたあと、親のHandlerにも渡ります(propagate)。自分のロガーとルートの両方にHandlerを付けると、同じログが2回出ます。

logging.basicConfig()              # ルートにHandlerが付く
lg = logging.getLogger("dup")
lg.addHandler(logging.StreamHandler())
lg.warning("2回出る")
2回出る
WARNING:dup:2回出る

対策は、どちらか一方にだけHandlerを付けるか、子のロガーで lg.propagate = False にすることです。

Formatterでログのformatを決める

Formatterは、1行のログの形を決める部品です。%(名前)s の形で、入れたい情報を並べます。

書き方入る内容
%(asctime)s日時(例: 2026-11-11 20:55:50,648)
%(levelname)sレベル名(INFO・ERRORなど)
%(name)sロガー名
%(message)sログの本文
%(filename)s / %(lineno)d出力したファイル名と行番号
%(funcName)s出力した関数名

日時の形を変えたいときは、logging.Formatter(fmt, datefmt="%Y-%m-%d %H:%M:%S") のように datefmt を渡します。

Filterで出力前に中身を書き換える

Filterは、ログを出すかどうかを決める部品です。filter(record) が False を返したログは捨てられ、True なら出力されます。

捨てるだけでなく、record の中身を書き換えてから True を返すこともできます。私は書き換えの仕組みを、パスワードやAPIキーを伏せ字にする用途で使っています(実物は後半の共通ロガーの節で紹介します)。

Python loggingでファイル出力する方法

ログをファイルに残すときは、Handlerをファイル用のものに替えます。気をつけるのは、文字コードとファイルの膨らみ方の2つです。

FileHandlerにはencoding="utf-8"を付ける

一番素直なファイル出力は FileHandler です。既定は追記モードなので、実行のたびに前回の続きへ書き足されます。

handler = logging.FileHandler("app.log", encoding="utf-8")
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
logger.addHandler(handler)

encoding を省くと、OSの既定の文字コードで書かれます。Macなら困りませんが、Windowsの日本語環境では日本語のログが文字化けする原因になります。basicConfig(filename="app.log", encoding="utf-8") のように、basicConfigでも指定できます(Python 3.9以降)。

RotatingFileHandlerでサイズに上限をつける

FileHandlerのままだと、ログファイルは消さない限り膨らみ続けます。RotatingFileHandler を使うと、決めたサイズを超えたところで新しいファイルに切り替わります。

from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler(
    "app.log",
    maxBytes=5 * 1024 * 1024,  # 5MBを超えたら切り替える
    backupCount=2,             # app.log.1 と app.log.2 まで残す
    encoding="utf-8",
)

超えると app.log が app.log.1 に、app.log.1 が app.log.2 に送られ、それより古いものは消えます。この設定なら、ディスクを使うのは最大で5MB×3ファイル=15MBです。maxBytes を0のままにすると、切り替えは起きません。

日次ローテーションをやめてサイズ方式に戻した理由

日付で切り替えたいなら TimedRotatingFileHandler(when="midnight") もあります。私も一度はこちらにしました。

LOG2026年3月日次にして、2週間でサイズ方式へ戻した

2026年3月15日に共通ロガーを作ったときはサイズ方式でした。同じ日のうちに、毎日深夜に切り替わる日次方式へ変えています。

3月29日、サイズ方式に戻しました。どのジョブも1日1回、数十行しか書かないので、日次で切るとファイルが細切れになるだけだったからです。

2026年11月11日時点で、ジョブのログを大きい順に見ると、一番大きいものでも1.2MBでした。週1回動くSSL証明書の監視ジョブは、2026年5月9日の初回から数えて62行です。5MBの上限には、まだどれも届いていません。

比べる点RotatingFileHandler(サイズ)TimedRotatingFileHandler(日付)
切り替わる時指定サイズを超えたとき毎日0時など、決めた時刻
ディスクの上限◎ 決まる(サイズ×世代数)△ 1日の量しだい
日付でログを探す△ 中を検索する◎ ファイル名で分かる
向いている使い方出力の少ない定期ジョブ1日の量が多いWebサーバーなど

27本の定期ジョブで使う共通ロガーsetup_logger

ここからは、私のリポジトリで実際に使っているロガーの話です。launchdで動かすPythonのジョブ27本(2026年11月11日時点)は、すべて python/common/ にある setup_logger() でロガーを作っています。リポジトリ全体では57本のスクリプトがこの関数を呼んでいます。

setup_loggerのコード(要点版)

実物はコメント込みで300行近くあるので、骨組みだけを抜き出しました。ファイルと画面の両方に同じ書式で出し、サイズでローテーションします。

import logging
from logging.handlers import RotatingFileHandler

FORMAT = "%(asctime)s - %(levelname)s - %(message)s"


def setup_logger(name, log_file, max_bytes=5 * 1024 * 1024, backup_count=2):
    """ファイルと画面の両方に出すロガーを返す"""
    logger = logging.getLogger(name)
    if logger.handlers:  # 2回呼ばれても、Handlerを二重に付けない
        return logger
    logger.setLevel(logging.INFO)
    formatter = logging.Formatter(FORMAT)

    file_handler = RotatingFileHandler(
        log_file, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8"
    )
    file_handler.setFormatter(formatter)
    stream_handler = logging.StreamHandler()
    stream_handler.setFormatter(formatter)

    logger.addHandler(file_handler)
    logger.addHandler(stream_handler)
    logger.addFilter(SecretMaskingFilter())  # 次の節で紹介
    return logger

使う側は、スクリプトの先頭で1行呼ぶだけです。SSL証明書の監視ジョブでは、こう書いています。

logger = setup_logger("ssl_cert_monitor", str(_DIR / "ssl_cert_monitor.log"))

if logger.handlers の2行は、前の節で見た「同じログが2回出る」を防ぐためのものです。同じ名前で何度呼んでも、Handlerは1組だけになります。

あわせて読む

APIキーやWebhook URLをログに残さない

ログには、うっかり秘密の値が混ざります。エラーの中身をそのまま書くと、接続先のURLやトークンまで出てしまうことがあるからです。共通ロガーには、出力の直前に伏せ字へ置き換えるFilterを付けています。

import re

_SECRET_KEY = re.compile(
    r"(?i)\b(api[_-]?key|secret|password|token|webhook[_-]?url)\b(\s*[:=]\s*)(\S+)"
)
_WEBHOOK_URL = re.compile(r"https://(?:discord(?:app)?\.com/api/webhooks|hooks\.slack\.com/services)/\S+")


class SecretMaskingFilter(logging.Filter):
    """ログ本文の秘密情報らしき文字列を伏せ字にする"""

    def filter(self, record):
        message = record.getMessage()
        masked = _WEBHOOK_URL.sub("[MASKED_WEBHOOK_URL]", message)
        masked = _SECRET_KEY.sub(lambda m: f"{m.group(1)}{m.group(2)}[MASKED]", masked)
        if masked != message:
            record.msg = masked
            record.args = ()  # 置き換え済みの本文に、引数を当て直さないようにする
        return True

実際に流すと、ファイルにはこう残ります。

2026-11-11 20:55:50,648 - INFO - api_key=[MASKED] で接続
2026-11-11 20:55:50,648 - INFO - 送信先 [MASKED_WEBHOOK_URL]

Loggerに付けたFilterは子のロガーに効かない
Loggerに付けたFilterは、そのLoggerで直接出したログにしか効きません。子のロガーから上がってきたログは素通りします。全部に効かせたいときは、Handlerの側にもFilterを付けます。

ログの書式はダッシュボードの解析にそろえる

書式を %(asctime)s - %(levelname)s - %(message)s に固定しているのには理由があります。全ジョブの成否を集計する自作のダッシュボードが、ログの各行をこの正規表現で読んでいるからです。

r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d+ - (INFO|WARNING|ERROR|CRITICAL|DEBUG) - (.*)"

ダッシュボードは、この形の行にERRORがあるかどうかで失敗を判定します。1本だけ書式を変えると、そのジョブはエラーが出ても失敗として数えられません。

共通ロガーにまとめると、書式を変えるときは1か所直せば27本すべてが同じ形になります。ログを機械に読ませるなら、書式は全スクリプトで1つにそろえるのが一番の近道でした。

共通ロガーで実際に起きた4つの落とし穴

共通ロガーにまとめても、ログが「期待した場所に、期待した形で」残るとは限りません。2026年10月に見つかった4つの問題は、10月24日にまとめて直しました。どれも、エラーは出ていないのにログが静かに欠けたり、散らばったりする種類のものです。

落とし穴何が起きたか対処
__main__ のエラーERRORがファイルに残らず、失敗が「成功」と表示されたルートロガーに転送用のHandlerを付ける
相対パスのログ実行した場所にログファイルが散らばった呼び出し元のファイル基準で絶対パスにする
テストのERRORテストのエラーが本番のログに混ざったテスト中は書き込み先を一時ディレクトリへ逃がす
launchdのstderr標準エラーのログが8MBを超えた起動時に末尾1MBだけ残して切り詰める

__main__のエラーがファイルに残らなかった

冒頭のnews_botの件は、この落とし穴でした。失敗したのに、ダッシュボードには「成功」と表示されていました。

LOG2026年10月24日エラーはあったのに、ログファイルには1行もなかった

例外を捕まえてログに書くデコレーター(@handle_errors)は、スクリプトを直接実行すると「__main__」という名前のロガーに書きます。setup_loggerで作った「news_bot」ロガーの子ではないので、ファイル用のHandlerに届きません。

行き場のないエラーは標準エラーにだけ出て、アプリのログファイルには残りませんでした。ダッシュボードはファイルにERRORがないので、成功と判定していました。

対処として、ルートロガーに「WARNING以上を共通ロガーのHandlerへ転送するHandler」を1つ付けました。共通モジュールが getLogger(__name__) で書いたエラーも、転送用のHandlerを通ってファイルに届きます。INFOまで転送すると外部ライブラリのログで埋まるので、WARNING以上に絞っています。

相対パスのログが実行した場所に散らばった

RotatingFileHandler("static_publish.log") のように相対パスを渡すと、実行したときのカレントディレクトリを基準に開かれます。launchdから動かすぶんには毎回同じ場所でも、手で別のディレクトリから実行すると、そこに新しいログファイルができます。

2026年10月、static_publish.log がリポジトリの直下にできていて気づきました。いまは、相対パスを「setup_loggerを呼んだスクリプトのあるディレクトリ」基準の絶対パスに直してから開いています。呼び出し元のファイルは inspect.currentframe().f_back から取っています。

テストのERRORが本番ログに混ざった

多くのスクリプトは、import した瞬間に setup_logger() を呼び、本番のログファイルを開きます。pytestがそのスクリプトをimportすると、テストでわざと起こしたERRORまで本番のログに書かれます。

2026年10月、記事のエクスポートジョブ(wp_export)のログにテストのERRORが残り、ダッシュボードが失敗と判定しました。いまは、テスト実行中だけ環境変数(LOGGER_UTILS_REDIRECT_DIR)を立て、リポジトリ内のログの書き込み先を一時ディレクトリへ差し替えています。環境変数はルートの conftest.py で設定します。

launchdのstderrログが8MBを超えた

StreamHandlerは標準エラーに書きます。launchdから動かすと、標準エラーはplistの StandardErrorPath に指定したファイルへ追記され続けます。RotatingFileHandlerの管理外なので、上限がありません。

wp_exportでは、このファイルが8MBを超えていました。いまは、setup_loggerを最初に呼んだときに、標準エラーのファイルが5MBを超えていれば末尾1MBだけを残して切り詰めています。ログの置き場所の決め方は、launchdの記事にまとめています。

あわせて読む

Python loggingのよくある質問

Python loggingで、つまずきやすい点をQ&Aにまとめました。どれも手元のPython 3.11.14で動きを確かめています。

Q1. basicConfigを書いたのにログの設定が変わらないのはなぜですか?

A1. basicConfigは、ルートロガーにまだHandlerがないときだけ設定するからです。先にどこかで呼ばれていると、2回目以降は無視されます。logging.basicConfig(level=logging.DEBUG, force=True) のように force=True を付けると設定し直せます(Python 3.8以降)。

Q2. 同じログが2回出力されるのはなぜですか?

A2. 自分のロガーと親(多くはルートロガー)の両方にHandlerが付いているからです。子で出たログは親にも渡るため、2回書かれます。どちらか一方にだけHandlerを付けるか、子のロガーで propagate = False にしてください。

Q3. ログの本文はf-stringと%書式のどちらで書けばいいですか?

A3. logger.info("%s件を処理しました", count) のように、引数で渡す%書式がおすすめです。ログが捨てられるレベルのときは、文字列を組み立てる処理そのものを省けます。f-stringは捨てられるときも必ず組み立てられ、Ruffでも警告の対象(G004)です。

Q4. loggingの設定をファイルにまとめることはできますか?

A4. できます。辞書で渡す logging.config.dictConfig か、ini形式のファイルを読む logging.config.fileConfig を使います。どちらも既定で、それまでに作られたロガーを無効にする(disable_existing_loggers=True)点に気をつけてください。

Q5. printからloggingへはどう置き換えればいいですか?

A5. スクリプトの先頭で basicConfig と getLogger(__name__) を書き、print(...) を logger.info(...) に置き換えます。エラーを表示しているprintは logger.error、except の中なら logger.exception にすると、後で探しやすくなります。

Q6. ログファイルの日本語が文字化けするときはどうすればいいですか?

A6. ファイル用のHandlerに encoding="utf-8" を指定してください。指定しないとOSの既定の文字コードで書かれ、Windowsの日本語環境では化ける原因になります。basicConfigでも encoding="utf-8" を渡せます(Python 3.9以降)。

Python loggingを使い始める3つのポイントまとめ

Python loggingは、Logger・Handler・Formatter・Filterの4つの部品を組み合わせて、ログを残す仕組みです。最初は3行で始め、困ったら部品を足せば足ります。

2026年3月に共通ロガーを作ってから約8か月、27本の定期ジョブで使ってきて、押さえておけばよかったのは次の3つでした。

  • 書式・レベル・ローテーションは共通の関数に1か所でまとめ、Handlerの二重登録を防ぐ
  • 出力の少ない定期ジョブは、日次よりサイズ方式(5MB×3世代など)のほうが扱いやすい
  • エラーが本当にファイルまで届いているかを、わざと失敗させて確かめる

落とし穴はどれも、ログが黙って欠ける種類のものでした。ログは、書いたつもりの場所に残っていて初めて役に立ちます。

ERRORが出たときにスマホで気づけるようにしたいなら、Discordへの通知を組み合わせる方法があります。

あわせて読む
NEXT STEP
いま動かしているスクリプトに、わざとエラーを起こす1行を足して、ログファイルにERRORが残るか確かめてみてください。

残っていなければ、Handlerがどのロガーに付いているかを見直す合図です。確かめたら、その1行は消しておきましょう。

途中経過は、また書きます。

NEXT READ本棚から1本

AIで開発を組み直すなら、まずこの5冊から

入門・実践・設計まで、Claude Codeの本をレベル別に5冊選んで比べました。

→ おすすめ5冊を見る

  • この記事を書いた人
  • 最新記事

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

Python