Help us understand the problem. What is going on with this article?

ElastAlertの監視ルールを拡張してすこしだけ幸せになる

More than 1 year has passed since last update.

はじめに

株式会社エイチームライフスタイル でインフラエンジニアをやっている @ihsiek と申します。

以前、会社のアドベントカレンダーに寄稿したElasticsearchを活用したサービス監視ですこしだけ幸せになるという記事で、ElastAlertの導入について紹介しましたが、記事内でも述べている通り、ElastAlertをデフォルトで使用していると、いくつか使い勝手がよくないケースがでてきます。

そこで、今回はElastAlertの監視ルールタイプをカスタマイズして、突き当たった問題をいくつか解消した内容について紹介します。
ちなみに私の場合は、ほかのメンバーがKibanaを見なくても負荷原因を特定できるようになったことでアラート対応の差し込みが減り、ずいぶん幸せになりました。

背景

top_count_keysでピックアップした上位5件を表示しても障害が発生しているモジュールを特定できないし、アクセス元IPアドレスをアラートに載せてもWhoisを引かないとアクセス元を判別できないし、もう少し楽にできないかなーと思ったのがきっかけです。

実現したいこと

  • PercentageMatchの挙動に沿って、通知されるアラートの本文に以下2つを追加する
    • 抽出条件にマッチしたイベントのみを対象にサマリを出力する。
    • IPアドレスをWhoisサービスのURLにセットして表示する。

カスタムルールの追加方法

細かい説明とチュートリアルは公式のドキュメントにあるので、この記事では弊社で導入したカスタムルールのサンプルを紹介します。

準備

カスタムルール用のパッケージを作成

まずは、カスタムルール用のパッケージを用意します。

$ sudo mkdir /var/lib/elastalert/elastalert_modules/
$ sudo touch /var/lib/elastalert/elastalert_modules/__init__.py

続いて、ElastAlertのモジュール群から参照できるようにシンボリックリンクを貼ります。

$ sudo ln -sf /var/lib/elastalert/elastalert_modules/ /usr/lib/python2.7/dist-packages/elastalert/elastalert_modules

これで準備はできたので、次はカスタムルールを作っていきます。

カスタムルールの作成

カスタムルールの追加

今回はpercentage_matchの挙動を引き継ぎたいので、PercentageMatchRuleクラスを継承したPercentageMatchWithTopXRuleクラスを作成します。

/var/lib/elastalert/elastalert_modules/custom_rules.py
from elastalert.ruletypes import PercentageMatchRule
import re

class PercentageMatchWithTopXRule(PercentageMatchRule):

    def get_match_str(self, match):
        message = super(PercentageMatchWithTopXRule, self).get_match_str(match)

        top_events = [[key[11:], counts] for key, counts in match.items() if key.startswith('top_events_')]
        def events_to_message(items):
            message = ''
            items.sort(key=lambda x: x[1], reverse=True)
            for term, count in items:
                if re.match("^(\d{1,3}\.){3}\d{1,3}$", term):
                    # If value of term is an IP Address, add the URL of the whois service.
                    message += 'https://wq.apnic.net/static/search.html?query=%s : %s\n' % (term, count)
                else:
                    message += '%s : %s\n' % (term, count)

            return message

        for key, counts in top_events:
            message += '%s:\n' % (key)
            message += '%s\n' % (events_to_message(counts.items()))

        return message

    def generate_aggregation_query(self):
        """
        custom_top_count_keys: A list of fields. ElastAlert will perform a terms query for the top X most common values for each of the fields, where X is 5 by default, or custom_top_count_number if it exists.
        custom_top_count_number: The number of terms to list if custom_top_count_keys is set. (Optional, integer, default 5)
        """

        query = {
            'topx_match_aggs': {
                'filter': {
                    'bool': {
                       'must': self.match_bucket_filter
                    }
                },
                'aggregations': {
                }
            },
            'percentage_match_aggs': {
                'filters': {
                    'other_bucket': True,
                    'filters': {
                        'match_bucket': {
                            'bool': {
                                'must': self.match_bucket_filter
                            }
                        }
                    }
                }
            }
        }
        number = self.top_count_number = self.rules.get('custom_top_count_number', 5)
        keys = self.top_count_keys = self.rules.get('custom_top_count_keys')
        for key in keys:
            child_query = {
                'terms': {
                    'field': key + '.keyword',
                    'order': { '_count' : 'desc' },
                    'size': number
                }
            }
            query['topx_match_aggs']['aggregations'][key] = child_query
        return query

    def check_matches(self, timestamp, query_key, aggregation_data):
        match_bucket_count = aggregation_data['percentage_match_aggs']['buckets']['match_bucket']['doc_count']
        other_bucket_count = aggregation_data['percentage_match_aggs']['buckets']['_other_']['doc_count']

        if match_bucket_count is None or other_bucket_count is None:
            return
        else:
            total_count = other_bucket_count + match_bucket_count
            if total_count == 0:
                return
            else:
                match_percentage = (match_bucket_count * 1.0) / (total_count * 1.0) * 100
                if self.percentage_violation(match_percentage):
                    match = {self.rules['timestamp_field']: timestamp, 'percentage': match_percentage}
                    if query_key is not None:
                        match[self.rules['query_key']] = query_key

                    # Set TopX counts
                    counts = self.get_top_counts(aggregation_data)
                    match.update(counts)

                    self.add_match(match)

    def get_top_counts(self, aggregation_data):
        """
        Counts the number of events for each unique value for each key field.
        Returns a dictionary with top_events_<key> mapped to the top 5 counts for each key. 
        """
        all_counts = {}
        number = self.top_count_number
        keys = self.top_count_keys
        for key in keys:

            hits_terms = aggregation_data.get('topx_match_aggs').get(key, None)
            if hits_terms is None:
                top_events_count = {}
            else:
                buckets = hits_terms.get('buckets')

                terms = {}
                for bucket in buckets:
                    terms[bucket['key']] = bucket['doc_count']
                counts = terms.items()
                counts.sort(key=lambda x: x[1], reverse=True)
                top_events_count = dict(counts[:number])

            # Save a dict with the top 5 events by key
            all_counts['top_events_%s' % (key)] = top_events_count

        return all_counts

