aoishiの備忘録

備忘録

PrometheusとPushgatewayでGoogle Finance APIの株価をPushしてモニタリングしてみた

コンテナ環境などスケールするインフラ、システムのモニタリングに向いていると話題のPrometheusですが、Zabbix SenderみたいなPush型方式のモニタリングができるのか興味があったので調べてみました。

結論から述べると、下記の要領でPush型のモニタリングができるようです。

  1. Pushgatewayを起動する
  2. Pushgateway宛にメトリクスをPushする
  3. PushgatewayがPushされたメトリクスをエクスポートする
  4. PrometheusからPushgateway上のメトリクスをスクレイプする

Prometheusを扱うことも初めてでしたが、やってみたことをまとめてみます。

prometheus.io

prometheus.io

環境

1台のサーバにすべてのミドルウェアをインストールしていきます。

  • OS: CentOS 7.4
  • Prometheus: 2.2.1
  • node_exporter: 0.15.2
  • Pushgateway: 0.4.0
  • Grafana: 5.0.4

なお、下記のポートで各サービスが提供されるようになります。

port service
tcp/3000 Grafana
tcp/9090 Prometheus Server
tcp/9091 Prometheus Pushgateway
tcp/9100 Prometheus Node Exporter

参考

構築手順に関しては、下記を参考にしました。

PythonでPushgatewayにデータを送るスクリプトを作成するために下記を参考にしました。

モニタリング対象データとして、Google Finance APIで株価をリアルタイムで取得するために下記を参考にしました。

構築手順

Python 3.6のインストール

PythonでPushgatewayにメトリクスを送るスクリプトを記述するため、下記手順でPythonをインストールしておきます。

# yum install epel-release
# yum install python36
# python36 --version
Python 3.6.3

Prometheus各種サービス起動用のユーザ作成

Prometheus関連サービスの起動用のユーザを作成しておきます。

# useradd -U -s /sbin/nologin -M -d / prometheus

Node Exporterのインストール

Node Exporterのバイナリをダウンロードし、/usr/localに配置します。

# cd /tmp
# wget https://github.com/prometheus/node_exporter/releases/download/v0.15.2/node_exporter-0.15.2.linux-amd64.tar.gz
# tar zxvf node_exporter-0.15.2.linux-amd64.tar.gz
# mv node_exporter-0.15.2.linux-amd64 /usr/local
# ln -s -f /usr/local/node_exporter-0.15.2.linux-amd64 /usr/local/node_exporter

Node ExporterのSystemd用のUnitファイルを作成します。

# vi /etc/systemd/system/node_exporter.service
---
[Unit]
Description=node_exporter for Prometheus

[Service]
Restart=always
User=prometheus
ExecStart=/usr/local/node_exporter/node_exporter
ExecReload=/bin/kill -HUP $MAINPID
TimeoutStopSec=20s
SendSIGKILL=no

[Install]
WantedBy=multi-user.target
---

Node Exporterの自動起動設定を有効化し、起動します。

# systemctl daemon-reload
# systemctl enable node_exporter
# systemctl start node_exporter

Node Exporterのサービス用のURLにアクセスいて、取得しているメトリクス一覧を確認しましょう。

Pushgatewayのインストール

Pushgatewayのバイナリをダウンロードし、/usr/localに配置します。

# cd /tmp
# wget https://github.com/prometheus/pushgateway/releases/download/v0.4.0/pushgateway-0.4.0.linux-amd64.tar.gz
# tar zxvf pushgateway-0.4.0.linux-amd64.tar.gz
# mv pushgateway-0.4.0.linux-amd64 /usr/local
# ln -s -f /usr/local/pushgateway-0.4.0.linux-amd64 /usr/local/pushgateway

PushgatewayのSystemd用のUnitファイルを作成します。

# vi /etc/systemd/system/pushgateway.service
---
[Unit]
Description=pushgateway for Prometheus

[Service]
Restart=always
User=prometheus
ExecStart=/usr/local/pushgateway/pushgateway
ExecReload=/bin/kill -HUP $MAINPID
TimeoutStopSec=20s
SendSIGKILL=no

[Install]
WantedBy=multi-user.target
---

Pushgatewayの自動起動設定を有効化し、起動します。

# systemctl daemon-reload
# systemctl enable pushgateway
# systemctl start pushgateway

