Python | 自動化:エラー通知

Python
スポンサーリンク

概要(エラー通知は「落ちたことを人間に伝える仕組み」)

エラー通知は、

「プログラムがコケた瞬間に、人間がちゃんと気づけるようにする仕組み」

です。

例外ログは「証拠を残す」もので、
エラー通知は「誰かに知らせる」ものです。

自動化が進んでくると、

  • 夜中にバッチが落ちていた
  • 誰も気づかなくて、朝まで放置
  • データが更新されていないのに業務が進んでしまった

みたいな事故が簡単に起こります。

だから、「落ちたら分かる」をコードに埋め込んでおくことが、
自動化を本番運用に耐えさせるための必須条件になってきます。


まず考えるべきこと(誰に・何で・いつ知らせるのか)

誰に通知するのか(宛先の設計)

最初に決めるべきは「宛先」です。

自分だけにメールが来ればいいのか、
チームの Slack に飛ばしたいのか、
運用担当のグループアドレスに送りたいのか。

ここが曖昧なまま実装を始めると、

  • 誰にも届いていない通知をせっせと送り続ける
  • 見ていないメールボックスが通知で溢れる

といった意味のない「自己満通知」になります。

「誰が」「どのタイミングで」「どのチャネルを見ているか」を、
一旦自分の頭の中か紙に書き出してみてください。

何で通知するのか(チャネルの選択)

典型的な手段は、だいたいこのあたりです。

メール(Gmail、会社のメール)
チャット(Slack、Teams、Discord など)
監視ツール(Zabbix、Datadog など。ここは今回は深追いしない)

プログラミング初心者が最初に手を出しやすいのは「メール」か「Slack Webhook」です。

メールはどの環境でも通じるし、
Slack はチームで開いていることが多いのでリアルタイム性が高い。

どちらにも共通しているのは、

  • 宛先や API URL などは、ソースコードにベタ書きしない
  • 環境変数や設定ファイルから読むようにする

という点です。これも後で触れます。

いつ通知するのか(トリガーの設計)

「全部のエラーで通知する」と考えると、すぐに破綻します。

細かい警告レベルまで全部通知すると、
人間が慣れてしまって「またか」で終わり、誰も見なくなります。

現実的には、次のような考え方で線を引きます。

バッチ全体が止まるような致命的エラー
→ 必ず通知する(CRITICAL / ERROR レベル)

一部のレコードだけ失敗したが、全体は続いている
→ ログには残すが、通知は「一定件数以上」などの条件付きにする

タイムアウトや一時的エラーでリトライして最終的に成功した
→ 通知は不要で、ログにだけ残す

この線引きを決めてから、どこで「エラー通知関数」を呼ぶかを考えると、実装がブレません。


例1:メールでエラー通知する(smtplib を使う基本形)

最小の「バッチ異常終了メール」のイメージ

まずは、「バッチ全体が落ちたら自分にメールする」という超シンプルな例から。

Python 標準の smtplib を使う方法です。
ここでは構造が分かればいいので、Gmail を送信元にするイメージで書きます(実際に使うときはアプリパスワードやセキュリティ設定が必要です)。

import smtplib
from email.message import EmailMessage
import logging
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
LOG_FILE = BASE_DIR / "batch.log"

def setup_logging():
    logging.basicConfig(
        filename=LOG_FILE,
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
    )

def send_error_mail(subject: str, body: str):
    msg = EmailMessage()
    msg["Subject"] = subject
    msg["From"] = "from@example.com"
    msg["To"] = "to@example.com"
    msg.set_content(body)

    with smtplib.SMTP("smtp.example.com", 587) as smtp:
        smtp.starttls()
        smtp.login("from@example.com", "PASSWORD")
        smtp.send_message(msg)

def main():
    logging.info("バッチ開始")
    # ここにメイン処理
    1 / 0  # わざとエラー
    logging.info("バッチ正常終了")

