Apache/Nginx 自動再起動付き監視デーモンを golang で作ろう


はじめに

モダンな監視ツールを導入できない環境向けに簡単な監視デーモンを golang で簡単に実装してみます。
クロスコンパイルでほとんどの環境で動作するデーモンを作れるのでこういう時に golang は本当に頼りになります。

やってみよう

必用なもの

  • golang

仕様

監視対象URLにポーリング → エラー回数が上限を超え → 任意のコマンドを発行
上記のデーモンを golang で実装する
監視対象URLやエラー時に発行するコマンドなどを環境変数にから読み込む

実装

まずは必要な変数を定義します。

main.go
var (
    CheckUrl         string // 監視対象URL
    CheckTimeout     int64  // 監視のタイムアウト
    CheckInterval    int64  // 監視間隔(秒)
    RetryCount       int64  // リトライ最大回数
    StopCmd          string // エラー検知時のApache/Nginx停止コマンド
    StartCmd         string // Apache/Nginx起動コマンド
    WaitAfterStop    int64  // StopCmd実行後のWait(秒)
    WaitAfterRestart int64  // リスタート後のWait(秒)
    ErrorCounter     int64  // エラーカウンター
)

次に http.DefaultClient を使用して指定のURLにリクエストする func を作成します。

main.go
type Check struct {
    client *http.Client
}

func (c *Check) request(ctx context.Context, req *http.Request) (*http.Response, error) {
    req = req.WithContext(ctx)
    resCh := make(chan *http.Response)
    errCh := make(chan error)
    go func() {
        res, err := c.client.Do(req)
        if err != nil {
            errCh <- err
            return
        }
        resCh <- res
    }()
    select {
    case res := <-resCh:
        return res, nil
    case err := <-errCh:
        return nil, err
    case <-ctx.Done():
        return nil, errors.New("HTTP request cancelled")
    }
}

指定URLにリクエストして、エラーの場合はApache/Nginxを再起動する func を作成します。

main.go
func check() {
    c := Check{
        client: http.DefaultClient, // DefaultClient を使い回し
    }
    req, err := http.NewRequest(http.MethodGet, CheckUrl, nil)
    if err != nil {
        log.Fatal(err)
        return
    }
    defer func() {
        if ErrorCounter < RetryCount {
            return
        }
        // Apache/Nginxリスタート
        err := c.restart()
        if err != nil {
            log.Fatal(err)
        }
        ErrorCounter = 0
        time.Sleep(time.Second * time.Duration(WaitAfterRestart))
    }()
    res, err := c.request(context.Background(), req)
    if err != nil {
        log.Println(err)
        ErrorCounter++
        return
    }
    // ステータスコードが400番台以降ならエラーと判定してエラーカウンタをインクリメント
    if res.StatusCode >= 400 {
        log.Println(fmt.Sprintf("bad response status code %d", res.StatusCode))
        ErrorCounter++
        return
    }
    log.Println(fmt.Sprintf("%s %d", CheckUrl, res.StatusCode))
    ErrorCounter = 0
}

最後にStopCmdとStartCmdの実行 func を作成します。

main.go
func (c *Check) restart() error {
    var cmd []string
    var err error
    log.Println(`restart...`)
    cmd = strings.Fields(StopCmd)
    stop := exec.Command(cmd[0], cmd[1:]...)
    log.Println(fmt.Sprintf(`exec stop command. [%#v]`, cmd))
    err = stop.Run()
    if err != nil {
        return err
    }
    time.Sleep(time.Second * time.Duration(WaitAfterStop))
    cmd = strings.Fields(StartCmd)
    start := exec.Command(cmd[0], cmd[1:]...)
    log.Println(fmt.Sprintf(`exec start command. [%#v]`, cmd))
    err = start.Run()
    if err != nil {
        return err
    }
    log.Println(`done...`)
    return nil
}

func main() {
    t := time.NewTicker(time.Second * time.Duration(CheckInterval))
    for {
        select {
        case <-t.C:
            check()
        }
    }
}

今回のコード一式は以下にあります。
やっぱツール作るなら golang がお気に入りです。
https://github.com/noridas80/watch-localhost