Pushgatewayのサービス用のURLにアクセスし、Pushgateway上のメトリクスを確認してみましょう。後々Pushしたメトリクスが表示されるようになります。

Prometheusのインストール

まず、設定ファイルとモニタリングデータを配置するためのディレクトリを作成しておきます。

# mkdir /etc/prometheus /var/lib/prometheus
# chown prometheus:prometheus /var/lib/prometheus

Prometheusのバイナリをダウンロードし、/usr/localに配置します。

# cd /tmp
# wget https://github.com/prometheus/prometheus/releases/download/v2.2.1/prometheus-2.2.1.linux-amd64.tar.gz
# tar zxvf prometheus-2.2.1.linux-amd64.tar.gz
# mv prometheus-2.2.1.linux-amd64 /usr/local
# ln -s -f /usr/local/prometheus-2.2.1.linux-amd64 /usr/local/prometheus

設定ファイルを作成します。

# cp -piv /usr/local/prometheus/prometheus.yml /etc/prometheus/
# vi /etc/prometheus/prometheus.yml
---
global:
  scrape_interval:     15s # By default, scrape targets every 15 seconds.

  # Attach these labels to any time series or alerts when communicating with
  # external systems (federation, remote storage, Alertmanager).
  external_labels:
    monitor: 'codelab-monitor'

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    # Override the global default and scrape targets from this job every 5 seconds.
    scrape_interval: 5s

    static_configs:
      - targets: ['localhost:9090']

  # for Node Exporter in Prometheus Server
  - job_name: 'node'
    static_configs:
      - targets: ['localhost:9100']

  # for Pushgateway
  - job_name: 'pushgateway'
    static_configs:
      - targets: ['localhost:9091']
---

PrometheusのSystemd用のUnitファイルを作成します。

# vi /etc/systemd/system/prometheus.service
---
[Unit]
Description=Prometheus - Monitoring system and time series database

[Service]
Restart=always
User=prometheus
ExecStart=/usr/local/prometheus/prometheus --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/var/lib/prometheus/data
ExecReload=/bin/kill -HUP $MAINPID
TimeoutStopSec=20s
SendSIGKILL=no

[Install]
WantedBy=multi-user.target
---

Prometheusの自動起動設定を有効化し、起動します。

# systemctl daemon-reload
# systemctl enable prometheus
# systemctl start prometheus

PCなどからPrometheusのGraph描画用のページにアクセスし、取得しているメトリクスのグラフが描画できることを確認します。

  • http://{{ PrometheusをインストールしたサーバのIP }}:9090/graph

f:id:akihisa_oishi:20180424163109p:plain

Grafanaのインストール

下記を参考にインストールします。

http://docs.grafana.org/installation/rpm/

# sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm
# systemctl daemon-reload
# systemctl start grafana-server
# systemctl status grafana-server

その後、WEB画面から以下の設定を設定します。

上記のダッシュボードをインストールするだけで、OS関係のメトリクスの関しては十分なほど視覚化されます。

f:id:akihisa_oishi:20180424163314p:plain

Pushgateway経由でのモニタリング

Google Finance APIから株価データを取得してPushgatewayにPushしてみる

Google Finance APIを利用して、1分毎のリアルタイムな株価データを取得し、Pushgateway経由でPrometheusに連携してみたいと思います。 APIが非公式であり、使い方に癖があるため実装が間違っている箇所があるかもしませんので正確性についてはご了承下さい。

下記のようなスクリプトをstock.pyというファイル名で作成します。

from prometheus_client import CollectorRegistry, Gauge, push_to_gateway, generate_latest
import requests
import json
import re
import argparse
import traceback
import time

# Google Finance API用
API_URL = 'https://www.google.com/finance/getprices?q={code}&x={market}&i=61&p=1Df=d,c,v,o,h,l'
regex = re.compile('a(?P<date>\d+),(?P<close>[^,]+),(?P<high>[^,]+),(?P<low>[^,]+),(?P<open>[^,]+),(?P<volume>\d+)')
def google_finance_api_parser(response):
    lines = response.splitlines()
    m = regex.finditer(lines[-1])
    return next(m)

