Bottleは軽量でシンプルなPython製のWebフレームワークですが、開発を進めるうちに「print文でデバッグしていたはずのログが、いつの間にか出力されなくなっている」という現象に遭遇したことはないでしょうか。
この問題の原因は、多くの場合Bottleそのものにあるわけではなく、標準ライブラリのloggingモジュールの仕様と、Bottleのリクエストハンドリングの組み合わせ方にあります。
特に、開発サーバーの起動方法やロガーの初期化タイミングを誤ると、意図したログが握りつぶされたり、標準出力に一切表示されなくなったりするケースが少なくありません。
こうした現象に直面すると、多くのエンジニアはまずprint文を増やして対処しようとします。
しかし、それは対症療法に過ぎず、本番環境でのトラブルシューティングやログの永続化には対応できません。
根本的な原因を理解しないままロガーを実装すると、以下のようなアンチパターンに陥りがちです。
- ルートロガーとアプリケーションロガーを混同し、意図しないハンドラが重複登録される
- WSGIサーバー(wsgirefやgunicornなど)ごとに異なるログ出力の挙動を考慮していない
- リクエストごとに
basicConfigを呼び出してしまい、設定が上書きされ続ける
本記事では、なぜBottle開発において標準ログが消えてしまうのか、その技術的な背景をloggingモジュールの内部構造から解説します。
そのうえで、アンチパターンを一つずつ検証しながら、Bottleアプリケーションにおいて再現性が高く保守しやすいロガーの実装方法を、具体的なコード例とともにご紹介します。
ログ出力の仕組みを正しく理解し、開発効率とデバッグ精度を高めていきましょう。
Bottleでログが消える現象とは?開発者が直面する典型的な症状

Bottleを用いてWebアプリケーションを開発していると、動作確認のために仕込んだはずのログが、ある時点から突然出力されなくなるという現象に遭遇することがあります。
ローカルの開発サーバーでは問題なく表示されていたログが、少し実装を変更しただけで消えてしまう、あるいはサーバーを再起動すると出力されたりされなかったりする、といった再現性の低さも特徴の一つです。
このような現象は、Bottleのバグではなく、多くの場合Python標準のloggingモジュールの仕組みと、開発者の実装方法のミスマッチによって引き起こされます。
原因が分かりにくいため、多くのエンジニアが同じ問題で時間を浪費しているのが実情です。
printデバッグからの脱却で気づく落とし穴
開発初期の段階では、print文を使ったデバッグで十分に思えるかもしれません。
しかし、アプリケーションが複雑になるにつれて、出力のオンオフを切り替えたい、ログレベルを分けたい、ファイルに保存したいといった要求が出てきます。
ここで多くの開発者は、素直にloggingモジュールへ移行しようとします。
ところが、print文をそのままlogger.debug()やlogger.info()に置き換えただけでは、期待通りにログが出力されないケースが少なくありません。
これは、loggingモジュールがデフォルトではWARNING以上のレベルしか出力しないという仕様を理解していないことが原因です。
print文の感覚のまま移行すると、この初期設定の壁に必ずといっていいほどぶつかります。
標準出力とログファイルで挙動が変わるケース
さらに厄介なのが、標準出力への表示とログファイルへの書き込みとで、挙動が異なって見えるケースです。
以下のような状況を経験したことはないでしょうか。
- コンソール上には表示されるが、指定したログファイルには何も書き込まれない
- ログファイルには書き込まれるが、コンソールには何も表示されない
- ローカル環境では両方とも出力されるが、本番環境では片方しか機能しない
| 出力先 | 主な原因 | 確認すべき箇所 |
|---|---|---|
| 標準出力 | ハンドラ未設定、レベル未設定 | StreamHandlerの有無 |
| ログファイル | パーミッション不足、パス誤り | FileHandlerの設定 |
| 両方消失 | ロガー名の不一致 | getLoggerの呼び出し方 |
こうした挙動の違いは、ハンドラの設定漏れや、ロガーの取得方法の誤りに起因していることがほとんどです。
次の見出し以降では、この背景にあるloggingモジュールの内部構造を詳しく見ていきます。
なぜBottleアプリでloggingモジュールのログが出力されないのか

