Shellスクリプトのみを使って今監視ソフトを作るとしたら

# はじめに
本記事はQiita夏祭り2020の [「 〇〇（言語）のみを使って、今△△（アプリ）を作るとしたら」](https://qiita.com/official-events/5d181aadf5db26b73d33)のテーマ記事となります．

# 環境
- ubuntu18.04 LTS
- `/bin/sh` (dash)

# できたもの
https://github.com/taro-hida/shell-mon
![Peek 2020-07-09 22-33.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124948/349915a2-7246-a8d0-9a51-46114a066008.gif)

出力は以下のようになっています。
1行目：実行されたコマンドの内容
2行目：コマンドの出力（あれば）
3行目：コマンドの結果

実行するコマンドを変更することで、様々な監視を行うことができます。
実行するコマンドはファイルで指定します。一行ずつ読み込まれて実行されます。
今回であれば、以下のような内容です。

```check_command.txt
exit 0
exit 1
ping google.com -c 1 > /dev/null 2>&1
curl https://google.com/ > /dev/null 2>&1
mem=`ssh example.com -C 'free' 2> /dev/null | grep Mem | awk '{print $3*100/$2}'`;echo 'Mem Use: '$mem' %';if [ `echo $mem | awk '{printf("%d", $1)}'` -le 60 ]; then exit 0; else exit 1; fi
```

# 監視ソフトのアーキテクチャ
Nagiosをモデルに実装しました。Nagiosのアーキテクチャについては、[ペパポテックブログ](https://tech.pepabo.com/)さんより画像を引用しました。以下のような形となります。
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124948/702876f4-5586-36fe-5e0b-5009c26c4881.png)
[Nagiosの基礎 - ペパポテックブログ](https://tech.pepabo.com/2013/11/30/nagios-learning/#%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3)

今回実装する機能を切り出して図にしてみました。以下のような感じです。
![Untitled Diagram (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124948/6676d4d9-e1e4-c0ef-1785-4a75462b3031.png)

必要な部品は以下となります。

- 監視スケジューリングを行う
- プラグイン(実際の監視を行い、結果を返す)
- 監視プラグインを実行する
- 監視の結果を判定、表示する
- 監視の結果に応じて、アクションを実行する

# 監視スケジューリングを行う
何かを定期的に実行する、と言われてまず思い浮かぶのは crond ですが、
crond を使ってしまうと 「Shellスクリプトのみを使って」と言えなくなってしまいそうです。
Shellスクリプトの制御構文を使って実装しましょう。

```diff
+ #!/bin/sh
+ while true
+ do
+     sleep 60
+ done
```

無限ループを用いて、1分に一度何かを実行してくれるスクリプトが書けました。
これで監視実行のスケジュールとします。

# プラグイン(実際の監視を行い、結果を返す)
プラグインにはシェルコマンドを利用します。
コマンドを実際に実行し、実行後の "exit status" を `$?` で取得、0であれば成功と判断します。
成功の場合は0を、失敗の場合は0以外を返すようにプラグインを設計すれば、独自の監視プラグインの作成が可能です。
例えばpingの監視であれば `ping qiita.com -c 5` のようなコマンドラインです。

疎通成功のケース

```
$ ping qiita.com -c 1
...
--- qiita.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

$ echo $?
0
```

疎通失敗のケース

```
$ ping qiita.com -c 1
...
--- qiita.com ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

$ echo $?
1
```

これも、Nagiosの実装を参考にしています。

> Nagios による監視のステータスというのは 4 種類あって、 OK, WARNING, CRITICAL, UNKNOWN ですが、これは Nagios プラグインの exit status にてそれぞれ 0, 1, 2, 3 に対応しています。つまり、


> |  |  |
|:--|:--|
|ステータスのラベル|EXIT STATUS の値|
|OK|0|
|WARNING| 1 |
|CRITICAL| 2 |
|UNKNOWN| 3 |
ということです。

> 引用元: [テックブログ by GMOアドパートナーズグループ](https://techblog.gmo-ap.jp/2016/10/14/%E4%BB%8A%E6%9B%B4%E3%81%A0%E3%81%91%E3%81%A9%E3%80%81-nagios-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E7%9F%A5%E3%81%A3%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84-5-%E3%81%A4%E3%81%AE%E3%81%93%E3%81%A8/)

同様に、httpsの疎通監視であれば `curl`コマンドを用いて `curl -s https://qiita.com/`を実行します。

#  監視プラグインを実行する
監視プラグインはシェルコマンドを使用し、fileから1行ずつ読み込んで実行することにしましょう。
`check_command.txt` というファイルを用意し、そこに実行したいコマンドを羅列しておきます。

```
$ cat check_command.txt
echo 'しぇる'
ping qiitadon.com -c 1
curl https://qiita.com/
```
ファイルからのプラグイン読み込みと実行を先ほどのスクリプトに合わせて実装します。

```diff
  #!/bin/sh
  while true
  do
+     while read cmd; do
+         echo "$cmd"
+         $cmd > /dev/null 2>&1
+     done < check_command.txt
      sleep 60
  done
```

# 監視の結果を判定する
前述の通り、監視の結果は "exit status"にて判定します。
Shellスクリプトの `if ~ else`構文を用いて実装しましょう。

```diff
  #!/bin/sh
  while true
  do
      while read cmd; do
          echo "$cmd"
          $cmd > /dev/null 2>&1
+         if [ $? -eq 0 ]
+         then
+             echo 'OK'
+         else
+             echo 'NG'
+         fi
      done < check_command.txt
      sleep 60
  done
```

かなり形になってきましたね。

# 監視の結果に応じて、アクションを実行する
「監視結果がNGだった際、メールを送信する」等のアクションを実装しましょう。
これも、スクリプトにアクションをベタ書きします。

```diff
  #!/bin/sh
  while true
  do
      while read cmd; do
          echo "$cmd"
          $cmd > /dev/null 2>&
          if [ $? -eq 0 ]
          then
              echo 'OK'
          else
              echo 'NG'
+             echo 'NG' | mail root@localhost
          fi
      done < check_command.txt
      sleep 60
  done
```

# 完成
ということで、完成です！

```shell
#!/bin/sh
while true
do
    while read cmd; do
        echo "$cmd"
        $cmd > /dev/null 2>&1
        if [ $? -eq 0 ]
        then
            echo 'OK'
        else
            echo 'NG'
            echo 'NG' | mail root@localhost
        fi
    done < check_command.txt
    sleep 60
done
```

監視ソフトとしての基本的な機能は押さえて、かつ理解しやすいシンプルな形で実装できたように思います。

ただ、この状態ではいくつか不便な点が出てきます。

- 出力が確認できない
標準・エラー出力を `/dev/null` へのリダイレクトで殺してしまったので、結果の表示の柔軟性が失われてしまいました。
Nagios先生よろしく、標準出力を画面に出せるようにしましょう。

- `ping qiita.com`など、自動で終了しないコマンドを発行した場合処理が進まなくなる。
`ping qiita.com`などを指定すると、現在の実装では永遠にpingを打ち続け、そこから処理が進まなくなってしまいます。
監視のタイムアウトを設定します。例えば、プラグインの実行に10秒以上かかった場合は処理を中断するようにしましょう。

# 標準出力を画面に出せるようにする

コマンド実行を、子シェルにて実行するように変更します。

```diff
  #!/bin/sh
  while true
  do
      while read cmd; do
          echo "$cmd"
-         $cmd > /dev/null 2>&1
+         sh -c "$cmd"
          if [ $? -eq 0 ]
          then
              echo 'OK'
          else
              echo 'NG'
              echo 'NG' | mail root@localhost
          fi
      done < check_command.txt
      sleep 60
  done
```

# 監視のタイムアウト設定
timeoutコマンドを利用して、監視のタイムアウトを実装しましょう。

```diff
  #!/bin/sh
  while true
  do
      while read cmd; do
          echo "$cmd"
-         sh -c "$cmd"
+         timeout -k 15 10 sh -c "$cmd" 
          if [ $? -eq 0 ]
          then
              echo 'OK'
          else
              echo 'NG'
              echo 'NG' | mail root@localhost
          fi
      done < check_command.txt
      sleep 60
  done
```
timeoutコマンドは、指定した時間（今回は10秒）が経過しても引数にとったコマンドが動いていた場合、TERMシグナル（シグナルの数値は15）をコマンドに送信してくれるコマンドです。

なので、指定した時間より前にコマンドが終了した場合は何も行わず、その "exit status" は変化しません。

```
$ timeout 1 test 1 -gt 5
$ echo $?
1
$ timeout 1 test 5 -gt 1
$ echo $?
0
```

TERMシグナルにてコマンドが終了した場合は、 "exit status" が124となります。

```
$ timeout 1 ping google.com
...
$ echo $?
124
```

# リファクタリング
最後に、できる限りスッキリした形にスクリプトの構造を変更しましょう。

```shell
#!/bin/sh

MESSAGE_OK='--> OK'
MESSAGE_NG='--> NG'
ACTION_NG='echo NG | mail root@localhost'
CHECK_COMMAND_FILE_PATH='./check_command.txt'
CHECK_INTERVAL=60

# $1: check plugin command
exec_check()
{
    timeout -k 15 10 sh -c "$1"
    if [ $? -eq 0 ]
    then
        printf "\033[32m%s\033[m\n" "${MESSAGE_OK}"
    else
        printf "\033[31m%s\033[m\n" "${MESSAGE_NG}"
        sh -c "${ACTION_NG}"
    fi
}

while true
do
    while read cmd; do
        echo "$cmd"
        exec_check "$cmd"
        echo '-------'
    done < ${CHECK_COMMAND_FILE_PATH}
    sleep ${CHECK_INTERVAL}
done
```
出力に色もつけてみました。

# おわりに
Qiita夏「**祭り**」ということで何かの形でお祭りに参加したく、今回記事を執筆させていただきました。
監視用のShellスクリプトというのは何も珍しいものではなく、様々な用途で、数多くの現場で書かれてきたものかと思います。
思い切り車輪の再発明ですが、あくまで一つのネタ記事として楽しんでいただけましたら幸いです。

# 記事を書いてみて

- 既存の監視ツールのありがたみを感じました。
- diff 形式で追記していく形式の記事はもう二度と書きません。修正の際に全部訂正しないといけないから大変でした。

# お世話になったサイト
- [シェルスクリプトのechoで”問題なく”色をつける(bash他対応)](https://qiita.com/ko1nksm/items/095bdb8f0eca6d327233)
- [Bashで四捨五入したいの巻 - TrippyBoyの愉快な日々](https://blog.trippyboy.com/2011/shell/bash%E3%81%A7%E5%9B%9B%E6%8D%A8%E4%BA%94%E5%85%A5%E3%81%97%E3%81%9F%E3%81%84%E3%81%AE%E5%B7%BB/)