# Main
def main():
    argparser = argparse.ArgumentParser()
    argparser.add_argument('-g', '--gateway', type=str, required=True)
    argparser.add_argument('-m', '--market', type=str, required=True)
    argparser.add_argument('-c', '--code', type=str, required=True)
    argparser.add_argument('--debug', action='store_true', help='debug printing without pushing to gateway')

    args = argparser.parse_args()
    request_url = API_URL.format(market=args.market, code=args.code)
    r = requests.get(request_url)
    if r.status_code == 200:
        registry = CollectorRegistry()
        try:
            d = google_finance_api_parser(r.text)
            Gauge('stock_price_close', 'Stock price', ['market', 'code'], registry=registry).labels(market=args.market, code=args.code).set(d['close'])
            Gauge('stock_price_high', 'Stock price', ['market', 'code'], registry=registry).labels(market=args.market, code=args.code).set(d['high'])
            Gauge('stock_price_low', 'Stock price', ['market', 'code'], registry=registry).labels(market=args.market, code=args.code).set(d['low'])
            Gauge('stock_price_open', 'Stock price', ['market', 'code'], registry=registry).labels(market=args.market, code=args.code).set(d['open'])
            Gauge('stock_price_volume', 'Stock price', ['market', 'code'], registry=registry).labels(market=args.market, code=args.code).set(d['volume'])
            Gauge('stock_price_unixtime', 'Stock price', ['market', 'code'], registry=registry).labels(market=args.market, code=args.code).set(d['date'])
            Gauge('stock_price_last_success_unixtime', 'Stock price', ['market', 'code'], registry=registry).labels(market=args.market, code=args.code).set(int(time.time()))

            if args.debug:
                print(request_url)
                print(generate_latest(registry=registry).decode('utf-8'))
            else:
                grouping_key = {'market': args.market, 'code': args.code}
                push_to_gateway(gateway=args.gateway, job='STOCK', registry=registry, grouping_key=grouping_key)
        except:
            print('[Error] failed to send stock data. check api response from "{}"'.format(request_url))
            traceback.print_exc()

if __name__ == '__main__':
    main()

上記のスクリプトで生成されるメトリクスは下記の通りです。市場と証券コードはラベルで区別するようにしました。

メトリクス名 内容
stock_price_close 終値
stock_price_high 高値
stock_price_low 安値
stock_price_open 始値
stock_price_volume 出来高
stock_price_unixtime 終値の時刻(Unixtime)
stock_price_last_success_unixtime 株価を取得した時刻(Unixtime)

Google Finance APIでは国内の株価は20分ほど遅延しており、株価を取得した時刻(stock_price_last_success_unixtime)と取得できた終値の時刻(stock_price_unixtime)には差が生じます。このあたりも、後にグラフ化するとよくわかるようになります。

次に、下記のように実行環境を作成します。

$ mkdir ~/bin
$ cd ~/bin
$ vi stock.py
※ 上記で作成したスクリプトを保存します

$ python36 -m venv env
$ source env/bin/activate
(env) $ pip install prometheus_client requests

スクリプトにPushgatewayのURLとGoogle Finance APIから取得する株価の市場とコードを引数として与え実行します。

試しに標準出力にデータを出力してみます。
(env) $ python stock.py -g localhost:9091 -m TYO -c 6758 --debug
----
# HELP stock_price_close Stock price
# TYPE stock_price_close gauge
stock_price_close{code="6758",market="TYO"} 5426.0
# HELP stock_price_high Stock price
# TYPE stock_price_high gauge
stock_price_high{code="6758",market="TYO"} 5427.0
# HELP stock_price_low Stock price
# TYPE stock_price_low gauge
stock_price_low{code="6758",market="TYO"} 5425.0
# HELP stock_price_open Stock price
# TYPE stock_price_open gauge
stock_price_open{code="6758",market="TYO"} 5425.0
# HELP stock_price_volume Stock price
# TYPE stock_price_volume gauge
stock_price_volume{code="6758",market="TYO"} 67800.0
# HELP stock_price_unixtime Stock price
# TYPE stock_price_unixtime gauge
stock_price_unixtime{code="6758",market="TYO"} 1524548340.0
# HELP stock_price_last_success_unixtime Stock price
# TYPE stock_price_last_success_unixtime gauge
stock_price_last_success_unixtime{code="6758",market="TYO"} 1524549541.0
----

Pushgatewayにデータを送ります。
(env) $ python stock.py -g localhost:9091 -m TYO -c 6758 --debug

