はじめに
Pythonでのデバッグや本番監視に print を使い続けていませんか?print はシンプルですが、ログレベルの制御、ファイルへの出力、タイムスタンプの付与、本番での無効化が難しいです。
Pythonの標準ライブラリ logging を使うと、これらすべてを宣言的に制御できます。本記事では基礎から実践パターンまでを解説します。
ログレベルの種類
logging には5段階のログレベルがあります。
| レベル | 数値 | 用途 |
|---|---|---|
DEBUG | 10 | 詳細なデバッグ情報(開発時のみ) |
INFO | 20 | 正常系の処理記録 |
WARNING | 30 | 予期しない状況だが処理は継続できる |
ERROR | 40 | 処理が失敗した |
CRITICAL | 50 | システムが続行不可能な致命的エラー |
デフォルトのルートロガーは WARNING 以上のみ出力します。
ロガーの階層構造と伝播(propagation)
logging.getLogger(name) で作られるロガーは、name のドット区切りによって親子関係を持つ「木構造」を形成します。例えば myapp.db.pool というロガーは myapp.db の子であり、myapp.db は myapp の子、myapp はルートロガー(logging.getLogger() を引数なしで呼んだときに返るもの)の子です。
import logging
app_logger = logging.getLogger("myapp")
db_logger = logging.getLogger("myapp.db")
pool_logger = logging.getLogger("myapp.db.pool")
print(app_logger.parent.name) # root
print(db_logger.parent.name) # myapp
print(pool_logger.parent.name) # myapp.db
各ロガーには propagate という属性があり、デフォルトは True です。True の場合、そのロガー自身のハンドラーで処理された後も、ログレコードは親ロガーへ伝播し、親(さらにその親…ルートまで)に登録されたハンドラーでも処理されます。myapp.db.pool でログを出しても、myapp.db.pool 自体にハンドラーがなければ myapp や root のハンドラーが代わりに出力を担当することがあるのはこのためです。
「ロガーのレベル」と「ハンドラーのレベル」を混同しない
logging を学び始めたときに最もつまずきやすいのが、ロガーのレベルとハンドラーのレベルは別物であり、ログが実際に出力されるにはその両方を通過する必要があるという点です。
- まず、ロガー自身の実効レベル(
getEffectiveLevel()。NOTSETの場合は親を遡って探索)でフィルタされる - 次に、そのレコードを受け取った各ハンドラーの
levelでさらにフィルタされる
「ロガーを DEBUG に設定したのに DEBUG ログが出ない」という質問の多くは、ハンドラー側のレベルが WARNING や INFO のままになっているケースです。実際に確認してみます。
import logging
demo_logger = logging.getLogger("demo.gate")
demo_logger.setLevel(logging.DEBUG) # ロガー側のゲート:DEBUG以上を許可
demo_logger.propagate = False
handler = logging.StreamHandler()
handler.setLevel(logging.WARNING) # ハンドラー側のゲート:WARNING以上のみ許可
handler.setFormatter(logging.Formatter("[%(levelname)s] %(name)s: %(message)s"))
demo_logger.addHandler(handler)
demo_logger.debug("デバッグメッセージ(ハンドラーで落とされる)")
demo_logger.info("情報メッセージ(ハンドラーで落とされる)")
demo_logger.warning("警告メッセージ(両方のゲートを通過)")
demo_logger.error("エラーメッセージ(両方のゲートを通過)")
demo_logger.critical("致命的メッセージ(両方のゲートを通過)")
実行結果(実際の出力):
[WARNING] demo.gate: 警告メッセージ(両方のゲートを通過)
[ERROR] demo.gate: エラーメッセージ(両方のゲートを通過)
[CRITICAL] demo.gate: 致命的メッセージ(両方のゲートを通過)
debug() と info() はロガー側のゲート(DEBUG)は通過しているにもかかわらず、ハンドラー側のゲート(WARNING)で静かに破棄され、コンソールには一切現れません。「ロガーのレベルを下げたのにログが出ない」場合は、必ずハンドラー側のレベルも確認してください。
基本的な使い方
basicConfigによる設定
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logging.debug("デバッグ情報")
logging.info("正常処理")
logging.warning("警告")
logging.error("エラー発生")
logging.critical("致命的エラー")
出力例:
2026-03-12 10:00:00 DEBUG デバッグ情報
2026-03-12 10:00:00 INFO 正常処理
2026-03-12 10:00:00 WARNING 警告
2026-03-12 10:00:00 ERROR エラー発生
2026-03-12 10:00:00 CRITICAL 致命的エラー
モジュールごとのロガー
本番コードでは logging.getLogger(__name__) でモジュール専用のロガーを作ります。どのモジュールからのログかが一目でわかります。
import logging
logger = logging.getLogger(__name__)
def process_data(data):
logger.info("処理開始: %d件", len(data))
try:
result = [x * 2 for x in data]
logger.debug("処理結果: %s", result)
return result
except Exception as e:
logger.error("処理失敗: %s", e, exc_info=True)
raise
exc_info=True を指定するとスタックトレースも記録されます。
フォーマットのカスタマイズ
%(...)s スタイルのフォーマット文字列でログの見た目を自由に変えられます。
import logging
formatter = logging.Formatter(
fmt="%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
logger.addHandler(handler)
logger.info("サービス起動")
出力例:
2026-03-12T10:00:00 [INFO ] myapp:10 - サービス起動
主要なフォーマット変数:
| 変数 | 内容 |
|---|---|
%(asctime)s | タイムスタンプ |
%(levelname)s | ログレベル名 |
%(name)s | ロガー名 |
%(filename)s | ファイル名 |
%(lineno)d | 行番号 |
%(funcName)s | 関数名 |
%(message)s | ログメッセージ |
%(process)d | プロセスID |
%(thread)d | スレッドID |
ハンドラーの種類
ハンドラーはログの出力先を決めます。複数のハンドラーを同時に使えます。
StreamHandler(標準出力/標準エラー)
import logging
import sys
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
FileHandler(ファイル出力)
file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
RotatingFileHandler(ログローテーション)
本番環境ではログが肥大化しないよう、ファイルサイズで自動ローテーションします。
from logging.handlers import RotatingFileHandler
rotating_handler = RotatingFileHandler(
"app.log",
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5, # 最大5世代保持
encoding="utf-8",
)
app.log が 10MB に達すると app.log.1 にリネームされ、新しい app.log が作られます。
実際にローテーションが起きることを、maxBytes を極端に小さくして確認してみます。
from logging.handlers import RotatingFileHandler
import logging
logger = logging.getLogger("demo.size_rotate")
logger.setLevel(logging.DEBUG)
logger.propagate = False
handler = RotatingFileHandler(
"size.log",
maxBytes=200, # 意図的に小さくしてすぐローテーションさせる
backupCount=3,
encoding="utf-8",
)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(handler)
for i in range(40):
logger.info("サイズローテーションのテスト行 %03d - バイト数を稼ぐためのパディング", i)
実行後にディレクトリを確認すると、backupCount=3 の通り最大3世代のバックアップが実際に作られています。
$ ls -la
size.log 254 bytes
size.log.1 254 bytes
size.log.2 254 bytes
size.log.3 254 bytes
size.log が maxBytes を超えるたびに size.log → size.log.1 → size.log.2 → … と世代がスライドし、backupCount を超えた最も古い世代は削除されることが確認できます。
TimedRotatingFileHandler(時刻ローテーション)
日次でローテーションする場合:
from logging.handlers import TimedRotatingFileHandler
timed_handler = TimedRotatingFileHandler(
"app.log",
when="midnight", # 毎日深夜にローテーション
interval=1,
backupCount=30, # 30日分保持
encoding="utf-8",
)
when="midnight" は日次ローテーションなので実行確認に1日待つ必要がありますが、when="S", interval=1(1秒間隔)にすれば同じロジックを数秒で検証できます。
from logging.handlers import TimedRotatingFileHandler
import logging
import time
logger = logging.getLogger("demo.timed_rotate")
logger.setLevel(logging.DEBUG)
logger.propagate = False
handler = TimedRotatingFileHandler(
"timed.log",
when="S",
interval=1, # 意図的に1秒間隔にしてすぐローテーションさせる
backupCount=5,
encoding="utf-8",
)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(handler)
for i in range(4):
logger.info("時刻ローテーションのテスト行、バッチ %d", i)
time.sleep(1.1) # 1秒境界をまたぐ
実行結果:
$ ls -la
timed.log 86 bytes
timed.log.2026-07-19_09-56-19 86 bytes
timed.log.2026-07-19_09-56-20 86 bytes
timed.log.2026-07-19_09-56-22 86 bytes
RotatingFileHandler はファイルの連番サフィックス(.1, .2, …)でローテーションするのに対し、TimedRotatingFileHandler はタイムスタンプサフィックス(.2026-07-19_09-56-19 のような形式)でローテーションする点が実務上の大きな違いです。連番方式は「常に直近N世代」を保持し、タイムスタンプ方式は「いつのログか」がファイル名から一目で分かります。
実践:アプリケーション全体の設定
複数モジュールにまたがるアプリでは、専用の設定関数を用意します。
import logging
import sys
from logging.handlers import RotatingFileHandler
def setup_logging(log_level: str = "INFO", log_file: str = "app.log") -> None:
"""アプリケーション全体のログ設定を初期化する"""
level = getattr(logging, log_level.upper(), logging.INFO)
formatter = logging.Formatter(
fmt="%(asctime)s [%(levelname)-8s] %(name)s - %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)
# コンソール: INFO以上
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
# ファイル: DEBUG以上(ローテーション付き)
file_handler = RotatingFileHandler(
log_file,
maxBytes=10 * 1024 * 1024,
backupCount=5,
encoding="utf-8",
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
# ルートロガーに設定
root_logger = logging.getLogger()
root_logger.setLevel(level)
root_logger.addHandler(console_handler)
root_logger.addHandler(file_handler)
if __name__ == "__main__":
setup_logging(log_level="DEBUG")
logger = logging.getLogger(__name__)
logger.info("アプリケーション起動")
logger.debug("デバッグ情報(ファイルのみ)")
例外のログ記録
例外処理で logger.exception() を使うと、メッセージとスタックトレースを同時に記録します。
import logging
logger = logging.getLogger(__name__)
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
logger.exception("ゼロ除算エラー: a=%s, b=%s", a, b)
return None
divide(10, 0)
出力例:
2026-03-12T10:00:00 [ERROR ] __main__ - ゼロ除算エラー: a=10, b=0
Traceback (most recent call last):
File "example.py", line 7, in divide
return a / b
ZeroDivisionError: division by zero
辞書設定(dictConfig)
大規模アプリでは logging.config.dictConfig を使い、設定を辞書(またはYAML/JSON)で管理します。
import logging
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
},
"detailed": {
"format": "%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d %(funcName)s() - %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "standard",
"stream": "ext://sys.stdout",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "detailed",
"filename": "app.log",
"maxBytes": 10485760,
"backupCount": 5,
"encoding": "utf-8",
},
},
"loggers": {
"myapp": {
"handlers": ["console", "file"],
"level": "DEBUG",
"propagate": False,
},
},
"root": {
"handlers": ["console"],
"level": "WARNING",
},
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("myapp")
logger.info("dictConfigで設定したロガー")
構造化ログ(structlog)
JSONログはログ収集基盤(Datadog, CloudWatch, ELK)との連携が容易です。外部ライブラリ structlog を使うと構造化ログを簡単に実装できます。
# pip install structlog
import structlog
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_log_level,
structlog.processors.JSONRenderer(),
],
)
log = structlog.get_logger()
log.info("ユーザーログイン", user_id=42, ip="192.168.1.1")
出力例(JSON):
{
"timestamp": "2026-03-12T10:00:00Z",
"level": "info",
"event": "ユーザーログイン",
"user_id": 42,
"ip": "192.168.1.1"
}
structlog なしでJSONログを出す場合、logging.Formatter を継承したカスタムフォーマッターを書く方法もあります。
import json
import logging
class JsonFormatter(logging.Formatter):
def format(self, record):
log_data = {
"timestamp": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
return json.dumps(log_data, ensure_ascii=False)
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger = logging.getLogger("myapp")
logger.addHandler(handler)
logger.info("JSONフォーマットのログ")
実践でハマりやすい落とし穴(エッジケース)
落とし穴1: basicConfig() が「何もしていない」典型的な事故
logging.basicConfig() には、意外と知られていない重要な仕様があります。ルートロガーに既に1つでもハンドラーが設定されていると、basicConfig() は(force=True を指定しない限り)何もせず黙って終了します。 これは仕様どおりの動作ですが、実務では次のようなシナリオで静かにハマります。
- 使っているライブラリ(クラウドSDK、Jupyter、一部のWebフレームワークなど)が import 時にルートロガーへハンドラーを追加している
- 別のモジュールが自分より先に
logging.basicConfig()やlogging.warning(...)を呼んでいる(logging.warning()などのモジュールレベル関数は、内部で暗黙的に一度だけbasicConfig()を呼び出します)
この状態で自分のコードが logging.basicConfig(level=logging.DEBUG, format=...) を呼んでも、レベルもフォーマットも一切反映されません。実際に確認してみます。
import logging
def third_party_library_import_side_effect():
"""サードパーティ製ライブラリが import 時にルートロガーへ
ハンドラーを追加してしまうケースを再現する。"""
root = logging.getLogger()
h = logging.StreamHandler()
h.setFormatter(logging.Formatter("[サードパーティのハンドラー] %(levelname)s %(message)s"))
root.addHandler(h)
# ステップ1: サードパーティのimportがルートロガーにハンドラーを追加
third_party_library_import_side_effect()
print("handlers:", logging.getLogger().handlers)
# ステップ2: 自アプリがbasicConfig()でDEBUGレベルを設定したつもり
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(message)s",
force=False, # デフォルト値
)
print("level:", logging.getLevelName(logging.getLogger().level))
logging.debug("DEBUGとして出力されるはず")
logging.info("INFOとして出力されるはず")
実行結果(実際の出力。debug()/info() の行は何も出力されない):
handlers: [<StreamHandler <stderr> (NOTSET)>]
level: WARNING
ルートロガーのレベルは WARNING のまま変わらず、debug()・info() の呼び出しは静かに握りつぶされています。basicConfig() の呼び出し自体はエラーにならないため、原因に気づきにくいのが厄介な点です。
対処法:force=True を指定すると、既存のハンドラーをすべて閉じて除去したうえで新しい設定を適用します。
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(message)s",
force=True,
)
logging.debug("DEBUGとして出力されるはず")
logging.info("INFOとして出力されるはず")
2026-07-19 09:56:17,214 DEBUG DEBUGとして出力されるはず
2026-07-19 09:56:17,214 INFO INFOとして出力されるはず
force=True によって、期待通りのフォーマットとレベルでログが出力されるようになりました。
落とし穴2: マルチプロセス/マルチスレッドでのログ破損と QueueHandler/QueueListener による解決
複数のプロセス(あるいはスレッド)が同じログファイルに、それぞれ独自の FileHandler で書き込もうとするのは典型的なアンチパターンです。Python公式の Logging Cookbook でも次のように明記されています。
logging to a single file from multiple processes is not supported, because there is no standard way to serialize access to a single file across multiple processes in Python. (複数プロセスから単一ファイルへログを出力することはサポートされていません。Pythonには複数プロセス間でファイルアクセスを直列化する標準的な方法がないためです。)
各プロセスの FileHandler が持つロック(threading.Lock)は、あくまでそのプロセス内の複数スレッド間でしか排他制御ができません。別プロセスの書き込みとは何も同期されないため、複数プロセスが同時に書き込むと、書き込みのタイミング次第でログの行が途中で混ざり合う(文字化けする)ことがあります。
以下のデモでは、1レコードをあえて「ヘッダー部分」と「本体部分」の2回の write() 呼び出しに分けて書き込みます(これは、メッセージがストリームの内部バッファより大きい場合や、フォーマッターがヘッダーと本文を分けて書き込む場合に実際に起こりうる状況です)。これにより、レースコンディションが再現しやすくなります。
import multiprocessing
LOG_PATH = "naive_multiproc.log"
N_WORKERS = 6
N_LINES_PER_WORKER = 60
def worker(worker_id: int):
tag = chr(ord("A") + worker_id)
# 各プロセスが同じファイルを個別にopenする
# (=各プロセスが個別にFileHandler(LOG_PATH)を作るのと同じ状況)
f = open(LOG_PATH, "a", encoding="utf-8")
for i in range(N_LINES_PER_WORKER):
f.write(f"[{tag}#{i:03d}] ") # write呼び出し1
f.flush()
f.write(tag * 40 + "\n") # write呼び出し2(ここで競合しうる)
f.flush()
f.close()
if __name__ == "__main__":
procs = [multiprocessing.Process(target=worker, args=(i,)) for i in range(N_WORKERS)]
for p in procs:
p.start()
for p in procs:
p.join()
実行結果(実際の出力。360行を期待して書き込み、実際に文字化けした行数を検出):
期待される行数 : 360
実際にファイルにある行数 : 360
正常な行数 : 246
破損/文字化けした行数 : 114
--- 破損した行のサンプル(先頭6件、複数ワーカーのタグが混ざっている)---
'[B#003] [C#000] BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n'
'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n'
'[B#004] [C#001] BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n'
'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n'
'[B#005] [C#002] BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n'
'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n'
360行を期待して書き込んだのに、100行以上で別のプロセスの書き込みと混ざり合っていることが分かります([B#003] の直後に本来 B の本文が続くべきところに [C#000] が割り込んでいる、など)。なお、この破損の再現率は実行環境・タイミング依存で、メッセージサイズやOSのバッファリング挙動によっては0件になることもあります(実際、単純に1回の write() で書き切る短いメッセージだと、macOS上では今回の環境で破損が観測できないこともありました)。「たまたま開発環境では起きない」からこそ、本番の負荷やメッセージ長の違いで突然発覚しやすい、厄介なバグです。
正しい解決策は、各プロセス(各スレッド)にはファイルを直接触らせず、QueueHandler でログレコードをキューに送るだけにし、実際のファイル書き込みは1つの QueueListener に一任することです。ファイルを開くオブジェクトが常に1つしか存在しないため、原理的に競合が起こりえません。

import logging
import logging.handlers
import multiprocessing
LOG_PATH = "queue_fixed.log"
N_WORKERS = 6
N_LINES_PER_WORKER = 60
def worker(worker_id: int, queue: multiprocessing.Queue):
# 各プロセスはキューに送るだけで、ファイルには一切触れない
qh = logging.handlers.QueueHandler(queue)
logger = logging.getLogger(f"worker{worker_id}")
logger.setLevel(logging.INFO)
logger.addHandler(qh)
logger.propagate = False
tag = chr(ord("A") + worker_id)
for i in range(N_LINES_PER_WORKER):
logger.info("[%s#%03d] %s", tag, i, tag * 40)
if __name__ == "__main__":
log_queue: multiprocessing.Queue = multiprocessing.Queue(-1)
# ファイルを開くのはこれ1つだけ。QueueListenerが所有する。
file_handler = logging.FileHandler(LOG_PATH, mode="a", encoding="utf-8")
file_handler.setFormatter(logging.Formatter("%(message)s"))
listener = logging.handlers.QueueListener(log_queue, file_handler)
listener.start()
procs = [
multiprocessing.Process(target=worker, args=(i, log_queue))
for i in range(N_WORKERS)
]
for p in procs:
p.start()
for p in procs:
p.join()
listener.stop() # 残りのレコードをフラッシュしてリスナースレッドをjoin
実行結果(実際の出力):
期待される行数 : 360
実際にファイルにある行数 : 360
正常な行数 : 360
破損/文字化けした行数 : 0
同じワークロード・同じ並列度でも、破損した行が0件になりました。QueueHandler/QueueListener パターンは、マルチプロセスだけでなくマルチスレッド構成(threading + queue.Queue)でもそのまま使え、さらに「重いI/Oハンドラー(ネットワーク経由のログ収集基盤への送信など)をメインスレッドから切り離してブロッキングを防ぐ」という副次的なメリットもあります。
Python 3.11以降の logging モジュールの新機能
本記事の内容は概ねどのPython 3系でも動作しますが、近年のバージョンでは logging モジュールにも地味に便利な追加が入っています。
| バージョン | 追加機能 | 内容 |
|---|---|---|
| 3.11 | logging.getLevelNamesMapping() | レベル名 → 数値のマッピングを取得する公式API。以前は非公開の logging._nameToLevel を参照するしかなかった |
| 3.12 | logging.getHandlerByName() / logging.getHandlerNames() | 名前付きハンドラーを名前から取得・列挙できるAPI。dictConfig で設定したハンドラーを後から参照する際に便利 |
| 3.12 | dictConfig() の QueueHandler/QueueListener 対応 | dictConfig の handlers に queue / listener キーを指定するだけで、本記事のエッジケース2で解説したQueueHandler/QueueListener構成を宣言的に設定できるようになった(ただし listener.start() の呼び出しは引き続き手動) |
| 3.13 | LoggerAdapter の merge_extra パラメータ | 個々のログ呼び出しで渡した extra と、LoggerAdapter 自身が持つ extra をマージするかどうかを制御できるようになった(デフォルトはマージしない) |
特に3.12の dictConfig での QueueHandler/QueueListener 宣言的設定は、本記事で手書きした配線をそのまま設定ファイルに落とし込めるようになったという点で実務的なインパクトが大きい変更です。
printとloggingの比較
| 観点 | print | logging |
|---|---|---|
| ログレベル制御 | 不可 | 5段階で制御可 |
| ファイル出力 | リダイレクトのみ | FileHandler で簡単 |
| タイムスタンプ | 手動で追加 | フォーマッターで自動 |
| スタックトレース | 手動で traceback | exc_info=True で自動 |
| 本番での無効化 | 削除または条件分岐 | レベル設定で一括制御 |
| ログローテーション | 不可 | RotatingFileHandler で対応 |
| 構造化ログ | 不可 | dictConfig + structlog で対応 |
関連記事
- Pythonデコレータの仕組みと実践パターン
-
@timerや@retryデコレータとloggingを組み合わせた実装例 - Python asyncio入門 - 非同期コード内でのloggingの注意点
- Pythonでプログレスバーを自作する - CLIツールにおけるログ出力と進捗表示の使い分け
- Pythonでprintの上書きをする方法 - printを活用する場面とloggingとの使い分け