ファイルを作っただけでは、Supervisorで立ち上げたElastAlertのデーモンに、カスタムルールのクラスがロードされません。
カスタムルールのクラスをロードさせるために、デーモンの再起動をかけます。
これ書いてて気付きましたが、デーモン化しなくてもRundeckやCronから叩けば、デーモンの再起動いらないんですよね・・・。

$ supervisorctl restart elastalert

後はpercentage_matchを使用していた設定を書き換えるだけです。

監視ルールの設定

設定ファイル

/var/lib/elastalert/rules/status-5XX/sample.yml
es_host: elasticsearch1.example.com
es_port: 443
use_ssl: True

index: access-log-*
doc_type: access-log

name: HTTP status 5XX on hoge

type: "elastalert_modules.custom_rules.PercentageMatchWithTopXRule"

match_bucket_filter:
- query_string:
    query: "code: [500 TO 599]"

max_percentage: 0.02

custom_top_count_keys:
  - "host"
  - "agent"
  - "path"

timeframe:
  minutes: 5

timestamp_field: "time"

use_kibana4_dashboard: "https://kibana.example.com/#/discover/alert-5XX-hoge"

alert:
- "email"
email:
- "hoge@example.com"

変更点

  • type
    • [変更前] type: percentage_match
    • [変更後] type: "elastalert_modules.custom_rules.PercentageMatchWithTopXRule"
  • top_count_keys
    • [変更前] top_count_keys:
    • [変更後] custom_top_count_keys:

検証

以下のコマンドを実行して動作検証します。

$ elastalert-test-rule --config /etc/elastalert/config.yml

アラート本文

これでアラートの本文は以下のようになるはずです。

HTTP status 5XX on hoge

Percentage violation, value: 0.0659413122321 (min: None max : 0.02)

path:
/hogehoge : 3
/fugafuga : 2
/hogefuga : 2
/fugahoge : 1
/aaaaaaaa : 1

host:
https://wq.apnic.net/static/search.html?query=xxx.yyy.zzz.aaa : 3
https://wq.apnic.net/static/search.html?query=xxx.yyy.zzz.bbb : 2
https://wq.apnic.net/static/search.html?query=xxx.yyy.zzz.ccc : 2
https://wq.apnic.net/static/search.html?query=xxx.yyy.zzz.ddd : 1
https://wq.apnic.net/static/search.html?query=xxx.yyy.zzz.eee : 1

agent:
Mozilla/5.0 (compatible; SemrushBot/1.2~bl; +http://www.semrush.com/bot.html) : 10

kibana_link: https://kibana.example.com/#/discover/alert-5xx-hoge?_g=%28refreshInterval%3A%28display%3AOff%2Csection%3A0%2Cvalue%3A0%29%2Ctime%3A%28from%3A%272017-12-01T13%3A26%3A33.631170Z%27%2Cmode%3Aabsolute%2Cto%3A%272017-12-01T13%3A36%3A33.631170Z%27%29%29
num_hits: 15165
num_matches: 1
percentage: 0.0659413122321
time: 2017-12-01T13:31:33.631170Z

最後に

ElastAlertにカスタムルールを導入してから、障害の一次対応でKibanaを見る頻度がめっきり減りました。
そもそもアラートが飛ばないに越したことはありませんが、まずは何か問題が起こったときにすぐ気付ける仕組みづくりも大事だと考えています。

それから、弊社では経営陣にもアラート通知が届くようになっているため、副次的な効果として現状起こっている問題を伝えやすくなりました。
経営陣がインフラへの投資に理解を示してくれるのは、もともと弊社の良い部分ではあるのですが、投資の結果どうなったとか、こういう部分に問題があるから投資したいという話をするとき「〇〇な理由でアラートの発生頻度が高い」というコミュニケーションで済ませられるとずいぶん楽ですよね。

ということで、ElasticsearchとElastAlertを導入すると運用に関する些末な問題がずいぶん解消されて幸せになれるので、ぜひ導入を検討してみてください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした