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

いや、ほとんど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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/79a6767c-7500-f54b-ab35-2654f4ab2093.png)


# 作り方
## １．Ubuntu 20のVMを作る
Amazon Lightsailでいいだろう。サイズは3.5$、OSはUbuntu 20で。
Grafanaにアクセスするため、ネットワーキング/IPv4 ファイアウォールでTCP/3000のポートを開けておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/a6fe2750-d528-917a-9558-a45c83b7d531.png)


## ２．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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/aa966c97-8930-500e-4d8d-99a27cea2844.png)


## ３．Python/Flaskをインストールする

```
$ sudo apt install -y python3-flask
```

## ４．なんちゃって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ファイルの５カラム目が日付で、新規陽性者一人につき１レコードあるので同じ日付のレコードを数え上げる。
3. ハッシュ型で数え上げたデータを配列に変換して、Grafanaが食える形にする。

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


```python: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
```

## ５．なんちゃってを起動する

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/4157b9b6-4cb5-c54d-4827-36d3ced5c9ea.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/dce2e158-db36-4ca6-11f7-455d2710f694.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/9f4783a1-b272-d0db-f608-a4a88558bcb6.png)

## ７．ダッシュボードを作る
metricとして「new-cases」というのが見えるはずなので、それをグラフにする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/507ab185-af39-ebf1-9f6d-207f8492f1ae.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/560282f1-e611-51ea-88b6-bbca8fdaaf65.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/eb682253-239b-41b6-3192-38edb61bf732.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/aac79f85-409a-e2d5-dd5c-9c8104f05f6e.png)

デフォルトのグラフだと６時間ウィンドウでデータが表示されないので、９０日とか１年にする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/20140432-e4dc-68fd-8ae3-24e4937ecf60.png)

すると、データが歯抜けなのが悪いのか点グラフが表示される。折れ線グラフで表示したい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/0a42e42a-ceee-f644-ef65-576b4d1be52d.png)

Grafana画面右の「Graph styles > Connect null values」をAlwaysにすれば折れ線グラフにはなる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/3d544d42-c3a0-b0e4-8419-a5fb7bcc0153.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/8f5b1b53-e2bb-ac26-8328-75ad04ae84e6.png)


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