Bottleアプリでログが出力されない根本的な原因を理解するためには、Python標準のloggingモジュールがどのような設計思想で動いているのかを知る必要があります。
感覚的に扱ってしまいがちなモジュールですが、内部にはロガー、ハンドラ、フォーマッタという明確な階層構造が存在します。
この構造を把握しないままBottleアプリに組み込むと、思わぬところでログが握りつぶされてしまうのです。
ルートロガーとハンドラの基礎知識
loggingモジュールには、必ず一つだけ存在するルートロガーと、各モジュールで個別に取得する名前付きロガーという概念があります。
ロガーはあくまでメッセージを受け付ける窓口であり、実際に出力を行うのはロガーに紐づけられたハンドラの役割です。
このハンドラが未設定のままでは、いくらロガーに向けてログを出力しても、実際の出力先が存在しないため、何も表示されません。
以下は、基本的な構成要素の関係を整理した表です。
| 要素 | 役割 | 未設定時の挙動 |
|---|---|---|
| ロガー | ログメッセージの受付窓口 | メッセージ自体は生成される |
| ハンドラ | 出力先への書き込み処理 | 出力先が存在せず消失する |
| フォーマッタ | 出力形式の整形 | デフォルト形式で出力される |
さらに注意すべき点として、名前付きロガーはデフォルトで伝播(propagate)という仕組みを持ち、自身にハンドラがなくても親のロガー、最終的にはルートロガーへメッセージを渡します。
この伝播先にもハンドラが存在しない場合、メッセージは静かに消えてしまいます。
ロガーをgetLogger(__name__)で取得しただけで満足してしまうと、この伝播の仕組みに気づかず、ログが出ない原因を長時間探し続けることになりかねません。
WSGIサーバーごとに異なるログ出力の仕組み
Bottleは開発用の簡易サーバーだけでなく、gunicornやuWSGIといった本番用のWSGIサーバー上でも動作します。
この選択によって、標準出力やエラー出力の扱われ方が大きく異なる点も、ログが消える原因の一つです。
- 開発用サーバー(wsgiref)では、標準出力がそのままターミナルに表示される
- gunicornでは、ワーカープロセスの標準出力がデーモン化により破棄される場合がある
- uWSGIでは、専用のログ設定を行わない限り、アプリケーション側のログが別経路に吸収されることがある
つまり、ローカル環境で正常に動作していたロガー実装が、本番のWSGIサーバー環境では機能しなくなるという事態が十分に起こり得るのです。
次の章では、こうした背景を踏まえたうえで、開発者が陥りやすい具体的なアンチパターンを検証していきます。
Bottle開発でやりがちなロガー実装のアンチパターン集

