2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Grafanaで東京と大阪のコロナ新規感染者数をグラフにしてみた

Last updated at Posted at 2021-09-28

いや、ほとんどCovid-19関係ないのですが。GrafanaをだましてPrometheusの代わりに自作のスクリプトをデータソースにするのを試したのでそのメモ。

#やったこと
Python/Flaskで作った簡易WebアプリをGrafanaのデータソースとして指定した。
WebアプリはなんちゃってPrometheusとして振る舞う。

Grafanaは、データソースがPrometheusの場合は以下のような一連のリクエストをWebアプリに向けて投げてくるので、それぞれに対してそれらしい応答を返せばグラフを描画してくれる。

127.0.0.1 - - [28/Sep/2021 16:36:42] "POST /api/v1/query HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:36:52] "POST /api/v1/labels HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:36:52] "GET /api/v1/label/__name__/values?start=1632825412&end=1632847012 HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:36:52] "GET /api/v1/metadata HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:36:53] "POST /api/v1/labels HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:36:53] "GET /api/v1/label/__name__/values?start=1632825413&end=1632847013 HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:36:53] "GET /api/v1/metadata HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:36:53] "GET /api/v1/label/__name__/values?start=1632825413&end=1632847013 HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:36:55] "POST /api/v1/series HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:37:10] "POST /api/v1/labels HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:37:10] "POST /api/v1/query_exemplars HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:37:10] "GET /api/v1/label/__name__/values?start=1632825430&end=1632847030 HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:37:10] "GET /api/v1/metadata HTTP/1.1" 200 -
127.0.0.1 - - [28/Sep/2021 16:37:19] "POST /api/v1/query_range HTTP/1.1" 200 -

描画した結果は以下のような感じ。グラフは東京と大阪のここ90日間のコロナ新規感染者数。
image.png

作り方

1.Ubuntu 20のVMを作る

Amazon Lightsailでいいだろう。サイズは3.5$、OSはUbuntu 20で。
Grafanaにアクセスするため、ネットワーキング/IPv4 ファイアウォールでTCP/3000のポートを開けておく。
image.png

2.Grafanaをインストールする

sshで接続して、以下を実行する。
(参考)Grafana - Install on Debian or Ubuntu
https://grafana.com/docs/grafana/latest/installation/debian/

$ sudo apt-get install -y apt-transport-https
$ sudo apt-get install -y software-properties-common wget
$ wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
$ echo "deb https://packages.grafana.com/oss/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
$ sudo apt-get update
$ sudo apt-get install -y grafana
$ sudo systemctl daemon-reload
$ sudo systemctl start grafana-server
$ sudo systemctl enable grafana-server.service
$ sudo systemctl status grafana-server

Webブラウザで、VMに払い出されたパブリックIPの3000番ポートにアクセスすればGrafanaのログイン画面が表示されるはず。
ログインユーザーとパスワードはadmin/admin。
image.png

3.Python/Flaskをインストールする

$ sudo apt install -y python3-flask

4.なんちゃってPrometheusを作る

以下の、pseudo.pyファイルを作成する。ディレクトリはどこでもいいし、ユーザーもrootでなくてOK。
なお、Prometheusの各APIが何のデータを返すかについては以下:
(参考)Proetheus - HTTP API
https://prometheus.io/docs/prometheus/latest/querying/api/

各APIでそれっぽい応答を返す部分は説明割愛するが、'/api/v1/query_range'で、データを生成するところの処理については凡そ以下の通り:

fetch_tokyo()

  1. 東京の感染者数リスト(CSVファイル)をダウンロード。
    https://catalog.data.metro.tokyo.lg.jp/dataset/t000010d0000000068/resource/c2d997db-1450-43fa-8037-ebb11ec28d4c
  2. CSVファイルの5カラム目が日付で、新規陽性者一人につき1レコードあるので同じ日付のレコードを数え上げる。
  3. ハッシュ型で数え上げたデータを配列に変換して、Grafanaが食える形にする。

fetch_osaka()

  1. 大阪の感染者数リスト(CSVファイル)をダウンロード。文字コードがsjisなのでutf-8に変換。
    https://covid19-osaka.info/
  2. こちらはあらかじめ日付ごとの感染者数にサマライズされているので、1カラム目を日付、3カラム目を新規陽性者として配列に読み込み。
pseudo.py
from flask import Flask
from flask import request
app = Flask(__name__)

