CLOVER🍀

That was when it all began.

Sentryでアラート検知に合わせてメールを送信してみる

これは、なにをしたくて書いたもの?

Sentryへのログ送信に合わせて、アラートメールを送ってみようかなと。

以前にLogbackを使って、Sentryにログイベントを送信するところはやりました。

Sentry JavaのLogbackインテグレーションを試す - CLOVER🍀

こちらをもとにして、アラートメール送信をやってみます。

Sentryのインストール

こちらに沿って、Sentryをローカルインストールします。

GitHub - getsentry/onpremise: Sentry On-Premise setup

$ git clone https://github.com/getsentry/onpremise.git
$ cd onpremise
$ ./install.sh

今回インストールしたSentryのバージョンは、9.1.2です。

この時、ユーザーを作成することができるので、以下の情報で作成。

「user001@example.com / password」

また、メールサーバーにはMailCatcher 0.7.0を使用し、192.168.0.2で動作しているものとします。

これに合わせて、docker-compose.ymlを以下のように修正します。

x-defaults: &defaults
  restart: unless-stopped
  build:
    context: .
    args:
      SENTRY_IMAGE: ${SENTRY_IMAGE}
  depends_on:
    - redis
    - postgres
    - memcached
    - smtp
  env_file: .env
  environment:
    SENTRY_MEMCACHED_HOST: memcached
    SENTRY_REDIS_HOST: redis
    SENTRY_POSTGRES_HOST: postgres
    SENTRY_EMAIL_HOST: 192.168.0.2
  volumes:
    - sentry-data:/var/lib/sentry/files

メールサーバーの接続先だけ変更すればOKです。

起動。

$ docker-compose up

ところで、ドキュメントを読んでいるとconfig.ymlで設定しそうな雰囲気もあったのですが、今回こちらは設定がうまく反映されません
でした…。

Configuring Sentry / Mail

###############
# Mail Server #
###############

mail.backend: 'smtp'  # Use dummy if you want to disable email entirely
mail.host: '192.168.0.2'
mail.port: 1025
# mail.username: ''
# mail.password: ''
# mail.use-tls: false
# The email address to send on behalf of
mail.from: 'sentry-admin@localhost.localdomain'

とりあえず、Sentryにアクセスしてみます。「http://localhost:9000」にすると、こんな感じの画面が表示されるのでログイン。

f:id:Kazuhira:20191012155246p:plain

メール送信に関する続きの設定を求められるので、ここで設定します。メールサーバーもここで設定できていいと
思うんですけどね?

f:id:Kazuhira:20191012155542p:plain

なお、MailCatcherのSMTPポートは、1025です。

Sentryのプロジェクトを作成します。

f:id:Kazuhira:20191012155638p:plain

f:id:Kazuhira:20191012155726p:plain

こちらを利用する、サンプルコードを書いていきましょう。

サンプルプロジェクト

サンプルプロジェクトは、前回のエントリを少し変えた形で。

Maven依存関係。

        <dependency>
            <groupId>io.sentry</groupId>
            <artifactId>sentry-logback</artifactId>
            <version>1.7.27</version>
        </dependency>

リクエストパスに応じて、各ログレベルのイベントを発生させるようなサンプルアプリケーション。ログメッセージは、QueryStringで
指定できるようにしました。
src/main/java/org/littlewings/sentry/logback/Server.java