ここまで解説してきたloggingモジュールの仕組みを踏まえたうえで、実際にBottle開発の現場でよく見かけるアンチパターンを具体的に検証していきます。
いずれも一見動いているように見えるため、問題の根深さに気づきにくいという共通点があります。
basicConfigをリクエストごとに呼び出す誤り
最も頻繁に見られるアンチパターンが、リクエストハンドラの内部でlogging.basicConfig()を呼び出してしまう実装です。
@route('/hello')
def hello():
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
logger.debug('リクエストを受信しました')
return 'Hello'
basicConfig()は、ルートロガーにハンドラが一つも設定されていない場合にのみ効果を発揮する関数です。
つまり、初回のリクエストでハンドラが登録された後は、二回目以降にいくら呼び出しても設定は反映されません。
結果として、開発サーバーの起動直後は正しく動いているように見えても、リクエストのたびにログレベルを切り替えたいといった意図が反映されず、原因不明の挙動不一致に悩まされることになります。
ロガーの多重登録によるログ重複
次に多いのが、同じハンドラを何度も追加してしまい、一件のログが何行にもわたって重複出力される問題です。
Bottleの開発サーバーはコード変更を検知して自動リロードする機能を持っているため、ロガーの初期化処理がモジュールの再読み込みのたびに実行され、ハンドラが際限なく追加されてしまうことがあります。
- リクエストのたびに
logger.addHandler()を呼び出している - モジュールがインポートされるたびに初期化処理が実行される構成になっている
- リロード機能によって同じ初期化コードが複数回評価される
このような状態を防ぐには、ハンドラを追加する前に既存のハンドラの有無を確認する、あるいはアプリケーション起動時の一箇所でのみ初期化処理を行うといった対策が必要です。
モジュールごとに異なるロガー名を乱立させる
三つ目のアンチパターンは、モジュールごとに一貫性のない名前でロガーを取得してしまうことです。
logger = logging.getLogger('app_module_a')
このように文字列を直接指定する実装は、ファイルを分割・移動した際にロガー名とモジュールの対応関係が崩れやすく、後から見返した際にどのロガーがどこで使われているのか把握しづらくなります。
一般的にはgetLogger(__name__)を使い、モジュールの階層構造をそのままロガー名に反映させる方法が推奨されます。
この命名規則を守ることで、特定のモジュールだけログレベルを変更するといった柔軟な運用も可能になります。
これらのアンチパターンは、いずれも単体では小さな問題に見えますが、複数が組み合わさることで本番環境での深刻なトラブルへとつながっていきます。
アンチパターンが引き起こす本番運用でのトラブル事例

前章で挙げたアンチパターンは、開発環境では気づきにくいまま放置され、本番環境で運用が始まってから深刻な問題として表面化することが少なくありません。
ここでは、実際の現場でよく報告される二つのトラブル事例を取り上げ、その発生メカニズムを検証していきます。
ログが二重出力される問題
本番リリース後にログファイルを確認すると、同一のリクエストに対するログが二行、三行と重複して記録されているという相談は非常に多く寄せられます。
この現象の背景には、複数の原因が絡み合っているケースがほとんどです。
- アプリケーションの初期化処理がリロードやインポートのたびに複数回実行され、ハンドラが重複して追加される
- 名前付きロガーの伝播が有効なままルートロガーにもハンドラが設定されており、同じメッセージが二経路から出力される
- gunicornなどでワーカープロセスを複数起動しており、各プロセスが同じログファイルに書き込んでいる
特に二つ目の伝播に起因する重複は見落とされやすく、以下のような対応が必要になります。
logger = logging.getLogger(__name__)
logger.propagate = False
このようにpropagate属性を明示的に制御し、ロガーの責任範囲を明確にしておくことが、重複出力を防ぐ基本的な対策となります。
障害調査時にログが残っていない問題
もう一つの深刻な事例が、実際に障害が発生した際、肝心のエラーログがどこにも記録されていなかったというケースです。
これは開発時のログ出力先の確認不足が主な原因です。
例えば、開発環境ではコンソール出力のみで十分に運用できていたとしても、本番環境ではコンソールへの標準出力自体がデーモンプロセスの仕様上破棄されてしまうことがあります。
この事実に気づかないまま本番運用を続けると、次のような状況に陥ります。
| 状況 | 開発環境での見え方 | 本番環境での実際の挙動 |
|---|---|---|
| コンソール出力のみ設定 | 正常に表示される | 標準出力が破棄され記録が残らない |
| ログレベルがDEBUGのまま | 詳細な情報が見える | 出力過多でディスクを圧迫する恐れ |
| ファイル出力先が相対パス | カレントディレクトリで機能 | 起動ディレクトリの違いで書き込み失敗 |
障害調査という最もログを必要とする場面で記録が存在しないという事態は、システムの信頼性を大きく損ないます。
こうした事例を踏まえると、開発段階から本番環境を見据えたロガー設計を行うことがいかに重要か、改めて理解できるはずです。
次の章では、この教訓を踏まえた正しい設計の考え方を整理していきます。
Bottleアプリケーションに適した正しいロガー設計の考え方

