【Django】ServerError(500)になったら管理者にメール送信


投稿日 2024年7月13日 >> 更新日 2024年7月15日

概要

今回はサーバーエラー(ステータスコード500)が発生してしまった際に、管理人へメールを送信する機能を利用してみたいと思います。

サーバーエラーは何かしらの処理を実行させようとして発生するエラーなので、もしもユーザーがそこに出くわしてしまうとサイトの信用度はガタ落ちになるはずです。

ユニットテスト等をしっかり行っていれば防げることかと思いますが、人間が作るものなので多かれ少なかれミスは起こります。

ということで、SMTPサーバーの設定と送信者の設定等をするだけで「Server Error(500)」が起こった際にメールを送信させてみる内容です。

他にも送信メールの件名やメッセージに対する変更や追加設定、フィルター機能を使って特定のエラーによってはスキップする実装もします。

実行環境&使用ライブラリ

実行環境
Windows Subsystem for Linux
Python 3.6.9
使用ライブラリ ライセンス
Django==2.2.28 BSD

エラー時の処理内容

エラーが起こった際に実行される処理を確認してみます。

アクセスログやエラーログはPython標準ライブラリであるLoggingというモジュールを使用しており、Djangoはそのloggingモジュールをラップした独自のモジュールを持っています。

Djangoのデフォルトデフォルトロギングの設定は以下のようになっています。

# django/utils/log.py

DEFAULT_LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        },
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'formatters': {
        'django.server': {
            '()': 'django.utils.log.ServerFormatter',
            'format': '[{server_time}] {message}',
            'style': '{',
        }
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
        },
        'django.server': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'django.server',
        },
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    'loggers': {
        'django': {
            'handlers': ['console', 'mail_admins'],
            'level': 'INFO',
        },
        'django.server': {
            'handlers': ['django.server'],
            'level': 'INFO',
            'propagate': False,
        },
    }
}

handlersというキーで設定されている辞書は、各種辞書で定義されているlevelによって発生された時に実行される処理です。

"level":"ERROR"‘が発生した場合はmail_adminsが実行されます。

すでに管理者にメール送信するための設定はmail_adminsキーで定義されているので、ここは何も手を加えなくても大丈夫です(カスタマイズする際は手を加えます。)。

