子供がPCで遊んでいないかPrometheusで監視する

子供はまだゲームできるほどの大きさではないのだが、自分がこっそりゲームするタイプだったので、将来多分子供もこっそりゲームをするのであろう。
そういう風に育った時、「学校の調べ物をするためにPC使ってるはずが、CPU利用率張り付いているの何でやねん」と子供に突っ込めるようにしておきたい。
ということで、Windows内のリソースの使用状況の高騰をキャッチして通知するシステムを作る。

構成は以下のような感じとなる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/27986f8b-fadc-ca3d-45ec-e18fac5e762c.png)

父のPCに監視基盤となるPrometheus、AlertManager、Grafanaを構築し、子のPCにWindowsのメトリクスを公開するWindows Exporter（WMI Exporter）を導入する。
父のPCからメトリクスを定期的に取得し、負荷状況を確認し、負荷のレベルによってはメールにてアラート通知を行う。

なお、常時監視する都合上、前提として父のPCは立ち上げっぱなしとする。
電気代が気になる方はRaspberry Piとかに置き換えるのも多分可能だが、ここでは割愛する。
また、子供がゲームをするのにスマートフォンを使うケースの方が圧倒的に多そうだが、スマートフォン向けのPrometheus Exporterを導入するには脱獄が必要そうな気がするので、こちらも今回は割愛する。