ここまで検証してきたアンチパターンとトラブル事例を踏まえると、Bottleアプリケーションにおけるロガー設計には、明確な原則が必要であることが分かります。
その原則とは、ログの初期化処理を一箇所に集約し、実行環境に応じて設定を切り替えられる構造にするという点に集約されます。
アプリケーション起動時にロガーを一元管理する
まず前提として、ロガーの初期化はリクエストハンドラの中で行うべきではなく、アプリケーションの起動処理として独立させる必要があります。
具体的には、以下のような設計方針が有効です。
- ロガーの設定を行う関数を一つ用意し、
app.run()を呼び出す前に一度だけ実行する - 各モジュールでは
logging.getLogger(__name__)によってロガーを取得するだけにとどめ、設定処理自体は記述しない - ハンドラの追加は起動処理の中でのみ行い、リクエスト処理のたびに実行されないようにする
この方針を守ることで、前章で挙げたハンドラの多重登録や、意図しない設定の上書きといった問題を構造的に防ぐことができます。
責任の所在を明確に分離することが、保守性の高いロガー実装の第一歩といえるでしょう。
環境ごとにログレベルを切り替える設計
開発環境と本番環境とでは、必要とされるログの詳細度が大きく異なります。
開発時にはデバッグ情報を含めた詳細なログが有用である一方、本番環境で同じレベルのログを出力し続けると、ディスク容量の圧迫や処理性能への影響が懸念されます。
この課題に対しては、環境変数を用いてログレベルを動的に切り替える設計が有効です。
以下に、環境ごとの設定方針を整理します。
| 環境 | 推奨ログレベル | 主な出力先 |
|---|---|---|
| 開発環境 | DEBUG | 標準出力 |
| ステージング環境 | INFO | ファイルまたは標準出力 |
| 本番環境 | WARNING以上 | ファイルおよび外部ログ基盤 |
このように環境ごとの方針をあらかじめ定義しておくことで、コードを変更することなく、デプロイ先に応じた適切なログ出力を実現できます。
次の章では、こうした設計思想を実際に反映させた具体的なサンプルコードを示しながら、Bottleアプリケーションへの実装方法を解説していきます。
実装で学ぶBottle用ロガーのサンプルコードと設定方法

理論と設計方針を整理したところで、ここからは実際にBottleアプリケーションへ組み込める具体的なサンプルコードを示していきます。
前章で述べた「一元管理」と「環境ごとの切り替え」という二つの原則を、コードレベルでどのように実現するかを見ていきましょう。
logging.configを使った設定ファイルの実装
複数のハンドラやフォーマッタを扱う場合、コード内に設定を直接記述するよりも、logging.configモジュールを用いて設定を一元的に管理する方法が推奨されます。
辞書形式で設定を定義するdictConfigは、可読性が高く、環境変数と組み合わせやすいという利点があります。
import os
import logging.config
LOG_CONFIG = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'standard',
},
'file': {
'class': 'logging.handlers.RotatingFileHandler',
'filename': 'app.log',
'formatter': 'standard',
},
},
'root': {
'handlers': ['console', 'file'],
'level': os.environ.get('LOG_LEVEL', 'INFO'),
},
}
logging.config.dictConfig(LOG_CONFIG)
このように設定を一つの辞書にまとめておくことで、ハンドラの重複登録を防ぎつつ、環境変数LOG_LEVELによって開発環境と本番環境のログレベルを容易に切り替えられます。
この初期化処理は、アプリケーションのエントリーポイントで一度だけ呼び出すようにしてください。
Bottleのフックを利用したリクエストログの実装
ロガーの初期化が完了したら、次はBottle固有の機能を活用して、リクエストごとの情報を自動的に記録する仕組みを組み込みます。
Bottleには@hookデコレータが用意されており、リクエストの前後に任意の処理を挟み込むことが可能です。
from bottle import hook, request, response
logger = logging.getLogger(__name__)
@hook('after_request')
def log_request():
logger.info(
'%s %s -> %s',
request.method,
request.path,
response.status_code
)
この実装により、個々のルート関数の中に逐一ログ出力の記述を追加する必要がなくなり、アクセスログの記録漏れを防ぐことができます。
加えて、before_requestフックと組み合わせて処理時間を計測すれば、パフォーマンス監視の基礎データとしても活用できます。
このように、初期化処理とフックによる自動記録を組み合わせることで、保守性が高く、意図しないログ消失の起きにくい実装が実現できます。
本番環境でも安心なログ管理とローテーション設定

