APIの死活監視をRocketChatに送信する

Page content

※ 過去に書いた記事を載せています。今動作するかは不明。。。。

サーバ監視ツールを作成しようというお話をします。

サーバが本当に動いているかどうかを少し前までは、メールで送信が多かったのですが、 最近は、Slack等のチャットに連絡するような方法が多くなってきているようです。

そこで、RocketChatを使ってサーバが死んでいるときにメッセージを送るツールをGo言語で作ってみようと思います。

Go言語には、サードパーティー製のパッケージ(Golangがあらかじめ用意していないソースのこと)などを 使用せずともある程度組めるようにはなっております。 ですので、1ファイルでコンパイルできプラットフォーム依存のないソースを作成します。 今回もそういう感じで作ったので、WindowsでもMacでもLinuxでも動作すると思います。 (すみません、プロクシは設定しておりません。。。。)

基礎知識は、英語ですが、本家を見て頂くか A Tour of GoGo言語の初心者が見ると幸せになれる場所を見ていくとわかりやすいです。

※ 時間の関係上細かいところまで説明しておりませんご了承ください。

前準備(サーバーの仕様)

サーバは簡易的にGo言語で用意します。(解説は面倒なので抜き)

HTTPステータスが正常(200)、異常(500)と、 Jsonのステータスが正常(0),異常(1)を返すものを4種類のAPIを作成しておき、 それによってどう動作するかを確認します。

以下の仕様です

  • http://localhost:3000
  • 引数なしのPOST
  • response JSONで以下のように返す
{
  "comment": コメント,
  "status": ステータス(0 or -1)
}
  • URLは、以下を用意
URI内容
/test_status_ok_json_status_okHTTPステータス 正常・Jsonステータス正常
/test_status_ok_json_status_ngHTTPステータス 正常・Jsonステータス異常
/test_status_ng_json_status_ngHTTPステータス 異常・Jsonステータス正常
/test_status_ok_json_status_ok_long_timeHTTPステータス 正常・Jsonステータス正常(responseが遅い)

以下がサンプルソースです。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"time"
)

type ResponseJSON struct {
	Comment string `json:"comment"`
	Status  int    `json:"status"`
}

const rotationTime = 10 * time.Second

const urlTestStatusOkJsonStatusOk = "/test_status_ok_json_status_ok"
const testStatusOkJsonStatusNg = "/test_status_ok_json_status_ng"
const testStatusNgJsonStatusNg = "/test_status_ng_json_status_ng"
const urlTestStatusOkJsonStatusOkLongTime = "/test_status_ok_json_status_ok_long_time"

func commonHandler(w http.ResponseWriter, r *http.Request, resJSON ResponseJSON) {
	body := r.Body
	defer body.Close()

	buf := new(bytes.Buffer)
	io.Copy(buf, body)

	// jsonエンコード
	outputJson, err := json.Marshal(resJSON)

	if err != nil {
		panic(err)
	}

	// jsonヘッダーを出力
	w.Header().Set("Content-Type", "application/json")
	// jsonデータを出力
	_, _ = fmt.Fprint(w, string(outputJson))
}

func urlTestStatusOkJsonStatusOkLongTimeHandler(w http.ResponseWriter, r *http.Request) {
	time.Sleep(20 * time.Second)
	w.WriteHeader(http.StatusOK)

	resJSON := ResponseJSON{
		Comment: "Status Ok JsonStatus Ok LongTime",
		Status:  0,
	}
	commonHandler(w, r, resJSON)
}

func testStatusOkJsonStatusOkHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)

	resJSON := ResponseJSON{
		Comment: "Status Ok JsonStatus Ok",
		Status:  0,
	}

	commonHandler(w, r, resJSON)
}

func testStatusOkJsonStatusNgHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	resJSON := ResponseJSON{
		Comment: "Status Ok JsonStatus NG",
		Status:  -1,
	}

	commonHandler(w, r, resJSON)

}

func testStatusNgJsonStatusNgHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusInternalServerError)
	resJSON := ResponseJSON{
		Comment: "Status NG JsonStatus NG",
		Status:  -1,
	}
	commonHandler(w, r, resJSON)
}

