ども、cloudpackかっぱ (@inokara) です。

はじめに

冬休みの宿題として Go を触ってみたいと思います。何か目的が無いと長続きしないと思ったので sensu プラグインを作りながら Go を学んでいきたいと思います。


Go とは

ひろみです。

Google が公開したプログラミング言語です。
Bash や Ruby とは異なりコンパイルして実行します。

Go とはについては自分のような初心者がどうこう言えませんので golang.jp をご覧ください。


sensu プラグインを作りながら

sensu プラグインについて

チェック用のプラグインについてはスクリプトに言語は問わず、終了のステータスコードを以下のように出力させることでプラグインを実装することが出来ます。(今回はメトリクスプラグインは触れません。)

ステータス コード
正常 0
Warning 1
Critical 2
上記以外 上記以外の数値

尚、Go で作るにあたって出来るだけ Sensu-Community-Plugin にアップされているスクリプトと同じ方法でチェック、結果を返すようにするのが目標です。

check-cpu

CPU の使用率をチェックするプラグインです。

参考にしたプラグインは check-cpu.rb です。このプラグインは /proc/stat の値をチェックします。

cpu  104528 0 38935 8960969 494 16 2972 0 0 0
cpu0 14094 0 13390 2244649 247 16 2571 0 0 0
cpu1 49156 0 6885 2221203 147 0 128 0 0 0
cpu2 12056 0 9542 2256779 66 0 132 0 0 0
cpu3 29222 0 9118 2238338 34 0 141 0 0 0
intr 2089129 25 10 0 0 12 0 0 0 1 0 0 0 149 0 0 0 289 0 0 84791 15 28287 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ctxt 3247041
btime 1419826317
processes 16404
procs_running 1
procs_blocked 0
softirq 2257245 0 744345 4 197647 25399 0 1 586480 4911 698458

以下のように /proc/stat の一行目を一定間隔で取得してその差分を計算して CPU 使用率を算出します。

cpu  104528 0 38935 8960969 494 16 2972 0 0 0

当初は以下のように /proc/stat を頑張って読み込んでいました。

func acquire_cpu_stats(p string)(s string) {
    // /proc/stat を読み込む     
        cpu_stats, err := os.OpenFile(p, os.O_RDONLY, 0)

        if err != nil {
                fmt.Println(err)
                os.Exit(1)
        }

        // ファイルを閉じる(defer 関数で main 関数の処理が終わる際(最後)に実行してくれる))
        defer func() {
                cpu_stats.Close()
        }()

    // 読み込んだ /proc/stat を一行ずつ読み込んで "^cpu " な行(一行目)をだけを取得して main() に返却
        scanner := bufio.NewScanner(cpu_stats)
        for scanner.Scan() {
                // 返り値が二つあるけど一つしか使わないのでブランク識別子(_)を入れる
                matched, _ := regexp.MatchString("^cpu ", scanner.Text())
                if matched {
                        return scanner.Text()
                }
        }

        if err := scanner.Err(); err != nil {
                fmt.Fprintln(os.Stderr, "reading standard input:", err)
        }
        return s
}

ところが、ところが、以下のパッケージを利用することで cat /proc/stat の処理をパッケージにお願いすることが出来るようになり、自前で /proc/stat を読み込んで処理をする必要がなくなりました。

$GOPATH を指定した後で以下のようにパッケージを取得するだけで利用出来るようになります。

export $GOPATH=/path/to/go
go get bitbucket.org/bertimus9/systemstat

このパッケージを使うことで以下のように書けました。パッケージのおかげで基本的には取得した値を比較してそれぞれのステータスコードを返す処理を行うだけです。

package main

import (
        "bitbucket.org/bertimus9/systemstat"
        "fmt"
        "time"
        "flag"
        "os"
        "strconv"
)

var last_cpu systemstat.CPUSample
var current_cpu systemstat.CPUSample
var cpu_stat systemstat.SimpleCPUAverage
var cpu_average systemstat.CPUAverage

func main() {

...
(snip)
...
        if cpu_stat.BusyPct > critical {
                fmt.Println("critical")
                os.Exit(2)
        } else if cpu_stat.BusyPct > warning {
                fmt.Println("warning")
                os.Exit(1)
        } else {
                fmt.Println("ok")
                os.Exit(0)
        }
}

引数を扱う為には flag を利用して Warning と Critical を指定出来るようにしています。

...
(snip)
...
        var w = flag.String("warning", "80", "Warning Level(%)")
        var c = flag.String("critical", "100", "Critical Level(%)")
        //var s = flag.Int("Sleep", 1000, "Set SleepTime")
        flag.Parse()
...
(snip)
...

-h を指定すると以下のようにヘルプメッセージにも対応しています。

Usage of ./check-sysstat-cpu:
  -critical="100": Critical Level(%)
  -warning="80": Warning Level(%)

実際にプラグインをコンパイルして実行すると以下のに出力されます。

# ./check-sysstat-cpu -warning=80 -critical=100
CPUAverage={3.846153846153846 0 0.2564102564102564 95.89743589743588 0 0 0 0 0 2014-12-29 20:01:50.213357406 +0900 JST 1.001324773}
ok

