【discord.py】Sentryを使ってエラー監視する


## はじめに

discord botやその他アプリケーションを運用する際、エラーを一元管理するためにSentryを導入しました。

その際にdiscord botで行った手順を紹介します。

## Sentryとは

[Sentry](https://sentry.io/welcome/) とは、エラーを検知、監視できるサービスです。
複数アプリケーションのエラーを一元管理できます。

## エラー監視方法

### プロジェクトの作成

![Create a new Project](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266322/9b57680b-ff86-3c54-4bf1-2f9640896df3.png)

Choose a platform： 「PYTHON」を選択します。

Set your default alert settings：デフォルトの「I'll create my own alerts later」を選択します。

プロジェクトに名前を付けてください：プロジェクト名を設定します。

### Sentry用環境変数の設定

プロジェクトの作成完了後、使い方が表示されます。

![Configure Python](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266322/26c07bf2-f4af-c162-4243-9e97bcd58972.png)

この使い方に従えばSentryでエラー監視できます。
ただし、 `sentry_sdk.init` の最初の引数、DSNはハードコーディングを避け、環境変数として設定しましょう。

DSNとはData Source Nameの略で、エラーなどイベントの送信先です。Sentryのプロジェクトごとに作成されます。
DSNが外部に流出すると、あなたのプロジェクトに第三者がイベントを送信する可能性があります。
そのため環境変数としてDSNを設定しましょう。

今回は `.env` を使って環境変数を設定します。

1. `.env` を扱うためのパッケージをインストール

    PipenvやPoetryなどパッケージ管理ツールを使う場合は適宜読み替えてください。

    ```
    pip install python-dotenv
    ```
1. `.env` ファイルを作成

    ```bash:.env
    SENTRY_DSN="https://xxxxxxxxxxxx"
    SENTRY_ENVIRONMENT="development"
    ```

    `SENTRY_DSN` の値にDSNを設定します。未設定の場合、Sentryへイベント送信を行いません。

    `SENTRY_ENVIRONMENT` ではproduction、developmentなどの環境を表す文字列を設定できます。

    Sentryでは他にも環境変数を設定できます。詳細は、[公式ドキュメント](https://docs.sentry.io/platforms/python/configuration/options/) を参照ください。

### 例外のキャプチャ

Sentryはデフォルトでは、キャッチされない例外、ログレベルERROR以上のログをイベントとして送信します。
discord.pyはBotが停止しないよう、デフォルトでエラーをハンドリングしていて、そのままではイベントが送信されません。

コマンドは [on_command_error](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html?#discord.ext.commands.Bot.on_command_error) 、タスクは [tasks.Loop.error](https://discordpy.readthedocs.io/ja/latest/ext/tasks/index.html#discord.ext.tasks.Loop.error) 内で `capture_exception` を使ってエラーをキャプチャする必要があります。

```python
import os

import sentry_sdk
from discord.ext import commands, tasks
from dotenv import load_dotenv
from sentry_sdk import capture_exception


class MyBot(commands.Bot):
    async def on_ready(self):
        print(f"Ready: {self.user}")
        self.add_cog(MyCog(self))

    async def on_command_error(self, context: commands.Context, exception: Exception):
        capture_exception(exception)
        return await super().on_command_error(context, exception)


class MyCog(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot
        self.do_task.start()

    @commands.command()
    async def ping(self, ctx: commands.Context):
        await ctx.send("pong")

    @tasks.loop(minutes=1)
    async def do_task(self):
        1 / 0

    @do_task.error
    async def task_error(self, exception: Exception):
        capture_exception(exception)


def main():
    load_dotenv()
    sentry_sdk.init(traces_sample_rate=1.0)

    token = os.environ["DISCORD_BOT_TOKEN"]
    bot = MyBot(command_prefix="/")
    bot.run(token)


if __name__ == "__main__":
    main()
```

注：コード例では環境変数 `DISCORD_BOT_TOKEN` にdiscord botのtokenを設定しています。

存在しないコマンドを入力したり、時間経過でタスクが実行されると、画像のようにIssueへエラーが追加されていきます。

![Issues](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266322/82368410-47ec-5f0d-d58d-48e494b8a148.png)

### Discord/Slackとの連携

このままではエラーが発生しても通知が来ず不便です。
DiscordやSlackと連携して、エラーの通知が来るようにしましょう。

現在、Slack integrationは有料プランのみになっていますが、Slack(Legacy)のURLに直接アクセスすれば無料プランでも設定可能なようです。

以下の記事を参考にして、設定することができました。

https://blog.m6a.jp/entry/2020/12/02/230257

Webhook URLを変更すれば、Slackも通知可能です。

## 参考URL

[Python | Sentry Documentation](https://docs.sentry.io/platforms/python/)

[エラー監視には Sentry が超便利！ - Qiita](https://qiita.com/Chanmoro/items/a9cbde57fd6c0926b5b4)

[Sentryのエラー通知をDiscordに送信する - #えむけーろぐ](https://blog.m6a.jp/entry/2020/12/02/230257)