func serverSample() {
	// curl -i http://localhost:3000/test_status_ng_json_status_ng
	http.HandleFunc(urlTestStatusOkJsonStatusOk, testStatusOkJsonStatusOkHandler)

	// curl -i http://localhost:3000/test_status_ok_json_status_ng
	http.HandleFunc(testStatusOkJsonStatusNg, testStatusOkJsonStatusNgHandler)

	// curl -i http://localhost:3000/test_status_ng_json_status_ng
	http.HandleFunc(testStatusNgJsonStatusNg, testStatusNgJsonStatusNgHandler)

	// curl -i http://localhost:3000/test_status_ok_json_status_ok_long_time
	http.HandleFunc(urlTestStatusOkJsonStatusOkLongTime, urlTestStatusOkJsonStatusOkLongTimeHandler)

	go func() {
		log.Fatal(http.ListenAndServe(":3000", nil))
	}()

}

func main() {
	// サンプルサーバー
	serverSample()

	for range time.Tick(rotationTime) {
	}

}

ステータス監視

前準備で、サーバーを用意しました。 死活監視として何を見るかというと

  1. HTTPステータス異常ではないか
  2. レスポンスが異常ではないか
  3. レスポンス応答が遅くないか この3つを監視します。

HTTPステータスとレスポンスの取得

今回はメッチャ簡単に「http」パッケージを用いて、HTTPステータスとレスポンスを取得します HTTPステータスが200以外の場合は、エラーを返します。 また、通信で異常があった場合もエラーを返します。 正常の場合のみ、レスポンスを返すようにします。

レスポンス(JSON)の中身は、DataStoreとして構造体に入れてわかりやすく中身を管理します。 「encoding/json」パッケージで簡単に構造体に入れてくれます。

type ResponseJSON struct {
	Comment string `json:"comment"`
	Status  int    `json:"status"`
}

func validationAPI(urlAddress string, res *ResponseJSON) (int, error) {

	req, err := http.NewRequest(
		"POST",
		urlAddress,
		nil,
	)

	if err != nil {
		return -1, err
	}

	client := &http.Client{}
	resp, err := client.Do(req)

	// ネットワークエラー
	if err != nil {
		return -1, err
	}

	statusCode := resp.StatusCode

	// httpステータスが正常でなければエラーとする
	if statusCode != 200 {
		return statusCode, errors.New("ステータスエラー")
	}

	defer resp.Body.Close()

	byteArray, _ := ioutil.ReadAll(resp.Body)
	responseJsonStr := string(byteArray)

	_ = json.Unmarshal([]byte(responseJsonStr), res)

	return statusCode, nil
}

各ステータスとレスポンス時間を取得

上記で作成した「validationAPI」メソッドは、ステータスやサーバ異常は、エラーと返しますが、 レスポンス中身は、確認していません。また、レスポンス時間なども確認してません。 そういったものを確認するためのルーチンを以下で制御し、エラーであった場合RocketChatに投げます。

// ステータスチェック
func statusCheck(urlAddress string) {

	responseJSONData := new(ResponseJSON)
    
	// 通信開始時刻取得
	_now := time.Now()
	// 通信開始
	statusCode, err := validationAPI(urlAddress, responseJSONData)
	// 通信終了

	// 開始時刻からの時間を取得
	_duration := (time.Now()).Sub(_now)
		・・・ 省略・・・ 
	_second := _millisecond / 1000

	var message string
	message = urlAddress + "\n"

	if err != nil {
		message = message + "--- 問題あり ---" + "\n"
		・・・ 省略・・・ 
		err = RocketChatMessagePush(message)
	} else if _second > responseTime {
		message = message + "--- 問題あり ---" + "\n"
		・・・ 省略・・・ 
		err = RocketChatMessagePush(message)
	} else if responseJSONData.Status !=0 {
		message = message + "--- 問題あり ---" + "\n"
		・・・ 省略・・・ 
		err = RocketChatMessagePush(message)
	} else {
		message = message + "--- 問題なし ---" + "\n"
		・・・ 省略・・・ 
	}
}

ここまでをポーリングでまわせば死活監視できます。 さて、エラーをRocketChatに送信する方法です。

RocketChatの送信