if __name__ == "__main__":
    setup_logging()
    try:
        main()
    except Exception:
        logging.exception("バッチで予期しない例外が発生しました")
        subject = "[ERROR] 日次バッチ異常終了"
        body = "日次バッチでエラーが発生しました。ログを確認してください。"
        try:
            send_error_mail(subject, body)
        except Exception as notify_err:
            logging.exception(f"エラー通知の送信にも失敗しました: {notify_err}")
        raise
Python

この例で押さえておきたいポイントを、少し深掘りします。

1つ目は、「エラー通知はトップレベルの try/except で行っている」ことです。
main() の中で何が起きても、最終的にはここで捕まり、ログ+メール通知が行われます。

2つ目は、「通知の失敗自体も例外になる可能性がある」ことを考慮している点です。
send_error_mail 自体もネットワークや認証などで失敗しうるので、その例外も logging.exception で記録し、最悪メールは飛ばなくてもログには残るようにしています。

3つ目は、「メール本文はシンプルにしておき、詳細はログを見る設計」にしていることです。
長文をメールに全部書こうとせず、「落ちたことが分かればよい」「詳しくは log を見て」という線引きで十分なことが多いです。

メールに「どのバッチがいつ落ちたか」を含める

もう少し実用的にするなら、件名や本文に最低限の文脈情報を含めます。

例えば、日付やバッチ名、環境名(dev / prod)などです。

from datetime import datetime

def notify_batch_error():
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    subject = f"[ERROR][prod] 日次売上バッチ異常終了 ({now})"
    body = f"日次売上バッチが異常終了しました。\n発生時刻: {now}\nログファイル: {LOG_FILE}"
    send_error_mail(subject, body)
Python

この程度でも、「どの環境のどのバッチが、いつ落ちたか」がメールだけで把握できます。


例2:Slack Webhook でエラー通知する(よくある現場パターン)

Slack Webhook の基本構造

Slack の Incoming Webhook を使うと、
決まった URL に POST するだけで特定チャンネルにメッセージを送れます。

設定は Slack の管理画面側で行う必要がありますが、
Python 側から見ると単なる HTTP POST です。

import requests
import logging

SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/XXXXX/XXXXX/XXXXX"

def send_slack_error(text: str):
    payload = {"text": text}
    resp = requests.post(SLACK_WEBHOOK_URL, json=payload, timeout=5)
    try:
        resp.raise_for_status()
    except Exception as e:
        logging.exception(f"Slack通知の送信に失敗: {e}")
Python

これをトップレベルのエラー通知に組み込みます。

def main():
    logging.info("バッチ開始")
    # メイン処理
    1 / 0
    logging.info("バッチ正常終了")

if __name__ == "__main__":
    setup_logging()
    try:
        main()
    except Exception:
        logging.exception("バッチで予期せぬ例外")
        try:
            send_slack_error("[ERROR] 日次バッチ異常終了。ログを確認してください。")
        except Exception as notify_err:
            logging.exception(f"Slack通知でもエラー: {notify_err}")
        raise
Python

このパターンも、メール版と考え方は同じです。

致命的エラーが起きたときに、
Slack の特定チャンネルに「落ちたよ」と一言飛ばすだけで、
誰かがすぐに気づけるようになります。

Slack メッセージに最低限含めたい情報

Slack はテキストなので、シンプルに書けます。

例えば、

  • [ERROR] or [WARN] などのレベル
  • バッチ名
  • 環境名(dev / staging / prod)
  • 発生時刻

あたりがあると、パッと見で判別しやすいです。

from datetime import datetime

def build_slack_message(batch_name: str):
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    return f":warning: [ERROR][prod] {batch_name} が異常終了しました。\n発生時刻: {now}\n詳細はログを確認してください。"
Python

実際のスタックトレースやエラー詳細はログに任せ、
Slack では「気づかせる」ことに専念するのがポイントです。