package org.littlewings.sentry.logback;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import com.sun.net.httpserver.HttpServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class Server {
    static Logger logger = LoggerFactory.getLogger(Server.class);

    public static void main(String... args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 10);
        server.createContext("/", exchange -> {
            MDC.put("app", "java-app");

            String path = exchange.getRequestURI().getPath();
            String query = exchange.getRequestURI().getQuery();

            Map<String, String> queryValues = new HashMap<>();
            if (query != null) {
                for (String kvPair : query.split("&")) {
                    String[] kv = kvPair.split("=");
                    queryValues.put(kv[0], kv[1]);
                }
            }

            Optional<String> logMessage = Optional.ofNullable(queryValues.get("m"));
            

            logger.info("{} access", path);

            byte[] body;
            switch (path) {
                case "/info":
                    infoLogging(logMessage);

                    body = "OK!!".getBytes(StandardCharsets.UTF_8);
                    exchange.sendResponseHeaders(200, body.length);
                    exchange.getResponseBody().write(body);
                    break;
                case "/warn":
                    warnLogging(logMessage);

                    body = "WARN!!".getBytes(StandardCharsets.UTF_8);
                    exchange.sendResponseHeaders(500, body.length);
                    exchange.getResponseBody().write(body);
                    break;
                case "/error":
                    errorLogging(logMessage);

                    body = "ERROR!!".getBytes(StandardCharsets.UTF_8);
                    exchange.sendResponseHeaders(500, body.length);
                    exchange.getResponseBody().write(body);
                    break;
                case "/exception":
                    String em = Optional.ofNullable(queryValues.get("em")).orElse("Oops!!");
                    exceptionLogging(em, logMessage);

                    body = em.getBytes(StandardCharsets.UTF_8);
                    exchange.sendResponseHeaders(500, body.length);
                    exchange.getResponseBody().write(body);
                    break;
                default:
                    exchange.sendResponseHeaders(404, 0);
                    break;
            }

            MDC.clear();
        });

        server.start();
    }

    private static void infoLogging(Optional<String> logMessage) {
        logger.info(logMessage.orElse("INFOログだよ"));
    }

    private static void warnLogging(Optional<String> logMessage) {
        logger.warn(logMessage.orElse("WARNログです"));
    }

    private static void errorLogging(Optional<String> logMessage) {
        logger.error(logMessage.orElse("エラーですよ"));
    }

    private static void exceptionLogging(String exceptionMessage, Optional<String> logMessage) {
        logger.error(logMessage.orElse("例外だよ"), new RuntimeException(exceptionMessage));
    }
}

Logbackの設定。ここに、Sentryの設定も入ります。 src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5level] [%X{app}] %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="Sentry" class="io.sentry.logback.SentryAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>WARN</level>
        </filter>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="Sentry"/>
    </root>
</configuration>

これで、起動。作成したプロジェクトのDSNを指定してください。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.sentry.logback.Server -Dsentry.dsn=http://c2cdf4bc618c4b8181d9c0e1e15be772@localhost:9000/2

とりあえず、WARNレベルのログイベントを発生させてみます。

$ curl localhost:8080/warn?m=warn0001
WARN!!

これで、Sentryにログイベントが送信され、

f:id:Kazuhira:20191012160108p:plain

メールも通知されます。

f:id:Kazuhira:20191012160210p:plain

これは、Sentryでデフォルトでプロジェクト内の全メンバーに通知が行われるようになっているからです。

Alert Notifications - Docs

ちなみに、この状態で再度イベントを発生させても、通知が来なかったりします。

$ curl localhost:8080/warn?m=warn0002
WARN!!
$ curl localhost:8080/warn?m=warn0003
WARN!!
$ curl localhost:8080/error?m=error0001
ERROR!!

f:id:Kazuhira:20191012160445p:plain

f:id:Kazuhira:20191012160523p:plain

ちなみに、イベントをクローズ後に再度発生させたりすると、メールが来ますよ。

f:id:Kazuhira:20191012160604p:plain

f:id:Kazuhira:20191012160707p:plain

これは、デフォルトのアラートルールがプロジェクトで最初のイベントを通知するようになっているからですね。

Alert Notifications - Docs

f:id:Kazuhira:20191012161034p:plain

これを変更するには、カスタムアラートルールを作ることになるみたいです。

Creating Custom Alert Notifications - Docs

Routing Alert Notifications - Docs

f:id:Kazuhira:20191012161751p:plain

あとは、メール配信のタイミングを調整したりすると良いかもしれません。

f:id:Kazuhira:20191012161111p:plain

ややハンパですが、今回はここまでで。