ターゲットのチャットルーム(プライベートユーザー)送信するにはどうしたらいいのか RocketChatは、APIがきっちり用意されていてすごく簡単です。

メッセージを送信するAPIの仕様 Post a chat message を見ると 最初の行に「URL」「Requires Auth」「HTTP Method」が表にのっています。 「URL」把握背するところとわかり、「HTTP Method」は、POSTで送信しろと書いており、 「Requires Auth」が必要というようなことが書いております。 つまり、ログインしてる情報をひっつけてこのURLでPOSTすればいいことがわかります。

どうやってポストをするのか。パラメータがいっぱい書いてますけど良くわかりませんよね?(;^ω^) こういうときはサンプルがどっかに書いてあることが多いです。 「Example Call」と「Example Result」を見ます。 前者は、POSTの仕方が書いてあり、後者はそのresponseですね。 前者の「Example Call」を見ると、 curlでのコマンドが書いてあり、「-H」と「-d」とURLが引数となっています。 「-H」は、送信するヘッダのことで、「X-Auth-Token」と「X-User-Id」と、「Content-type:application/json」と言うのがあります。 「Content-type:application/json」は、送信方法をJSONで送ると言う意味で、 残りの、「X-Auth-Token」と「X-User-Id」は、たぶん、名前の通りで行くと認証の文字列でしょう。 おそらく、どこかで取得したものをここに設定するのだと思います。

っということは、もしかしたらログインの時にこのデータを作成しているのではないかなと推測します。 なので、REST APIで、認証で、ログインで入れるAPIの仕様を見てみましょう。 「rest-api/authentication/login/」をこれを見ると「Result」の項目を見ると!! 「authToken」というのと「userId」てのがありますね。そう、これが、先ほどの「X-Auth-Token」と「X-User-Id」に入ります。

細かいことは端折りますが、

  1. ログインで、「ユーザー」「パスワード」を入れて、POSTする。
  2. レスポンスで、「authToken」と「userId」を取得
  3. Post a chat messageで先ほどの「authToken」と「userId」をヘッダに、書き込むチャンネルと文言を一緒に送信する
  4. メッセージが書き込まれる。

てな具合です。簡単でしょ?

「HTTPステータスとレスポンスの取得」で作成した「validationAPI」に、 パラメータとヘッダーの設定し、送信するれば良いだけです。

これで、大体の作り方がわかったはずです。


/*
	RocketChatログイン
*/
func RocketChatLogin(urlAddress string, userName string, password string, res *RocketChatLoginResponse) error {
	jsonParam := RocketChatLoginParam{
		User:     userName,
		Password: password,
	}

	bytesData, err := json.Marshal(&jsonParam)

	if err != nil {
		return err
	}

	jsonStr := string(bytesData)
	req, err := http.NewRequest(
		"POST",
		urlAddress+"/api/v1/login",
		bytes.NewBuffer([]byte(jsonStr)),
	)
	if err != nil {
		return err
	}

	// Content-Type 設定
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	byteArray, _ := ioutil.ReadAll(resp.Body)

	responseJsonStr := string(byteArray)

	_ = json.Unmarshal([]byte(responseJsonStr), res)

	return nil
}

/*
	RocketChatログイン
*/
func RocketChatPostMessage(urlAddress string, authToken string, userId string, chatRoom string, message string) error {
	jsonParam := RocketChatPostMessageParam{
		Channel: chatRoom,
		Message: message,
	}

	bytesData, err := json.Marshal(&jsonParam)

	if err != nil {
		return err
	}

	jsonStr := string(bytesData)
	req, err := http.NewRequest(
		"POST",
		urlAddress+"/api/v1/chat.postMessage",
		bytes.NewBuffer([]byte(jsonStr)),
	)
	if err != nil {
		return err
	}

	// Content-Type 設定
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-User-Id", userId)
	req.Header.Set("X-Auth-Token", authToken)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	return nil
}

最後に

なんか説明してなく、ソースをあげているだけのような気がしていてしょうがないですが、(; ̄ー ̄A アセアセ・・・ よしとしてください。。。。 あと、Go言語好きな人集まりが出来たら良いなぁって最近は思ってます。。。。。

※ 端折って書いたりしておりますので実際のサンプルサーバと監視ツールを合体したソースをGistにあげておきます。