重要ポイント:エラー通知の「範囲」と「頻度」を決める

すべての例外で通知すると「通知疲れ」になる

エラー通知は、やろうと思えば

  • API のリトライ失敗1回ごと
  • ファイル1件の処理失敗ごと

に飛ばすこともできますが、これはほぼ確実に失敗パターンです。

通知が多すぎると、人間はすぐにこうなります。

  • 最初はちゃんと見る
  • そのうち「どうせまただろう」とスルーする
  • 本当に重大な通知も埋もれてしまう

これを避けるために、

  • バッチ全体が落ちたときだけ通知
  • 1バッチ中の失敗件数が一定以上になったときだけ通知
  • 同じ種類のエラーが短時間に連発したときだけ通知

といったルールを導入します。

「致命的なエラー」と「許容される失敗」の線引き

設計として、一度考えておいてほしいのが次の問いです。

この処理では、何が起きたら「人間が今すぐ気づくべき」なのか?
逆に、何が起きても「ログだけ残せばOK」なのか?

例えば、

日次バッチ全体が途中で止まった
→ 即通知(CRITICAL)

1ファイルだけ読めなかった(ログの1行が壊れていた)
→ ログに残す。通知は不要

API の一時的なエラーだが、リトライで最終的に成功した
→ ログに残す。通知は不要

API の失敗が 10件を超えた
→ 何かおかしいので Warning 通知

この線引きが決まっていれば、
「どの例外ハンドラで通知を呼ぶか」がはっきりします。


設計で気をつけるべきこと(セキュリティと堅牢性)

認証情報や URL をソースにベタ書きしない

メールのログイン情報や、Slack の Webhook URL を
ソースコードにそのまま書くのは、セキュリティ的に危険です。

最低限、

環境変数(os.environ)から読む
設定ファイル(.env や config.json)に外出しする

のどちらかはやっておいたほうがいいです。

import os

SMTP_USER = os.environ.get("SMTP_USER")
SMTP_PASS = os.environ.get("SMTP_PASS")
SLACK_WEBHOOK_URL = os.environ.get("SLACK_WEBHOOK_URL")
Python

こうしておけば、コードを Git に上げたり共有したりするときも、
認証情報が漏れにくくなります。

通知処理自体が本体を巻き込んで落ちないようにする

エラー通知は、「失敗のときにだけ呼ばれる処理」です。
ここでさらに例外が出ると、本来のエラー原因を覆い隠してしまうことがあります。

なので、

通知処理の中でも try/except を入れて、
通知失敗はログにだけ残し、元の例外の流れを壊さない

ように設計します。

先ほどの例でいうと、

try:
    send_slack_error(...)
except Exception as notify_err:
    logging.exception(f"エラー通知の送信にも失敗: {notify_err}")
Python

といった形です。

「エラー通知も完璧でなければならない」と思う必要はありません。
「最低でも元のエラー原因がログに残る」ことを優先してください。


まとめ(エラー通知は「動かしっぱなしにするための最後の一手」)

Python 自動化におけるエラー通知のポイントを整理すると、こうなります。

誰に・何で・いつ通知するのか(宛先・チャネル・トリガー)を先に決める。
トップレベルの try/except で致命的例外を捕まえ、例外ログとエラー通知(メール・Slackなど)をセットで行う。
通知内容には「どのバッチが」「どの環境で」「いつ落ちたか」を最低限含め、詳細はログに任せる。
全ての例外で通知せず、「致命的なエラー」や「異常な頻度で発生している失敗」に絞ることで、通知疲れを防ぐ。
通知処理自体も安全側に書き、認証情報やURLは環境変数などに外出ししてセキュリティを確保する。

エラー通知が入ると、プログラムは「動いている間だけ頼れるツール」から、
「落ちてもすぐ気づける、運用に耐えるツール」に変わります。

タイトルとURLをコピーしました