@app.route('/api/v1/query', methods=["POST"])
def query():
    return "{}"

@app.route('/api/v1/labels', methods=["POST"])
def labels():
    return '''
{
    "status": "success",
    "data": [
        "__name__",
        "region"
    ]
}
'''

@app.route('/api/v1/label/__name__/values')
def label__name__values():
    return '''
{
    "status": "success",
    "data": [
        "new-cases"
    ]
}
'''

@app.route('/api/v1/label/region/values')
def label_region_values():
    return '''
{
    "status": "success",
    "data": [
        "tokyo", "osaka"
    ]
}
'''

@app.route('/api/v1/metadata')
def metadata():
    return '''
{
    "status": "success",
    "data": [
        "new-cases": [
            {
                "type": "counter",
                "help": "Number of new cases",
                "unit": ""
            }
        ]
    ]
}
'''

@app.route('/api/v1/series', methods=["POST"])
def series():
    return '''
{
    "status": "success",
    "data": [
        {
            "__name__": "new-cases",
            "region": "tokyo"
        },
        {
            "__name__": "new-cases",
            "region": "osaka"
        }
    ]
}
'''

@app.route('/api/v1/query_exemplars', methods=["POST"])
def query_exemplars():
    return '''
{
    "status": "success",
    "data": [
        {
            "seriesLabels": {
            },
            "exemplars": [
            ]
        }
    ]
}
'''


@app.route('/api/v1/query_range', methods=["POST"])
def query_range():
    import json

    tokyo = fetch_tokyo()
    osaka = fetch_osaka()
    return '''
{
    "status": "success",
    "data": {
        "resultType": "matrix",
        "result": [
            {
                "metric": {
                    "__name__": "new-cases",
                    "region": "tokyo"
                },
                "values":
''' + json.dumps(tokyo) + '''
            },
            {
                "metric": {
                    "__name__": "new-cases",
                    "region": "osaka"
                },
                "values":
''' + json.dumps(osaka) + '''
            }
        ]
    }
}
'''

def fetch_tokyo():
    import os
    import csv
    import datetime
    import urllib

    if(not os.path.exists("tokyo.csv")):
        urllib.request.urlretrieve("https://stopcovid19.metro.tokyo.lg.jp/data/130001_tokyo_covid19_patients.csv", "tokyo.csv")

    f = open("tokyo.csv")
    r = csv.reader(f)
    h = next(r)
    d = {}
    for l in r:
        t = datetime.datetime.strptime(l[4], "%Y-%m-%d")
        if(t not in d):
            d[t] = 0
        d[t] = d[t] + 1
    a = []
    for k in d:
        a.append([k.timestamp(), d[k]])
    return a

def fetch_osaka():
    import os
    import csv
    import datetime
    import urllib
    import subprocess

    if(not os.path.exists("osaka-u.csv")):
        urllib.request.urlretrieve("https://covid19-osaka.info/data/summary.csv", "osaka.csv")
        subprocess.run("iconv -f sjis -t utf8 osaka.csv -o osaka-u.csv", shell=True)

    f = open("osaka-u.csv")
    r = csv.reader(f)
    h = next(r)
    a = []
    for l in r:
        t = datetime.datetime.strptime(l[0], "%Y-%m-%d")
        a.append([t.timestamp(), l[2]])
    return a

5.なんちゃってを起動する

ポート9090番で、先に作成したなんちゃってPrometheusを起動する。
ローカルのGrafanaが読みに来るだけなのでファイアウォールのポート開けは不要。

# FLASK_APP=pseudo.py flask run --port=9090

6.Grafanaのデータソースを設定する

データソースの種類としてPrometheusを選択し、「http://localhost:9090」から読むよう指定する。

image.png

image.png

image.png

7.ダッシュボードを作る

metricとして「new-cases」というのが見えるはずなので、それをグラフにする。

image.png

image.png

image.png

image.png

デフォルトのグラフだと6時間ウィンドウでデータが表示されないので、90日とか1年にする。
image.png

すると、データが歯抜けなのが悪いのか点グラフが表示される。折れ線グラフで表示したい。
image.png

Grafana画面右の「Graph styles > Connect null values」をAlwaysにすれば折れ線グラフにはなる。
image.png

image.png

おわりに

汎用BIツールとしてのGrafanaの用途が広がればと思ったが、どうだろう・・思ったよりコスくて面倒くさい。
Prometheusがもう少し軽くて気軽に使えれば良いのだが。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?