メインコンテンツへスキップ

Flask ログの設定を行う

·5 分
ひとりアドベントカレンダー2024 Python Flask Logging
目次

はじめに
#

「OKAZAKI Shogo のひとりアドベントカレンダー2024」の16日目です。 Flask アプリでログの設定を入れていきます。

ログの設定
#

instance/config/dev.py にログの設定を記述する。dictConfig() を使用する。

今回は以下のような設定をする:

  • ログはアプリ、アクセス、エラー用でそれぞれファイル出力する
  • ファイルは日付ごとに分ける
  • ログファイルは logs/YYYY/MM と月ごとに分ける
  • コンソールにはデバッグレベルの全てのログを出力する
 1import os
 2from datetime import datetime, timedelta, timezone
 3
 4# 中略
 5
 6# ログの設定
 7# ログファイルを月ごとに分ける
 8# JST のタイムゾーンを考慮して、月ごとのタイムスタンプを取得
 9JST = timezone(timedelta(hours=9))
10current_time_jst = datetime.now(JST)
11year_month = current_time_jst.strftime("%Y/%m")
12
13# 年ごとのログディレクトリ
14LOG_FOLDER = os.path.join(BASE_DIR, "logs", year_month)
15if not os.path.exists(LOG_FOLDER):
16    os.makedirs(LOG_FOLDER)
17
18# ログファイルのパス
19ACCESS_LOG_FILE = os.path.join(LOG_FOLDER, f'access_{current_time_jst.strftime("%Y-%m-%d")}.log')
20ERROR_LOG_FILE = os.path.join(LOG_FOLDER, f'error_{current_time_jst.strftime("%Y-%m-%d")}.log')
21APP_LOG_FILE = os.path.join(LOG_FOLDER, f'app_{current_time_jst.strftime("%Y-%m-%d")}.log')
22LOGGING_CONFIG = {
23    "version": 1,
24    "disable_existing_loggers": False,
25    "formatters": {
26        "default": {
27            "format": "[%(asctime)s] %(levelname)-7s | %(name)s | %(funcName)s.%(lineno)d | %(message)s",
28        },
29        "app": {
30            "format": "[%(asctime)s] %(levelname)-7s | %(name)s | %(filename)s:%(lineno)d | %(message)s",
31        },
32    },
33    "handlers": {
34        # アクセスログ(Werkzeugのアクセスログ)
35        "access_log_handler": {
36            "level": "INFO",
37            "class": "logging.handlers.TimedRotatingFileHandler",
38            "filename": ACCESS_LOG_FILE,
39            "when": "midnight",  # 毎日0時に新しいファイルにローテーション
40            "interval": 1,  # 1日ごと
41            "backupCount": 30,  # 過去30日分のログを保持
42            "formatter": "default",
43        },
44        # エラーログ(エラー専用のログファイル)
45        "error_log_handler": {
46            "level": "ERROR",
47            "class": "logging.handlers.TimedRotatingFileHandler",
48            "filename": ERROR_LOG_FILE,
49            "when": "midnight",
50            "interval": 1,
51            "backupCount": 30,
52            "formatter": "default",
53        },
54        # アプリのログ(アプリケーションの動作を記録)
55        "app_log_handler": {
56            "level": "INFO",
57            "class": "logging.handlers.TimedRotatingFileHandler",
58            "filename": APP_LOG_FILE,
59            "when": "midnight",
60            "interval": 1,
61            "backupCount": 30,
62            "formatter": "app",
63        },
64        # コンソールログ(デバッグ用)
65        "console": {
66            "level": "DEBUG",
67            "class": "logging.StreamHandler",
68            "formatter": "default",
69        },
70    },
71    "loggers": {
72        # アクセスログ
73        "access": {
74            "handlers": ["access_log_handler"],
75            "level": "INFO",
76            "propagate": True,
77        },
78        # エラーログ
79        "error": {
80            "handlers": ["error_log_handler"],
81            "level": "ERROR",
82            "propagate": True,
83        },
84        # アプリケーションのログ
85        "app": {
86            "handlers": ["app_log_handler"],
87            "level": "INFO",
88            "propagate": True,
89        },
90        # すべてのログ
91        "": {
92            "handlers": ["console"],
93            "level": "DEBUG",
94            "propagate": False,
95        },
96    },
97}