cronで毎分実行させるために、crontabに設定を追加します。

$ crontab -e
---
* * * * * cd ${HOME}/bin && source env/bin/activate && python stock.py -g localhost:9091 -m TYO -c 6758 > /dev/null 2>&1
* * * * * cd ${HOME}/bin && source env/bin/activate && python stock.py -g localhost:9091 -m TYO -c 9432 > /dev/null 2>&1
---

PushしたメトリクスをPushgatewayで確認する

Pushgatewayのメトリクス一覧を確認すると、下記のようにPushしたメトリクスがエクスポートされるようになります。

# HELP stock_price_close Stock price
# TYPE stock_price_close gauge
stock_price_close{code="6758",instance="",job="STOCK",market="TYO"} 5433
stock_price_close{code="9432",instance="",job="STOCK",market="TYO"} 5163
# HELP stock_price_high Stock price
# TYPE stock_price_high gauge
stock_price_high{code="6758",instance="",job="STOCK",market="TYO"} 5434
stock_price_high{code="9432",instance="",job="STOCK",market="TYO"} 5164
# HELP stock_price_last_success_unixtime Stock price
# TYPE stock_price_last_success_unixtime gauge
stock_price_last_success_unixtime{code="6758",instance="",job="STOCK",market="TYO"} 1.524550204e+09
stock_price_last_success_unixtime{code="9432",instance="",job="STOCK",market="TYO"} 1.524550205e+09
# HELP stock_price_low Stock price
# TYPE stock_price_low gauge
stock_price_low{code="6758",instance="",job="STOCK",market="TYO"} 5433
stock_price_low{code="9432",instance="",job="STOCK",market="TYO"} 5162
# HELP stock_price_open Stock price
# TYPE stock_price_open gauge
stock_price_open{code="6758",instance="",job="STOCK",market="TYO"} 5434
stock_price_open{code="9432",instance="",job="STOCK",market="TYO"} 5163
# HELP stock_price_unixtime Stock price
# TYPE stock_price_unixtime gauge
stock_price_unixtime{code="6758",instance="",job="STOCK",market="TYO"} 1.524549e+09
stock_price_unixtime{code="9432",instance="",job="STOCK",market="TYO"} 1.524549e+09
# HELP stock_price_volume Stock price
# TYPE stock_price_volume gauge
stock_price_volume{code="6758",instance="",job="STOCK",market="TYO"} 21800
stock_price_volume{code="9432",instance="",job="STOCK",market="TYO"} 3900

今回のように、同一メトリクスに対してラベルを切り替えて複数回に分けてデータをPushする場合は、grouping_keyを適切に設定しないとすでにPush済みのデータが削除されてしまうこともあるので注意が必要です。

私の場合は、当初grouping_keyを全く設定していなかったため、cronで設定したうちの一つの株価しかPushgatewayに残りませんでした。

詳細は下記を参照して下さい。

PushしたメトリクスをGrafanaで確認する

下記のように株価のグラフを作成します。ローソクではなく1分毎の終値でプロットさせていますが、なんとなく株価のように見えますね。

f:id:akihisa_oishi:20180424163432p:plain

次に、APIの更新遅延のグラフを作成します。取引時間中はだいたい1200秒(20分)程度遅延しているようですね。お昼休みと取引時間外はAPIの最新データの時刻が更新されないため遅延が拡大していっています。

f:id:akihisa_oishi:20180424163305p:plain

まとめ

PrometheusとPushgatewayでPush型のモニタリングができるかをGoogle Finance APIのデータを使って試してみました。

Google Finance APIの調査に時間をかけてしまいましたが、Prometheus自体は簡単に構築できてNode Exporterだけでも十分すぎるほどメトリクスが取得でき、またGrafanaでも簡単に視覚化できるので非常に便利だなと思いました。

Zabbix Senderと比較して思ったのが、PrometheusではPushgatewayをスクレイプした時刻でメトリクスを記録するため、過去の事項に遡ってデータを登録するといったことは難しそうということです。ちなみに、Zabbix Senderではタイムスタンプ付きでデータを送れるためバルクインサート的なことが可能になっています。

PrometheusでもバルクインサートAPIを求めるissueが上がっているようです。

Add API for bulk imports · Issue #535 · prometheus/prometheus · GitHub

今度は、アラート通知などもやってみたいと思います。