ロガーの実装が完成しても、本番環境で長期間運用を続けると、ログファイルが際限なく肥大化していくという新たな課題に直面します。
ディスク容量を圧迫し続けるログは、最悪の場合サーバー自体の障害につながりかねません。
この章では、ログファイルを適切に管理するための具体的な仕組みを解説します。
RotatingFileHandlerによるログローテーション
先の設定例ではRotatingFileHandlerをファイルサイズに応じたローテーションとして組み込みましたが、実務では時間の経過を基準にローテーションを行いたい場面も多くあります。
そのような場合には、日次や週次で自動的にファイルを切り替えるTimedRotatingFileHandlerが有効です。
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
filename='app.log',
when='midnight',
backupCount=14
)
when='midnight'と指定することで、毎日0時を境に新しいログファイルへと切り替わり、backupCountで指定した世代数を超えた古いログは自動的に削除されます。
障害調査の際に「いつ発生した問題なのか」で該当のログファイルを特定しやすくなる点も、時間軸でのローテーションを採用する利点です。
サイズ基準と時間基準、それぞれの特性を以下に整理します。
| ローテーション方式 | 適した用途 | 注意点 |
|---|---|---|
| サイズ基準 | アクセス数の変動が大きいサービス | ファイル数の増減が読みにくい |
| 時間基準 | 障害調査で日付を軸に探すサービス | アクセス集中時にファイルが肥大化 |
クラウド環境でのログ収集との連携
自前のサーバーでログファイルを管理するだけでなく、クラウド環境で運用する場合は、外部のログ収集基盤と連携させる設計も検討すべきです。
特に、複数のインスタンスやコンテナでBottleアプリケーションを動かしている場合、各サーバーに個別に残されたログファイルを都度確認するのは非効率です。
- 標準出力へのログを、クラウドプロバイダーのログ収集サービスに自動転送する構成にする
- コンテナ環境では、ファイルへの書き込みではなく標準出力への出力を基本方針とする
- 収集したログを一元的に検索・分析できる基盤と連携し、障害調査の効率を高める
このようにローカルでの保全策と、クラウド基盤での一元管理策を組み合わせることで、スケールしても破綻しない、堅牢なログ運用体制を構築できます。
Bottle開発のログ実装でよくある質問と注意点

ここまでBottleにおけるロガーの設計から実装までを一貫して解説してきましたが、実際に手を動かす中で疑問に感じやすいポイントがいくつか残っています。
この章では、特に質問として寄せられる頻度の高い二つのテーマを取り上げ、注意点を整理します。
gunicorn利用時の注意点
本番環境でBottleアプリケーションをgunicornと組み合わせて運用する場合、開発サーバーとは異なる挙動に戸惑うケースが多く見られます。
特に注意すべき点は、gunicornが複数のワーカープロセスを起動する仕組みを持っているという事実です。
- ワーカープロセスごとにPythonのプロセス空間が独立しているため、ロガーの初期化処理も各プロセスで個別に実行される
- 複数プロセスが同一のログファイルへ同時に書き込むと、書き込みタイミングによってはログが混在したり欠落したりする恐れがある
- gunicorn自体のアクセスログ機能と、アプリケーション側で実装した独自のロガーが重複して動作することがある
これらの問題を避けるためには、複数プロセスからの同時書き込みに対応した設計にするか、あるいは各プロセスの標準出力をgunicornの--access-logfileや--error-logfileオプションと連携させ、ログ集約の責任範囲を明確に分離しておくことが重要です。
デバッグモードとログレベルの関係
Bottleにはdebug(True)という開発を補助するモードが用意されていますが、このデバッグモードとログレベルの設定は、それぞれ独立した仕組みであるという点を正しく理解しておく必要があります。
from bottle import debug, run
debug(True)
run(host='localhost', port=8080)
debug(True)は、例外発生時に詳細なトレースバックをブラウザ上に表示したり、テンプレートの自動リロードを有効にしたりする機能であり、これ自体がloggingモジュールのログレベルを変更するわけではありません。
そのため、デバッグモードを有効にしただけでは、期待していたDEBUGレベルのログが出力されないという誤解が頻繁に生じます。
| 設定項目 | 制御する対象 | 混同しやすい誤解 |
|---|---|---|
debug(True) |
例外表示、自動リロード | ログレベルも変わると誤解しやすい |
ロガーのlevel設定 |
出力するログの詳細度 | debugモードと連動していない |
両者はあくまで別個の設定であるという前提に立ち、開発環境ではBottleのデバッグモードとロガーのログレベルを、双方とも明示的に指定しておくことをおすすめします。
まとめ:Bottle開発における正しいロガー実装のポイント

