狛ログ

2021年12月27日月曜日

【Python】loggingでログをカスタマイズする【Flask】


狛ログをご覧のみなさん、はじめまして。
2021年にオフィス狛に入社いたしました、nago(なご)です。
技術に関するブログ記事を書くのは初めてですが、有益な情報を投稿できるように頑張ります。
よろしくおねがいします☺

さて、今回はPython(Flask)で、お試し開発をしていた時に苦戦したログ出力についてまとめたいと思います。

作ろうとしていたのは、外部のREST APIにリクエストを投げて、何かしらの結果を受け取り、画面に表示させる機能です。
まず、先輩社員が投稿してくださっていたこちらの記事を参考にして、AWS API GatewayでAPIのMockを作成しました。

正しい合言葉をidに設定してリクエストすると、ステータスコードが200となり、とってもタメになる情報を受け取れるAPIにしました。
合言葉を間違えていたらステータスコードが401となりエラーが返ってきます。

  

  
↓ ↓ ↓
合言葉を間違えると401エラーとなり、以下のメッセージが返ってきます。
  
アプリケーションは以下のような構成で作成しました。
 	 
     api-test/
         ├ static/
         │ └ styles.css
         ├ templates/
         │ └ api_test.html
         ├ views/
         │ ├ __init__.py
         │ └ api_test.py
         ├ __init__.py
         ├ app.py
         ├ get-pip.py
         ├ Pipfile
         └ Pipfile.lock
   	 
  	

コードはこちら。
・api_test.html
    
      <!doctype html>
      <html lang="ja">

        <head>
            <meta charset="utf-8">
            <title>APIテスト</title>
            <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
        </head>

        <body>
            <form action="{{ url_for('api_test.call') }}" method="POST">
                <input type="text" name="id">
                <input type="submit" value="API CALL">
            </form>
            <input type="button" onclick="location.href=&#39;{{ url_for('api_test.show_api_test') }}&#39;" value="RESET">
            <p>{{ message }}</p>
        </body>
     
  

・views/app.py
    
        from flask import Flask, redirect, url_for
        from views.api_test import api_test

        app = Flask(__name__)

        app.register_blueprint(api_test)
			
        # 最初の画面へリダイレクト
        @app.route("/")
        def redirect_to_api_test():
            return redirect(url_for("api_test.show_api_test"))

        if __name__ == "__main__":
            app.run(host="0.0.0.0", port=8080)
    
  

・api_test.py
 	 
      import requests, json

      from flask import Blueprint
      from flask import render_template, request

      api_test = Blueprint("api_test", __name__)

      # 画面表示(初期 & リセット時)
      @api_test.route("/")
      def show_api_test():
          message = ""
          return render_template(
              "api_test.html",
              message=message
          )

      # APIリクエスト & レスポンス受け取り & 画面表示
      @api_test.route("/call", methods=["POST"])
      def call():

          # APIGatewayでMock作成時に設定されたURL
          url = "https://xxxx.amazonaws.com/api-test/my/info"
          # クエリ文字列を作成
          payload = { "id": request.form.get("id") }
          # リクエストヘッダを作成
          headers = { "Content-Type": "application/json" }
          
        # リクエスト & レスポンス受け取り
        response = requests.post(url, data=json.dumps(payload), headers=headers)
        # レスポンスからメッセージを取り出す
         message = response.json()["message"]

          # 画面遷移 & メッセージ表示
          return render_template(
              "api_test.html",
              message=message
          )
	 
  

これで、APIから返ってきたメッセージをそのまま画面に表示することができました。
次に、APIがコールされるたびに、そのリクエスト内容と結果のログを出力する仕組みを作ろうと思います。

Pythonでログを出力するには、Pythonで用意されているloggingというモジュールを使います。
今回は設定ファイルを既存のファイルとは別に作成して、ログの設定をしてみます。
私はプロジェクトルートに「config」という名前のディレクトリを作り、その中にログの設定ファイルを作りました。
また、ログファイルを出力したかったので、ログファイルを置くための「log」ディレクトリも作成しました。

以下が作成したディレクトリと設定ファイルです。

 	 
     api-test/
         ├ config/
         │ ├ __init__.py
         │ └ logging_config.py
         ├ log/
         ├ static/
         │ └ styles.css
         ├ templates/
         │ └ api_test.html
         ├ views/
         │ ├ __init__.py
         │ └ api_test.py
         ├ __init__.py
         ├ app.py
         ├ get-pip.py
         ├ Pipfile
         └ Pipfile.lock
   	 
  	
※赤字が追加分