## 子のPCのセットアップ
Windows PCのメトリクスを公開するためのPrometheus Exporterとして [WMI Exporter](https://github.com/prometheus-community/windows_exporter)があるのでこれを使う。
インストール時のオプションでメトリクスを公開するポート等が変更できるようだが、ここではデフォルト値でインストールする。
[Release Note](https://github.com/prometheus-community/windows_exporter/releases)ではmsi形式とexe形式の2種類のバイナリが用意されているが、msi形式だとサービスの作成やFirewallの穴あけもやってくれるので、ここではmsi形式のバイナリを使ってインストールする。
ダウンロードしてインストールすると、Windowsの`サービス`に`windows_exporter`が追加される。
![35fefb52aa627d09fa959a26868eafa39cf5cf83ed3367082af91f404e116a00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/08479dfa-96e3-9203-4ce0-5922ca7623f9.png)

また、[http\://localhost:9182](http://localhost:9182)にアクセスするとWindows ExporterのWeb画面にアクセスできるようになっている。
これの`/metrics`を使って監視する。

## 父のPCのセットアップ
監視で利用するPrometheus、AlertManager、Grafanaはdocker-composeを使って構築する。
docker-composeを使ってるので、MacやLinuxでも多分動くはず。
コンテナを使う都合上、WSL（Windows Subsystem for Linux）が有効になっていて、WSLを起動して進めるものとする。
### docker、docker-composeのインストール
`snap`で導入し、`sudo`なしで利用できるようユーザをdockerのグループに追加する。
```sh
sudo snap install docker
sudo snap install docker-compose
sudo usermod -aG docker $USER
exit
```

### Prometheus、AlertManager、Grafanaの構築
[こちら](https://github.com/docker/awesome-compose/tree/master/prometheus-grafana)で配布されていたdocker-composeのYAMLにAlertManagerとデータの永続化の設定を追加したものを作って構築する。
最初にそれぞれの設定ファイルを置くためのディレクトリを作成する。
```sh
mkdir prometheus grafana alertmanager
```
Prometheusでスクレイプ設定をする際に指定する、子供のPCのIPを環境変数で定義しておく。
```sh
TARGET_IP=192.168.0.41
```
なお、DHCPでIPを割り当てている場合はDNSを別途用意するか、サービスディスカバリの仕組みを取り入れるなどIPが変わっても追従できるようにしておく。（面倒なら固定IPを割り当てる）
次にPrometheusの設定ファイルを作成する。
```yaml:prometheus/prometheus.yml
cat <<EOF > ./prometheus/prometheus.yml
global:
  scrape_interval: 15s
  scrape_timeout: 10s
  evaluation_interval: 15s
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - alertmanager:9093
    scheme: http
    timeout: 10s
rule_files:
- "rules-game-check.yml"
scrape_configs:
- job_name: prometheus
  honor_timestamps: true
  scrape_interval: 15s
  scrape_timeout: 10s
  metrics_path: /metrics
  scheme: http
  static_configs:
  - targets:
    - localhost:9090
- job_name: windows
  metrics_path: /metrics
  scheme: http
  static_configs:
  - targets:
    - $TARGET_IP:9182
```
`job_name: windows`の箇所でWindows Exporter（＝子供のPC）をスクレイプするようにしている。
また、アラートの条件は`rules-game-check.yml`に書いている。
ということで、アラートの条件定義である`rules-game-check.yml`を作成する。
```yaml:prometheus/rules-game-check.yml
cat <<'EOF' > ./prometheus/rules-game-check.yml
groups:
- name: game-check
  rules:
  - alert: WindowsServerCpuUsage
    expr: 100 - (avg by (instance) (rate(windows_cpu_time_total{mode="idle"}[2m])) * 100) > 70
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: Windows Server CPU Usage (instance {{ $labels.instance }})
      description: "CPU Usage is more than 80%\n  VALUE = {{ $value }}\n  LABELS = {{ $labels }}"
EOF
```
条件式は[Awsome Prometheus Alerts](https://samber.github.io/awesome-prometheus-alerts/rules#windows-server)から拝借した。
CPU以外の条件でアラートを上げたい人はAwsome Prometheus Alertを参照すると他の条件式も確認できるので、必要に応じて参照してほしい。
評価式は以下部分となる。
```yaml
    expr: 100 - (avg by (instance) (rate(windows_cpu_time_total{mode="idle"}[2m])) * 100) > 70
    for: 5m
```
`2分`間のアイドル時間の割合をベースにCPU利用率を算出し、`70%`を超える利用率が`5分`続くとアラートを上げる、という意味となる。
環境によってここはかなり異なるので、ここは子供のPCのリソース使用状況を見ながらチューニングするとよい。
なお、評価式はメトリクスがないと評価されないので、子供のPCがシャットダウンしている場合はアラートは飛んでこない。

次にAlertManagerの設定ファイルを作成する。ここではGmailを使ってメールで通知する設定としている。
``` yaml:alertmanager/config.yml
MY_MAILADDRESS=mymailaddress@gmail.com
MY_APP_PASSWORD=abcdhijkefghlmno
cat <<'EOF' > ./alertmanager/config.yml
global:
  smtp_smarthost: 'smtp.gmail.com:465'
  smtp_auth_username: "${MY_MAILADDRESS}"
  smtp_auth_password: "${MY_APP_PASSWORD}"
  smtp_require_tls: false
  smtp_from: 'Alertmanager <alertmanager@localhost.localdomain>'

route:
  receiver: 'game-check'

receivers:
- name: 'game-check'
  email_configs:
  - to: "${MY_MAILADDRESS}"
EOF
```
注意点として、ここでのパスワードはGoogleログイン時に使用するものではなく、アプリケーションが使うためのパスワードとなる。
発行方法は[こちら](https://support.google.com/accounts/answer/185833?hl=ja)を参照のこと。

次にGrafanaの設定ファイルを作成する。
```yaml:grafana/datasource.yml
cat <<EOF > ./grafana/datasource.yml
apiVersion: 1

datasources:
- name: Prometheus
  type: prometheus
  url: http://prometheus:9090
  isDefault: true
  access: proxy
  editable: true
EOF
```
最後にdocker-compose用のファイルを作成する。
```yaml:compose.yaml
cat <<EOF > ./compose.yaml
services:
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
    ports:
      - 9090:9090
    restart: unless-stopped
    volumes:
      - ./prometheus:/etc/prometheus
      - prom_data:/prometheus
  grafana:
    image: grafana/grafana
    container_name: grafana
    ports:
      - 3000:3000
    restart: unless-stopped
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=grafana
    volumes:
      - ./grafana:/etc/grafana/provisioning/datasources
      - grafana_storage:/var/lib/grafana
  alertmanager:
    image: prom/alertmanager
    container_name: alertmanager
    volumes:
      - ./alertmanager:/etc/alertmanager
    command: "--config.file=/etc/alertmanager/config.yml"
    ports:
      - 9093:9093
    restart: always
volumes:
  prom_data: {}
  grafana_storage: {}
```
データの永続化のためにPrometheusとGrafanaにはVolumeを割り当てている。
以上で設定が揃ったので起動する。
```sh
docker-compose up -d
```
起動に成功すると、それぞれ以下でアクセスできる。
- Prometheus: [http://localhost:9090](http://localhost:9090)
- AlertManager: [http://localhost:9093](http://localhost:9093)
- Grafana: [http://localhost:3000](http://localhost:3000) ※ID/PW:admin/grafana

子供のPCが見えているかは、Prometheusの`Status`->`Targets`で確認できる。
![ee80d023d44e1783129e3fad577b65b43e0ae900c223b0c33ed221e8ac754c26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/1762e4e6-8c67-784e-56f5-1b3a275258a2.png)
Grafanaに関してはダッシュボードを用意していないので、今の状態では使えない。後ほど用意する。

## 動作確認
### アラートの発行の確認
子供のPCにCPU負荷をかけてアラートが飛ぶか確認する。
負荷のかけ方は何でもよいが、特に重いゲームなどを持っていなかったのでここではMicrosoftが配布している[sysinternals](https://learn.microsoft.com/ja-jp/sysinternals/)というツールキット内の`CPUSTRES64.EXE`を実行し、以下の画像のように複数スレッドで同時に負荷をかける。
![429ac8265c1dca5fc04286b3902936bd421f93f00a82d3c8ba4366ecf1eb5274.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/c0a573ef-6bec-5c8b-a135-1b7f37b6d613.png)

するとPrometheusの`Alerts`タブからアラートの条件に引っかかっていることが確認できる。
![6d438e8082507fdb1255784532a858782b474e0e55c66f121d8029ca7b4e23aa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/c3c34bdd-b1c1-dcc5-341b-edef38c0348d.png)

`Pending`は条件（`expr`）は満たしているが、期間（`for`）を満たしていない状態を示している。
しばらく待つと状態が`firing`に変わり、以下のようにGmailに通知が届く。
![c9c204d7affe50093afef8ca26eb5ae0c7a512983709643b54b10f22f8ae0a81.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/dc21ebd8-7bbc-5625-6180-dc4410ef3a7f.png)

なお、上手く届かない場合はAlertManagerのログを確認するとよい。
例えば以下はアプリパスワードが設定できていなかった時の例。
```sh
$ docker logs alertmanager
ts=2024-04-01T07:41:13.682Z caller=notify.go:848 level=warn component=dispatcher receiver=game-check integration=email[0] aggrGroup={}:{} msg="Notify attempt failed, will retry later" attempts=7 err="*email.loginAuth auth: 534 5.7.9 Application-specific password required. For more information, go to\n5.7.9  https://support.google.com/mail/?p=InvalidSecondFactor fa17-20020a056a002d1100b006f6b52eb59asm7164300pfb.126 - gsmtp"
```
### ダッシュボードによる確認
アラートルールを作る際や、意図していないメトリクスのスパイクを確認するためにGrafanaのダッシュボードは活用した方がよい。
デフォルトではダッシュボードは用意されていないので、自力で作成するか有りものを流用することになる。
有りものを流用する場合、Grafana LabsがDashboardsを配布している[Dashboards](https://grafana.com/grafana/dashboards)で[Windowsで検索すると](https://grafana.com/grafana/dashboards/?search=windows)いくつかWindows Exporter用のダッシュボードが確認できるので、これらを流用すると一からダッシュボードを用意するよりは楽に用意することができる。
ここではそこそこ使えそうだった[ID:14694](https://grafana.com/grafana/dashboards/14694-windows-exporter-dashboard/)を導入してみる。
Grafanaにログインし、`Dashboards`->`New`->`Import`を選択する。
`Find and import dashboards for common applications at grafana.com/dashboards`の入力欄に`14694`と入力して`Load`をクリックし、データソースに`Prometheus`を入力後、`Import`をクリックするとダッシュボードが取り込まれる。
取り込まれたダッシュボードを参照すると、以下のような感じでCPU負荷などが確認できる。
![98a5cf4cb3d29d0edbf0434fc904045da195ca9f25af2f82a810749c33756e26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/e703ff5d-d4dd-4044-95b9-7a5d9a6bf2c1.png)

ディスクI/Oなども見れるので、変なものをダウンロードしていないかとかも気にする人は確認するといいかも。

## 終わりに
こっそりやると親子関係が破綻するかもしれないので、子供にはちゃんと監視してる旨を通知した方がいいかも。
あと仕組みを教えてあげて迂回路を考えてもらうと、子供の勉強にいいのかもしれない。