Sentryを使ってプログラム上のエラーをトラッキング -dockerでお試し～外部連携-

サービスの運用として、信頼性を高めるには、インフラ側の状況を把握しているだけでは駄目で、アプリケーション側やユーザサイドでどんなことが起こっているかも把握していないといけないです。
そんな用途にうまく活用できるかもしれないSentryを試してみたので情報を残しておきます。

# Sentryとは？
アプリケーションで発生するエラーをトレースして管理できるツールです。
様々な種類のプログラムに対応していて、サービス型およびOSSで提供されています。

[Sentry公式サイト](https://sentry.io)
[Sentry Githubリポジトリ](https://github.com/getsentry/sentry)

プログラムの処理内にRavenというツールを組込み、例外処理をフックすることで発生したエラー情報をSentryサーバに送付するといった仕組みのようです。

基本的な使い方等は様々なブログ記事が既にあるので割愛します。

---
* 参考)
    * http://d.hatena.ne.jp/heavenshell/20130325/1364224078
    * https://qiita.com/ikuyamada/items/4e3ad4fdfd22090b918a

---

# dockerイメージを使ってサクッと試してみる

今回は、Sentryがオフィシャルで提供しているコンテナイメージを使って手元でサクッと試す手順を紹介します。
公式のドキュメントに詳細な記載があるので基本的にはこのままの手順で実施すればOKです。

今回試した環境は以下。

* AWS上のEC2インスタンス
* その上にDocker導入済み
* Sentryはバージョン8.22.0

[公式ドキュメント](https://docs.sentry.io/server/installation/docker/)
[Docker Hub上の解説](https://hub.docker.com/_/sentry/)

## sentryで利用されるredis用のコンテナを起動

```shell-session
$ docker run -d --name sentry-redis redis
```

## sentryのデータ管理に利用されるPostgreSQL用のコンテナを起動

```shell-session
$ docker run -d --name sentry-postgres -e POSTGRES_PASSWORD=secret -e POSTGRES_USER=sentry postgres
```

## sentryで利用するシークレットキーを生成する。
```shell-session
$ docker run --rm sentry config generate-secret-key
```
キーが標準出力されるのでこの内容をメモ。

## 初期設定実施
```shell-session
$ docker run -it --rm -e SENTRY_SECRET_KEY='先程出力されたシークレットキー' --link sentry-postgres:postgres --link sentry-redis:redis sentry upgrade
```
DBの設定等の初期化処理が行われます。
途中、以下のように初期ユーザの登録を促す内容が出力されるので、指示に従ってemailアドレス、パスワード等を登録しておきます。

```
Created internal Sentry project (slug=internal, id=1)

Would you like to create a user account now? [Y/n]: 
```

コンテナをうまく使って、コンテナ内で初期化のプロセスを走らせて、処理が終わればそのコンテナは用済みで削除するといった感じです。
初期登録されたデータはPostgreSQL用のコンテナ経由でDBに反映されています。

## Sentryサーバを起動

ここまでできれば、Sentryサーバを起動させます。

```
$ docker run -d --name my-sentry -p 8080:9000 -e SENTRY_SECRET_KEY='先程設定したシークレットキー' --link sentry-redis:redis --link sentry-postgres:postgres sentry
```

## Sentryサーバにアクセス

この状態で、Dockerホストの8080ポートでSentryサーバが稼働しています。
http://dockerホストIP:8080
にアクセスしてみます。
すると初期設定の画面が表示され、画面の指示に従って登録します。

![init_config_welcome_page.png](https://qiita-image-store.s3.amazonaws.com/0/45851/a5c2de37-ac82-5f27-dcdc-040e113cc2a2.png)

## プロジェクトの登録

初期設定が終われば、まずはプロジェクトを登録します。

ダッシュボードの[New Project]から名前やどの言語のプロジェクトかの選択等行います。
![dashboard.png](https://qiita-image-store.s3.amazonaws.com/0/45851/54bcfb5b-01e2-0d75-46ac-6cce68543665.png)

すると、新しいPJの登録が完了し、アプリケーション側でどのような設定をすれば良いかの案内が表示されます。
![create_pj.png](https://qiita-image-store.s3.amazonaws.com/0/45851/5a7dab2e-66be-c5ea-d408-45abacacb517.png)

今回試しに作成したのは、PythonのPJだったので、以下のような案内が提示されます。

### Pythyonのモジュールの導入
```shell-session
$ pip install raven --upgrade
```

### コード内に以下の処理を組込

```py3
from raven import Client

client = Client('http://アクセスキー:シークレットキー@xx.xx.xx.xx:8080/2')

try:
    1 / 0
except ZeroDivisionError:
    client.captureException()
```

上記はお試しのコードなので、無理やり例外発生させていますが、任意の処理の箇所で
例外発生時にclient.captureException()を呼び出してやれば連携が可能になります。

ポイントは、Clientを作成する際に、SentryサーバのURLやキー情報を指定している点です。

任意のポイントで以下のようにイベントを強制的に発行させることも可能なようです。

```py3
client.captureMessage('何らかのメッセージ')
```

## 画面上でのイベントの確認
あとは、先程アクセスしたSentryの管理画面上にどんどん発行されたイベント情報が現れてきます。
![eventdata.png](https://qiita-image-store.s3.amazonaws.com/0/45851/13635939-c3b9-30d0-8e66-4d8e2b15409c.png)

プログラム内で発生した例外をエラーとしてキャッチ。そもそもプログラムが稼働せずにダウンしてしまうようなものはFatalのエラーとして自動的に登録されてきました。
同様のエラー内容であれば新規登録ではなく、同じものの発生カウントが増えるようになっていたり、発生しているホストのホスト名を自動的に展開してくれたりと発生原因を追いやすいような工夫がされています。


以上がとりあえず動かすサンプルです。
以降は少し応用編の使い方を紹介します。

----
# 外部連携

SentryはSentryサーバに情報を集約して状況を確認するという使い方に加え、外部の様々なツール/サービスに連携できる仕組みを持っています。
外部連携でどれを有効/無効にするかはPJ毎に選択できるようになっています。
標準で、GitHub、Slack等々に対応しています。
サードパーティプラグインだと、ZabbixとかZendeskとか種々対応しています。

また、自前でカスタマイズして組み込むことも簡単にできます。
ここでは、独自の外部連携機能を実装する方法をご紹介します。

外部連携のパターンとしては、以下の4パターンあるようです。
AlertsとData Forwardingの使い分けとかあまり違いがよくわからなかったのですが、それぞれどういったものかのイメージは以下のようです。

* Alerts
    * Sentryサーバに送付されてきた例外イベントに対し、フィルタリングを行い特定のものを外部連携させる
    * 例) HipChat, PagerDuty, Slack, Zabbix等
* Issue Tracking
    * Sentryサーバに送付されてきた例外イベントを課題として外部連携させる
    * 例) GitHub, GitLab, JIRA, Asana等
* Release Tracking
    * 外部ツール・サービスでリリース管理を行っている場合に、そのリリース情報をSentryサーバに取り込み連携させる。リリース情報をWeb Hookとかで登録できるようになっている模様。
    * 例) Heroku
* Data Forwarding
    * Sentryサーバに送付されてきた例外イベントを分析のために全て転送する
    * 例) Amazon SQS, SEGMENT等

Sentryサーバで受信したデータを外で利用したい場合にはAlerts/Issue Tracking/Dat Forwardingが良さそうです。

# 外部連携機能を動かすための事前設定

外部連携をPJ毎に有効/無効に設定するだけでは連携機能は動かないようです。
連携処理を行うworkerを稼働させる必要があるようです。
以下のようにworkerおよびcron処理のコンテナを稼働させます。

```shell-session
$ docker run -d --name sentry-cron -e SENTRY_SECRET_KEY='シークレットキー' --link sentry-postgres:postgres --link sentry-redis:redis sentry run cron
$ docker run -d --name sentry-worker-1 -e SENTRY_SECRET_KEY='シークレットキー' --link sentry-postgres:postgres --link sentry-redis:redis sentry run worker
```

これで準備OKです。


# カスタム外部連携モジュール作成

今回、ひとまずサンプルとして、Data Forwardingに対応した連携モジュールを動かしてみます。
SentryはPythonで実装されているため、pythonのモジュールとして実装します。
あまり詳しいガイドは公開されていない？ようなので、既存のモジュールを参考にしつつ作ってみます。

作成したファイル群は以下になります。
sentry-sampleという名称で連携モジュールを作成します。

```
./
├── setup.py
└── src
    └── sentry_sample
        ├── __init__.py
        └── plugin.py
```

各ファイルの中身は以下です。

```py3:setup.py
#!/usr/bin/env python
from setuptools import setup, find_packages

install_requires = [
    'sentry>=8.0.0',
]


setup(
    name='sentry-sample',
    version='0.0.1',
    author='Daisuke Ikeda',
    author_email='dai.ikd123@gmail.com',
    url='http://github.com/ike-dai/',
    description='[Sample]A Sentry extension which send errors stats',
    long_description="sample",
    license='MIT',
    package_dir={'': 'src'},
    packages=find_packages('src'),
    install_requires=install_requires,
    entry_points={
        'sentry.plugins': [
            'sentry_sample = sentry_sample.plugin:SamplePlugin'
        ],
    },
    include_package_data=True,
    zip_safe=False,
    classifiers=[
        'Intended Audience :: Developers',
        'Intended Audience :: System Administrators',
        'Operating System :: OS Independent',
        'Programming Language :: Python',
        'Framework :: Django',
        'Topic :: Software Development'
    ],
    keywords='sentry sample',
)
```

```py3:src/sentry_sample/__init__.py
from __future__ import absolute_import
```

```py3:src/sentry_sample/plugin.py
from __future__ import absolute_import

from sentry.plugins.bases.data_forwarding import DataForwardingPlugin
from sentry.utils import json

from sentry_plugins.base import CorePluginMixin

class SamplePlugin(CorePluginMixin, DataForwardingPlugin):
    title = 'Sample'
    author = 'Daisuke Ikeda'
    author_url = 'https://github.com/ike-dai'
    version = '0.0.1'
    slug = 'sample'
    description = '[Sample] Forward Sentry events'
    conf_key = 'sample'

    def get_config(self, project, **kwargs):
        return [
            {
                'name': 'file_path',
                'label': 'File path',
                'type': 'text',
                'placeholder': '/tmp/events.txt',
            },
        ]

    def forward_event(self, event, payload):
        file_path = self.get_option('file_path', event.project)

        message = json.dumps(payload)
        f = open(file_path, 'a')
        f.write(message)
        f.close()
        return True
```

内容としては、受信したイベントの内容をパラメータで指定したパスにあるファイルに吐き出すだけのシンプルなものです。
処理内容はforward_eventという関数に実装します。
get_configというところにパラメータとして読み込ませたい情報を記載しておけば、Sentryサーバの管理画面上で登録フォームが自動的に作れられるのも便利です。

これらのファイル群を準備して、インストール→Sentryサーバ再起動すれば外部連携として使える状態になります。
この時注意が必要です。
workerが稼働しているサーバにもインストール必要があります。
実際に連携処理を行うのはworkerのためです。

```
$ python setup.py install
```
この処理はworkerのコンテナ上でも実施します。

## カスタムモジュールの有効化

組込が完了すれば、画面上から有効化します。

![plugins_list.png](https://qiita-image-store.s3.amazonaws.com/0/45851/22e9de42-6d12-1f64-247f-47b94a41d3b6.png)

PJの設定ページの外部連携の設定ページに行くと、先程追加したSampleのプラグインが追加されているのがわかります。
このプラグインを有効化し、設定を行います。
get_configで指定した項目が設定画面上で登録できるようになっているので、Sampleプラグインの「Configure plugin」のリンクをクリックします。

![plugin_configuration.png](https://qiita-image-store.s3.amazonaws.com/0/45851/3d384b70-a489-5816-2241-7e3ef31f2652.png)

設定を保存して完了です。
これで何らかのイベントを受信すれば、上記画面で設定したファイル上に吐き出されます。

ファイル出力されたイベントの内容のイメージはこんな感じです。

```json
{
	"eventID": "6e95c7b94822425dbc0a4d2957b4xxxx",
	"packages": {
		"python": "3.6.3"
	},
	"dist": null,
	"tags": [
		{
			"value": "error",
			"key": "level"
		},
		{
			"value": "server-01",
			"key": "server_name"
		}
	],
	"contexts": {},
	"dateReceived": "2018-01-28T07:04:35.000000Z",
	"dateCreated": "2018-01-28T07:04:34.000000Z",
	"fingerprints": [
		"b506bacb9521cc0779f286be755e4exxx"
	],
	"metadata": {
		"title": "Something went fundamentally wrong"
	},
	"groupID": "7",
	"platform": "python",
	"errors": [],
	"user": null,
	"context": {
		"sys.argv": [
			"'app.py'"
		]
	},
	"entries": [
		{
			"type": "message",
			"data": {
				"message": "Something went fundamentally wrong"
			}
		},
		{
			"type": "breadcrumbs",
			"data": {
				"values": [
					{
						"category": "raven.base.Client",
						"level": "debug",
						"event_id": null,
						"timestamp": "2018-01-28T06:59:38.054818Z",
						"data": null,
						"message": "Configuring Raven for host: <raven.conf.remote.RemoteConfig object at 0x0000000005854550>",
						"type": "default"
					},
					{
						"category": "raven.base.Client",
						"level": "debug",
						"event_id": null,
						"timestamp": "2018-01-28T06:59:38.067318Z",
						"data": null,
						"message": "Sending message of length 980 to http://xx.xx.xx.xx:8080/api/2/store/",
						"type": "default"
					},
					・・・略
					
				]
			}
		}
	],
	"message": "Something went fundamentally wrong",
	"sdk": {
		"version": "6.5.0",
		"name": "raven-python",
		"upstream": {
			"url": "https://docs.sentry.io/clients/python/",
			"isNewer": false,
			"name": "raven-python"
		}
	},
	"type": "default",
	"id": "59",
	"size": 3696
}
```

# まとめ
Pythonだけでなく、JavaScript、Android、Go、PHP、Ruby、.Net、Javaと様々に対応しているので使い所は幅広そうです。
外部連携機能も充実しているので効果的に活用すれば、調査分析をより効率良く行える可能性もあるかもしれないです。