ここまで、Bottle開発において標準ログが消えてしまう現象の原因から、アンチパターンの検証、そして本番運用まで見据えた正しい実装方法までを一貫して解説してきました。
最後に、記事全体で取り上げてきた要点を整理し、今後の開発に活かせる形でまとめておきます。
まず前提として理解しておくべきなのは、ログが消える現象の多くはBottle自体の不具合ではなく、Python標準のloggingモジュールが持つ仕様と、開発者側の実装方法との間に生じるミスマッチが原因であるという点です。
ロガーとハンドラの役割の違いや、ハンドラが未設定であればメッセージが静かに握りつぶされてしまうという仕組みを理解しないままprint文の延長で実装を進めてしまうと、必ずどこかで同じ問題に直面します。
その上で、本記事で繰り返し指摘してきたアンチパターンを避けることが、安定したログ運用の第一歩となります。
改めて要点を整理すると、以下のようになります。
- ロガーの初期化処理はリクエストハンドラの中ではなく、アプリケーション起動時の一箇所に集約する
basicConfig()は一度しか効果を発揮しないという仕様を理解し、リクエストごとの呼び出しを避ける- ハンドラの多重登録や、ロガーの伝播による重複出力を防ぐため、責任範囲を明確に分離する
- ロガー名は
getLogger(__name__)で統一し、モジュール構造とロガー名を一致させる
これらのアンチパターンを踏まえて設計を見直すだけでも、開発中に感じていた原因不明のログ消失や重複といった問題の多くは解消されるはずです。
さらに、実際の運用フェーズを見据えるのであれば、開発環境と本番環境でログレベルを切り替える設計や、ログファイルが際限なく肥大化しないようにするローテーションの仕組みも欠かせません。
特にクラウド環境やコンテナ環境でBottleアプリケーションを動かす場合には、ローカルのファイルに依存せず、標準出力を起点としてログ収集基盤へ集約する構成が、スケーラビリティの観点からも有効な選択肢となります。
また、gunicornなどの本番用WSGIサーバーと組み合わせる際には、ワーカープロセスごとにログの初期化処理が個別に実行されるという特性を理解しておく必要があります。
この特性を軽視すると、複数プロセスからの同時書き込みによってログが混在したり、意図せず欠落したりするトラブルにつながりかねません。
Bottleのデバッグモードとロガーのログレベルが独立した仕組みであることも、混同しやすいポイントとして改めて意識しておくとよいでしょう。
ロギングという機能は、アプリケーションの表向きの動作には直接影響しないため、つい後回しにされがちな領域です。
しかし、障害発生時の調査や、システムの健全性を継続的に把握する上で、ログは開発者にとって最も頼りになる情報源となります。
本記事で紹介した設計原則と実装パターンを踏まえ、Bottleアプリケーションにおいても、消えることのない、信頼できるログ基盤を構築していただければ幸いです。
ログの仕組みを正しく理解することは、目先のデバッグ効率だけでなく、長期的に運用しやすいシステムを設計する力にもつながっていくはずです。


コメント