LINE Engineering
Blog

BitBarを利用したPrometheus通知モニタリング

Paul Traylor 2017.04.28

現在、開発者向けの便利なサーバーモニタリングシステムと通知システムの構築に携わっています。

macOSメニュバーの活用

こんにちは。LINE Fukuokaの開発チームで働いているPaul Traylorです。LINE に入社してからの主な仕事は、内部サーバのモニタリングシステムをPrometheusGrafanaを利用してより使い勝手の良いものに改良することです。最終的には、個々の開発者が自分の通知を簡単に設定できるようにするのが狙いです。モニタリングシステムの管理担当者という仕事柄、異なる環境に構築された沢山のサーバや様々なチャートをこまめにチェックする作業が欠かせませんが、あらゆることに目を配るのはますます大変になってきています。そこでモニタリング作業の手間を省くことができるシンプルなツールを調べてみました。中でも私が重宝しているのは、BitBarと呼ばれるものです。このツールを利用して簡単なステータスプラグインを作っておくとだいたい何でもモニタリングできますので、私が作成したプラグインについてご紹介します。

Pythonへのプラグイン実装

BitBarの要は、スクリプトから出力された結果を取得してカスタムメニューに表示することです。上記キャプチャ画面のように、メニューバーには現在の通知状態を示すスナップショットが表示され、そこをクリックすると現状の詳細情報が表示される仕組みにしたいと考えました。様々な環境をモニタリングしないといけないため、スクリプトが各環境をもれなくチェックできること、設定情報はスクリプトの外部に保存できること、といった点も考慮します。

BitBarによって処理される出力結果の例

:rotating_light: [0, 1, 0]
---
:warning: release Active: 0 Silenced: 0 Ignored: 3 | href=http://release.alertmanager.example/
 
---
:warning: beta Active: 3 Silenced: 0 Ignored: 0| href=http://beta.alertmanager.example/
DiskWillFill | href=http://beta.prometheus.example/<alert>
NginxDown job=nginx service=Foo project=api | href=http://beta.prometheus.example/<alert>
NginxDown job=nginx service=Bar project=web | href=http://beta.prometheus.example/<alert>
---
:warning: alpha Active: 0 Silenced: 0 Ignored: 0| href=http://alpha.alertmanager.example/

以下は、私がプラグインに主に採用している基本ヘッダです。Python 3でテストする際にしばしばUnicode問題が起きたため、下記のような方法でUTF-8でstdoutへ出力するようにしました。BitBarプラグインは単なるスクリプトですので、ほとんどの開発作業はコンソールで行われます。そしてコンソールで実行するときは、追加情報はなるべくstderrに出力されるようにしています。

基本ヘッダ

#!/usr/local/bin/python3
import collections
import configparser
import logging
import os
import sys
 
import requests
 
  
if 'BitBar' in os.environ:
    logging.basicConfig(level=logging.WARNING)
    sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf8')
else:
    logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

複数の環境をモニタリングしているので、設定情報を別の場所に保存することで多様な環境のモニタリングの設定を容易にする必要があります。デフォルトのPython ConfigParserを利用すれば、自分の設定情報が簡単に保存できます。

設定ファイルの例

[release]
url = http://release.example.com
 
[beta]
url = http://beta.example.com
 
[alpha]
url = http://alpha.example.com

configのロードとデフォルトbucketの設定

# Load our config file. I try to follow standards as much as possible, so I save my
# settings using the XDG Base Directory Specification
config = configparser.ConfigParser()
config.read([os.path.expanduser('~/.config/bitbar/alertmanager.ini')])
environments = [(section, config.get(section, 'url')) for section in config.sections()]
  
# Setup a bucket to hold our per-environment alerts
alerts = collections.defaultdict(list)
# and setup some counters
silenced = collections.defaultdict(int)
ignored = collections.defaultdict(int)

Alert Managerからのクエリ実行

# Start by looping through each of our environments and query 
# the correct Alertmanager from our configuration file.
# If we have an error while querying a server, we'll just skip it for now 
# (we're probably getting email alerts for it anyways)
# I also like to add a custom user-agent to help with 
# debugging where a request comes from
for env, url in environments:
    try:
        result = requests.get(
            '{}/api/v1/alerts/groups'.format(url),
            headers={'user-agent': USER_AGENT}
        )
        result.raise_for_status()
    except:
        logger.error('Error querying server %s', env)
        continue

BitBarで処理する出力の生成

# A small helper function to handle formatting the labels from Alertmanager
def label(alert, label):
   if label in alert['labels']:
       if alert['labels'][label]:
           return ' {}={}'.format(label, alert['labels'][label])
   return ''

# Loop through each entry from Alertmanager, and build a list of our alerts
for entry in data:
   if entry['blocks']:
       for block in entry['blocks']:
           for alert in block.get('alerts', []):
               # I don't really care to see silenced alerts, so I'll skip those
               # and only show them in the total count
               if 'silenced' in alert:
                   logger.debug('Skipping silenced alert %s', alert['labels'])
                   silenced[env] += 1
                   continue
               # I've been testing some heartbeat checks to ensure that
               # prometheus is running well, so I want to skip my heartbeat
               # checks from the output as well
               if 'heartbeat' == alert['labels'].get('severity'):
                   logger.debug('Skipping heartbeat alert %s', alert['labels'])
                   ignored[env] += 1
                   continue
               # We want to start each of our lines with the actual alert name
               # being fired
               _buffer = alert['labels']['alertname']
               # And add to that a few specific Prometheus labels that we are
               # interested in
               _buffer += label(alert, 'job')
               _buffer += label(alert, 'service')
               _buffer += label(alert, 'project')
               _buffer += ' | '
               # And if we have the generatorURL (from Prometheus) then we
               # want to allow directly linking to the query itself
               if 'generatorURL' in alert:
                   _buffer += 'href=' + alert['generatorURL']
               alerts[env].append(_buffer)

最終出力

# Once we have processed all of our alerts from each instance of Alertmanager
# we are ready to build the actual output that will be rendered by BitBar

# We start with an Emoji of a rotating light, and then a quick formatting of
# the active alerts across each of our environments
print(':rotating_light: {}'.format(
   [len(alerts[env[0]]) for env in environments]
))

# We then loop through each of our environments
for env, url in environments:
   print('---')
   # and we print a basic summary of that Alertmanager
   print(':warning: {} Active: {} Silenced: {} Ignored: {}| href={}'.format(
       env, len(alerts[env]), silenced[env], ignored[env], url
   ))

   # And then loop through to show all the alerts. When we mess up and have
   # a LOT of messages from Alertmanager, we will only show a limited number
   # so that we do not make our MenuBar unreadable
   if len(alerts[env]) > MAX_SHOW:
       print(':bomb: Truncated error list to %s' % MAX_SHOW)
   print(u'\n'.join(sorted(alerts[env][:MAX_SHOW])))

おわりに

BitBarを利用するとどのようなスクリプトでもメニューバープラグインに変換できるので、様々なモニタリングを行うスクリプトが簡単に作成できます。たとえば、GitHubのレビュー待ちプルリクエストの有無をモニタリングするスクリプトを作ることも可能です。

今回ご紹介したBitBarプラグインを直接トライしてみたい方は、ぜひ下記Githubリポジトリをご確認ください。

https://github.com/kfdm/bitbar-alertmanager

BitBar Prometheus plugin monitoring

Paul Traylor 2017.04.28

現在、開発者向けの便利なサーバーモニタリングシステムと通知システムの構築に携わっています。

Add this entry to Hatena bookmark

リストへ戻る