CPUAvereage から始まる行はデバッグ用です。

{
  "checks": {
    "check_cpu": {
      "handlers": ["default"],
      "command": "/etc/sensu/plugins/sensu-plugins-go/check-cpu/check-cpu -warning=80 -critical=12345",
      "interval": 10,
      "standalone": true
    }
  }
}

check-mem

監視対象のメモリ空き容量をチェックするプラグインです。

このプラグインも systemstat パッケージを利用させて頂きます。

...
(snip)
...
        // Buffers Cached MemTotal MemUsed MemFree SwapTotal SwapUsed SwapFree time.Time
        // {80888 648992 2056668 1881204 175464 1438004 476 1437528 2014-12-30 09:57:09.298455307 +0900 JST}
        mem := systemstat.GetMemSample()
        // 純粋な空き容量を算出
        mem_free_uint := (mem.Buffers + mem.Cached + mem.MemFree)
        // uint64 => String => float64
        mem_free, _ := strconv.ParseFloat(strconv.FormatUint(mem_free_uint, 10), 64)
...
(snip)
...

mem 変数を宣言(省略形で)して systemstat.GetMemSample() の値を格納しています。

{80888 648992 2056668 1881204 175464 1438004 476 1437528 2014-12-30 09:57:09.298455307 +0900 JST}

mem の中身は上記のようになっており時刻以外の型は uint64 になっていますので、パーセンテージを算出する為に型の変換を行っています。(uint64 を直接 float64 に変換する良い方法が解らなかったので uint64 => String => float64 という変換をしています…)

実際にビルドしたものを実行させると以下のように出力されます。

# ./check-sysstat-memory -warning=10 -critical=5
Free Memory: 933516.00 KB
Total Memory: 2056668.00 KB
Free Percent: 45.39 %
Used Percent: 54.61 %
ok

このプラグインスクリプトは sensu のチェック設定に以下のように設定します。

{
  "checks": {
    "check_mem": {
      "handlers": ["default"],
      "command": "/etc/sensu/plugins/sensu-plugins-go/check-memory/check-sysstat-memory -warning=10 -critical=5",
      "interval": 10,
      "standalone": true
    }
  }
}

check-port

各種サービスの TCP / UDP のポートチェックするプラグインです。

check-ports.rb は nmap を利用して指定したポートが開放されているかをチェックしていますが、check-port.go は Go の net パッケージを利用してポートの開放をチェックしています。

// hoge project main.go
package main

import (
    "flag"
    "fmt"
    "net"
    "os"
)

func main() {
    //(name, default, help)
    var proto = flag.String("prot", "tcp", "Set Protocol")
    var hostname = flag.String("hostname", "www.example.com", "Set Hostname")
    var port = flag.String("port", "80", "Set Port Number")
    flag.Parse()

    fmt.Print("Checking..." + *hostname + ":" + *port + "n")
    conn, err := net.Dial(*proto, *hostname+":"+*port)

    if err != nil {
        fmt.Println(err)
        os.Exit(2)
    } else {
        fmt.Print("OK" + "n")
        os.Exit(0)
    }

    defer func() {
        conn.Close()
    }()
}

このスクリプトは「基礎からわかる Go言語」に載っていたサンプルに引数対応しただけです(汗)。このスクリプトをビルドして以下のように実行します。

# ./chek-port -hostname=www.google.com -port=80
Checking...www.google.com:80
OK

このプラグインスクリプトは sensu のチェック設定に以下のように設定します。

{
  "checks": {
    "check_port": {
      "handlers": ["default"],
      "command": "/etc/sensu/plugins/sensu-plugins-go/check-port/check-port -hostname=foo.bar -port=12345",
      "interval": 10,
      "standalone": true
    }
  }
}

ということで…

引き続き、Go を勉強していきたいと思いますが、気づいた点をいくつか。

自分はシェルスクリプトをちょっと触るくらいのレベルですが、こんなにも値の型を意識しなければいけない(異なる型同士の比較や代入等)のかと驚きました。C や Java 等を常に触っている方にとっては当たり前のことなのかもしれませんが。

代入(ブランク識別子)

人さまのソースコードを見ていて気づいたのが以下のような _ への代入です。

// baz が使われない場合にはエラーとなる
foo, baz = bar()
// 以下のように一つ目の戻り値しか利用しない場合に 2 番目の戻り値には _ を指定する
foo, _ = bar()

これはブランク識別子と呼ばれるもので関数の戻り値が複数ある場合、一つの戻り値しか利用しない場合に明示的に _ 指定してあげることでコンパイルエラーを防ぐ目的があるようです。(※ Go では利用しない変数を宣言しておくとコンパイルエラーになるようです)

変数

利用する変数はあらかじめ型や初期値について以下のように指定する必要があります。

var foo string = "bar"

ちなみに、以下のように初期値が与えられている場合には型を省略することも可能です。

var foo = "bar"

尚、上記の場合の変数 foo の型は String になるようです。さらに、以下のように var も省略することが出来るようです。

foo := "bar"

面白いですね。

引き続き…

この冬休みは Go で郷、郷していきたいと思います。

元記事はこちらです。
sensu プラグインを作って学ぶ Go