DEFAULT_LOGGING = {
    ...
    'handlers': {
       ...
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    ...
}

filtersキーに定義されているrequire_debug_falseは、「DEBUG=False」の時に実行されるという設定です。

classキーにはAdminEmailHandlerが設定されており、管理者用にメール送信するsend_adminsが内部で使われています。

実際は「DEBUG=False」の状態で実行される仕組みなので、メール送信機能を試すにはrequire_debug_falseのフィルターを外すか、DEBUGを「False」に設定する必要があります。

# settings.py

# DEBUG=Trueでサーバーエラー時にメールを送信する場合
# from django.utils.log import DEFAULT_LOGGING as LOGGING
# LOGGING['handlers']['mail_admins']['filters'] = []


DEBUG = False
# DEBUG=Falseの場合はHOSTヘッダーインジェクションを防止するため必須
ALLOWED_HOSTS = ['127.0.0.1']

あとはSMTPサーバーの設定と送信先(管理者)宛てのリストを作成するだけです。

SMTPサーバーの設定

SMTP(Simple Mail Transfer Protocol)サーバーとは、送信専用のプロトコル(規則)を持ったサーバーで、メールを送受信するいくつかのメールサーバーの内の1つです。

詳しくはNTT東日本のサイトで説明してくれています。

DjangoでSMTPサーバーの設定をするには、設定ファイルの「settings.py」で必要な変数に使用する企業のSMTPを定義していきます。

ここではGoogleの「Gmail」を設定していきます。

# settings.py

...

# SMTPサーバーの設定
# 以下はGmailのSMTPを設定しています。
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'exsample'
# EMAIL_HOST_USER = 'exsample@gmail.com'
EMAIL_HOST_PASSWORD = 'password'
EMAIL_PORT = 587
EMAIL_USE_TLS = True

上記は最低限必要とされる設定で、設定ファイル内の分かりやすい場所に定義しています。

各種変数の概要です。

変数 概要
EMAIL_HOST メーカーのSMTP
EMAIL_HOST_USER EMAIL_HOSTで設定したメーカーのアドレス名(@example.comは無くても動きました。)
EMAIL_HOST_PASSWORD EMAIL_HOSTで設定したメーカーのパスワード(取得方法はこちら
EMAIL_PORT EMAIL_HOSTに設定したメーカーに定められたポート番号
EMAIL_USE_TLS 通信するときに TLS (セキュア) 接続を使用するかどうか。

送信先(管理者)宛てのリストを作成

送信先(管理者)宛てのリストを作成するには、Djangoの設定ファイルに「ADMINS」変数を定義します。

Djangoでは既に「ADMINS」変数を保持していますが、デフォルト値は空のリストです。

リストの中にタプルかリストで管理者のメールアドレスと宛名を代入します。

ADMINS = [('name', 'address@exsample.com'),]

ADMINS = [['name', 'address@exsample.com'],]

複数に送信したい場合は、リストの中にタプルかリストを追加していきます。

ADMINS = [('name', 'address@gmail.com'), ('name', 'address@yahoo.co.jp')]

設定ファイルの全体像と送信のテスト

Djangoの設定ファイルの全体像(管理者メール関連)です。

# settings.py

INSTALLED_APPS = [
    ...
    send_admin,  # アプリ名
]

DEBUG = False
# DEBUG=Falseの場合はHOSTヘッダー必須
ALLOWED_HOSTS = ['127.0.0.1']

import os

# SMTPサーバーの設定
# セキュリティ対策のため、サーバーの環境変数から取得
EMAIL_HOST = os.getenv('EMAIL_HOST')
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
EMAIL_PORT = 587
EMAIL_USE_TLS = True

# 500エラーが発生した際のメール送信先
ADMINS = [('管理者', 'address@exsample.com'),]

サーバーエラー時のメール送信に使用する「views.py」です。

# send_admin/views.py

from django.core.exceptions import ViewDoesNotExist
from django.http import Http404, HttpResponseServerError
from django.views.generic import TemplateView


class Index(TemplateView):
    template_name = 'send_admin/index.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        req_dict = self.request.GET
        if req_dict:
            if req_dict['name'] == '500':
                raise HttpResponseServerError
            elif req_dict['name'] == '404':
                raise Http404
            elif req_dict['name'] == 'view':
                raise ViewDoesNotExist
            else:
                context["requ"] = req_dict['name']
        return context

アプリに使用する静的ファイルは以下です。

<!-- send_admin/templates/send_admin/index.html -->

<html lang="ja">
  <head>
    <!-- Webサイトタイトル -->
    <title>リクエスト</title>
  </head>
  <body>

    <h1>【Django】ServerError(500)になったら管理者にメール送信</h1>
    <br>
    <form method="GET">
      <fieldset>
        <legend>どのステータスコードを表示しますか?</legend>
        <div>
          <input type="radio" id="500" name="name" value="500" />
          <label for="huey">500</label>
        </div>
        <div>
          <input type="radio" id="404" name="name" value="404" />
          <label for="huey">404</label>
        </div>
        <div>
          <input type="radio" id="view" name="name" value="view" />
          <label for="huey">Viewエラー</label>
        </div>
        <div>
          <input type="radio" id="200" name="name" value="200" checked/>
          <label for="dewey">200</label>
        </div>
        <input type="submit" value="送信" />
      </fieldset>
    </form>
    <h1>{{ requ }}</h1>

  </body>
</html>

ステータスコード500で送信すると、サーバーエラーとなりメールが送信されます。

送信メール件名の頭文字を変更する

サーバーエラー時に送られてくるメールの件名は「[Django]ERROR (EXTERNAL IP): Internal Server Error: /」となっていますが、頭文字「[Django]」の部分がデフォルト値です。

デフォルト値を変更する方法は、EMAIL_SUBJECT_PREFIXに文字列を代入します。

# メール件名のプレフィックスを変更
EMAIL_SUBJECT_PREFIX = '[Send_Mail]'

Webページのコンテンツをそのままメールメッセージに表示する

Webページのコンテンツをそのままメッセージで表示するには、Logging設定値のmail_adminsキーにinclude_htmlキーを追加します。

Loggingの設定値は、django.utils.log.DEFAULT_LOGGINGにあります。

DEFAULT_LOGGING = {
    ...
    },
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler',
            'include_html': True,  # 追加
        }
    },
    ...
}

ログフィルターを使って特定のエラーは除外する

Loggingの設定値にDjangoのコールバックフィルターを設定することで、特定のサーバーエラー時はメールを送信しないようにすることができます。

Loggingの設定値は、django.utils.log.DEFAULT_LOGGINGにあります。

似たようなフィルターで、require_debug_falseが使用されています。

DEFAULT_LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        },
    ...
    },
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],  # DEBUG=Falseの際に実行される
            'class': 'django.utils.log.AdminEmailHandler',
        }
    },
    ...
}

上記のrequire_debug_falseフィルターと同じように、filtersキーに新しくdjango.utils.log.CallbackFilterクラスと自作のコールバック関数を追加します。

from django.core.exceptions import ViewDoesNotExist

# 自作のコールバック関数例
def original_callback_func(record):
    if record.exc_info:
        exc_type, exc_value = record.exc_info[:2]
        if isinstance(exc_value, ViewDoesNotExist):
            # Viewが存在しない場合のエラーはFalseを返す
            return False
    return True

DEFAULT_LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        },
        'original_callback_func': {  # 自作のコールバック関数
            '()': 'django.utils.log.CallbackFilter',
            'callback': original_callback_func,
        },
    ...
    },
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false', 'original_callback_func'],  # 追加
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    ...
}

自作のコールバック関数を「True」の返り値で通過するとメールが送信され、「False」の返り値で通過すると除外、詰まりメールは送信されません。

# 自作のコールバック関数例
def original_callback_func(record):
    if record.exc_info:
        exc_type, exc_value = record.exc_info[:2]
        if isinstance(exc_value, ViewDoesNotExist):
            # Viewが存在しない場合のエラーはFalseを返す
            return False
    return True

参考サイト

最後までご覧いただきありがとうございました。