Flask アプリへのログの設定
#

上記で設定したログの設定を app.config["LOGGING_CONFIG"] で取り出したものを dictConfig() に渡す。

app/__init__.py

 1import os
 2from logging.config import dictConfig
 3
 4# 中略
 5
 6def create_app(test_config=None):
 7    # appの設定
 8    app = Flask(__name__, instance_relative_config=True)
 9
10    # configファイルを読み込む
11    config_path = os.path.join("config", "dev.py")
12    app.config.from_pyfile(config_path)
13
14    # dev.py からログ設定を読み込んで適用
15    dictConfig(app.config["LOGGING_CONFIG"])
16
17    # 以下略

アクセスログとエラーログの出力設定
#

Flask アプリの設定で、アクセスログとエラーログを出力するように設定する。

  • @app.before_request で受け取ったリクエストを処理する前に実行する関数を指定する
    • 今回は、受け取ったリクエストの内容をアクセスログに出力する関数に付与する
  • @app.after_request でレスポンスをクライアントを処理する前に実行する関数を指定する
    • 今回は、返却するレスポンスの内容をアクセスログに出力する関数に付与する
  • @app.errorhandler(Exception) で全ての Exeption が発生した際に実行する関数を指定する
    • 今回は、 Exception が発生した場合に内容をエラーログに出力する関数を指定する

app/app.py に以下のように設定する:

 1import logging
 2
 3from flask import flash, redirect, request, url_for
 4from werkzeug.exceptions import RequestEntityTooLarge
 5
 6from app import create_app
 7
 8app = create_app()
 9
10
11if __name__ == "__main__":
12    app.run()
13
14
15@app.errorhandler(RequestEntityTooLarge)
16def handle_over_max_file_size(error):
17    flash("ファイルのサイズが大きすぎます。 50 MB 以下のファイルを選択してください。")
18    return redirect(url_for("regist_file_form.index"))
19
20
21def headers_to_string(headers):
22    return ", ".join(f"{key}: {value}" for key, value in headers.items())
23
24
25access_logger = logging.getLogger("access")
26error_logger = logging.getLogger("error")
27
28
29# 各リクエストの前後でログを記録
30@app.before_request
31def log_request_info():
32    request_info = (
33        f"Request: method={request.method}, url={request.url}, "
34        f"headers=[{headers_to_string(request.headers)}], "
35        f"body={request.get_data(as_text=True)}"
36    )
37    access_logger.info(request_info)
38
39
40@app.after_request
41def log_response_info(response):
42    response_info = f"Response: status={response.status}, headers=[{headers_to_string(response.headers)}]"
43    access_logger.info(response_info)
44    return response
45
46
47# 例外が発生したときのエラーログ
48@app.errorhandler(Exception)
49def handle_exception(e):
50    error_logger.error(f"Exception occurred: {e}", exc_info=True)
51    return "An error occurred", 500

アプリログの出力
#

次のようにして使うとアプリログが出力される:

 1import logging
 2
 3from flask import Blueprint, render_template
 4
 5index_bp = Blueprint("index", __name__, url_prefix="/")
 6
 7logger = logging.getLogger("app")
 8
 9
10@index_bp.route("/", methods=["GET", "POST"])
11def index():
12    logger.debug("インデックスページを表示")
13    return render_template("index.html", page_title="TOP")

ログの出力例
#

上記の設定では以下のようなログが出力される

アプリログ
#

logs/2024/12/app_2024-12-19.log

1[2024-12-19 00:49:56,350] INFO    | app | index.py:12 | インデックスページを表示

アクセスログ
#

logs/2024/12/access_2024-12-19.log