・config/logging_config.py
    
    from logging import DEBUG, INFO, config
    import datetime

    # ファイル名に入れる日付フォーマット
    today = datetime.date.today().strftime("%Y-%m-%d");

    config.dictConfig(
        {
            "version": 1,
            # ログフォーマット設定
            "formatters": {
                "formatter": {
                    "format": "%(levelname)s  %(asctime)s [%(module)s] %(message)s"
                }
            },
            # ハンドラー設定
            "handlers": {
                # 標準出力ハンドラー
                "streamHandler": {
                    "class": "logging.StreamHandler",
                    "formatter": "formatter",
                    "level": INFO
                },
                # ファイル出力ハンドラー
                "fileHandler": {
                    "class": "logging.FileHandler",
                    "formatter": "formatter",
                    "level": INFO,
                    # ログファイルを出力したい場所のパスとファイル名を指定
                    "filename":f"xxxx/api-test/log/{today}_api-test.log",
                    "encoding": "utf-8"
                }
            },
            # ロガー設定
            "loggers": {
                "basicLogger": {
                    "handlers": [
                        "streamHandler",
                        "fileHandler"
                    ],
                    "level": INFO,
                    "propagate": 0
                }
            }
        }
    )
    
  

ログの設定は、logging.config.dictConfigに辞書(Dictionary型の設定値)を渡すか、
logging.config.fileConfigにconfigparser形式ファイルを渡すかで設定できますが、
私は今回はdictConfigで設定してみました。

ログを出力するには、まずLogger(ロガー)の取得が必要なのですが、そこにHandler(ハンドラー)を指定することで出力形式の指定をプラスできるようになっています。
また、出力フォーマットも設定したい場合は、ハンドラーに対してFormatter(フォーマッター)を指定します。

ロガー、ハンドラー、フォーマッターの名前は自由につけることが出来ます。
私はロガーは「basicLogger」を作り、そこに標準出力ハンドラーの「streamHandler」と、ファイル出力の「fileHandler」をくっつけました。フォーマッターは「formatter」にしました(そのまんま)

"varsion" : 1 ってなんぞやと思い調べてみたのですが、辞書スキーマのバージョンのことでした。
現在は1しかないので、1を入れておけばいいそうです。こちらはdictConfigに辞書を渡す場合は必須のため、入れないと怒られます。

さて、ようやくapi_test.pyで設定ファイルを読み込んで、ログ出力してみます。

・views/api_test.py
    
    import requests, json
    from logging import getLogger

    from flask import Blueprint
    from flask import render_template, request

    from config import logging_config

    api_test = Blueprint("api_test", __name__)

    # ログ設定ファイル読込
    logging_config
    # ロガー取得
    logger = getLogger("basicLogger")

    # 画面初期表示
    @api_test.route("/")
    def show_api_test():
        message = ""
        return render_template(
            "api_test.html",
            message=message
        )

    # APIリクエスト & レスポンス受け取り & 画面表示
    @api_test.route("/call", methods=["POST"])
    def call():

        # APIGatewayでMock作成時に設定されたURL
        url = "https://xxxx.amazonaws.com/api-test/my/info"
        # クエリ文字列を作成
        payload = { "id": request.form.get("id") }
        # リクエストヘッダを作成
        headers = { "Content-Type": "application/json" }

        # リクエストログ出力
        logger.info("request : url = " + url + ", payload = " + str(payload))

        # リクエスト & レスポンス受け取り
         response = requests.post(url, data=json.dumps(payload), headers=headers)
        # レスポンスからステータスコードとメッセージを取り出す
         status_code = response.status_code
         message = response.json()["message"]

        # レスポンスログ出力
        if status_code == 200:
            logger.info("response : status_code = " + str(status_code) + ", message = " + message)
        else:
            logger.error("response : status_code = " + str(status_code) + ", message = " + message)

        # 画面遷移 & メッセージ表示
        return render_template(
            "api_test.html",
            message=message
        )
    
  
※赤字は追加分

・log/2021-12-15_api-test.log (2021/12/15にログが作成された場合のファイル名)
    
      INFO  2021-12-15 14:23:53,701 [api_test] request : url = https://xxxx.amazonaws.com/api-test/my/info, payload = {'id': 'sushi'}
      INFO  2021-12-15 14:23:53,778 [api_test] response : status_code = 200, message = お寿司はおいしい。猫はかわいい。ポメラニアンもかわいい。
      INFO  2021-12-15 14:24:46,184 [api_test] request : url = https://xxxx.amazonaws.com/api-test/my/info, payload = {'id': ''}
      ERROR  2021-12-15 14:24:46,254 [api_test] response : status_code = 401, message = 合言葉が違うようです・・・
    
  
正常パターンとエラーパターンを実行してみました。
フォーマッターで設定した「%(levelname)s %(asctime)s [%(module)s] %(message)s」の通りに表示されています。
出力時にapi_test.pyで設定したメッセージが「%(message)s」に代入されている形です。

調べてみると、loggingは思ったより奥が深くびっくりしました。
覚えておくと出したいログを見やすく出すことができて便利だと思います。

今回はこれでおしまいです。
ここまでお読みいただきありがとうございました。

参考リファレンス:
Logging HOWTO
logging.config --- ロギングの環境設定

, , ,

0 件のコメント:

コメントを投稿