1[2024-12-19 00:49:56,350] INFO    | access | log_request_info.36 | Request: method=GET, url=http://127.0.0.1:5000/, headers=[Host: 127.0.0.1:5000, User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0, Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8, Accept-Language: ja,en-US;q=0.7,en;q=0.3, Accept-Encoding: gzip, deflate, br, zstd, Dnt: 1, Sec-Gpc: 1, Connection: keep-alive, Referer: http://127.0.0.1:5000/, Cookie: session=.eJwlzsENwzAIAMBdePcBBtuQZSJjg9pv0ryq7t5IXeB0H9jziPMJ2_u44gH7a8EGyk59SYRItqlOZs4SXBRXTNY6Wqc2q0nDtZhqRqpwiLXJY2YG3wQGSWPD0uvg0mwUJcuCZJU6usoKRx26HM1HWO0FRw3yDnfkOuP4bwi-P4qfLoc.Z2OxPA.dR8i3lpkOULIJvGYUZua5h85epw, Upgrade-Insecure-Requests: 1, Sec-Fetch-Dest: document, Sec-Fetch-Mode: navigate, Sec-Fetch-Site: same-origin, Sec-Fetch-User: ?1, Priority: u=0, i], body=
2[2024-12-19 00:49:56,355] INFO    | access | log_response_info.42 | Response: status=200 OK, headers=[Content-Type: text/html; charset=utf-8, Content-Length: 1088]
3[2024-12-19 00:49:56,376] INFO    | access | log_request_info.36 | Request: method=GET, url=http://127.0.0.1:5000/static/css/default.css, headers=[Host: 127.0.0.1:5000, User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0, Accept: text/css,*/*;q=0.1, Accept-Language: ja,en-US;q=0.7,en;q=0.3, Accept-Encoding: gzip, deflate, br, zstd, Dnt: 1, Sec-Gpc: 1, Connection: keep-alive, Referer: http://127.0.0.1:5000/, Cookie: session=.eJwlzsENwzAIAMBdePcBBtuQZSJjg9pv0ryq7t5IXeB0H9jziPMJ2_u44gH7a8EGyk59SYRItqlOZs4SXBRXTNY6Wqc2q0nDtZhqRqpwiLXJY2YG3wQGSWPD0uvg0mwUJcuCZJU6usoKRx26HM1HWO0FRw3yDnfkOuP4bwi-P4qfLoc.Z2OxPA.dR8i3lpkOULIJvGYUZua5h85epw, Sec-Fetch-Dest: style, Sec-Fetch-Mode: no-cors, Sec-Fetch-Site: same-origin, If-Modified-Since: Wed, 04 Dec 2024 12:48:52 GMT, If-None-Match: "1733316532.6492927-644-2100238851", Priority: u=2], body=
4[2024-12-19 00:49:56,376] INFO    | access | log_response_info.42 | Response: status=304 NOT MODIFIED, headers=[Content-Disposition: inline; filename=default.css, Content-Type: text/css; charset=utf-8, Content-Length: 644, Last-Modified: Wed, 04 Dec 2024 12:48:52 GMT, Cache-Control: no-cache, ETag: "1733316532.6492927-644-2100238851", Date: Thu, 18 Dec 2024 15:49:56 GMT]

エラーログ
#

わざとエラーを発生させて出力。

logs/2024/12/error_2024-12-19.log

 1[2024-12-19 00:49:54,291] ERROR   | error | handle_exception.49 | Exception occurred: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
 2Traceback (most recent call last):
 3  File "/Users/shogo/Library/Caches/pypoetry/virtualenvs/bshssa-member-system-GhAlEoqL-py3.8/lib/python3.8/site-packages/flask/app.py", line 880, in full_dispatch_request
 4    rv = self.dispatch_request()
 5  File "/Users/shogo/Library/Caches/pypoetry/virtualenvs/bshssa-member-system-GhAlEoqL-py3.8/lib/python3.8/site-packages/flask/app.py", line 854, in dispatch_request
 6    self.raise_routing_exception(req)
 7  File "/Users/shogo/Library/Caches/pypoetry/virtualenvs/bshssa-member-system-GhAlEoqL-py3.8/lib/python3.8/site-packages/flask/app.py", line 463, in raise_routing_exception
 8    raise request.routing_exception  # type: ignore[misc]
 9  File "/Users/shogo/Library/Caches/pypoetry/virtualenvs/bshssa-member-system-GhAlEoqL-py3.8/lib/python3.8/site-packages/flask/ctx.py", line 362, in match_request
10    result = self.url_adapter.match(return_rule=True)  # type: ignore
11  File "/Users/shogo/Library/Caches/pypoetry/virtualenvs/bshssa-member-system-GhAlEoqL-py3.8/lib/python3.8/site-packages/werkzeug/routing/map.py", line 629, in match
12    raise NotFound() from None
13werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.

参考資料
#