3. 設定ファイルと、設定の構造体を作る
まずは設定ファイルの作成と、その設定を読み込む処理を作成します。
参考: https://www.elastic.co/guide/en/beats/libbeat/current/new-beat.html#config-method
Jstatbeatでは「GC情報を取得する間隔(ミリ秒)」と、jpsを実行した時に取れる「プロセス名」の
2つを設定できるようにします。
設定ファイルはtopbeatのものをコピーして、設定部分だけ変えるのが良いでしょう。
https://github.com/elastic/beats/blob/master/topbeat/etc/topbeat.yml
/etc/jstatbeat/jstatbeat.yml
input:
interval: 5000
name: "Elasticsearch"
そして、この設定ファイルに対応する構造体を作ります。
/beat/config.go
package beat
type JstatConfig struct {
Interval *string `yaml:"interval"`
Name *string `yaml:"name"`
}
type ConfigSettings struct {
Input JstatConfig
}
続いて、本体となるJstatbeatクラスのうち、設定ファイルを読み込むConfig関数を作成します。
/beat/jstatbeat.go
package beat
type Jstatbeat struct {
interval string
name string
pid string
isAlive bool
}
func (jsb *Jstatbeat) Config(b *beat.Beat) error {
var config ConfigSettings
err := cfgfile.Read(&config, "")
if err != nil {
logp.Err("Error reading configuration file: %v", err)
return err
}
jsb.name = *config.Input.Name
if config.Input.Interval != nil {
jsb.interval = *config.Input.Interval
} else {
jsb.interval = "5000"
}
return nil
}
(1) Jstatbeat自身のフィールドとして interval と name を定義しておき
(2) Config関数の中で
(3) 設定ファイルを読み込んで
(4) 読み込んだ値を、フィールドに代入します。
これでConfig関数は完成です。
ね、簡単でしょう?
4. 起動時の処理を作る
続いて、起動時の処理を行うSetup関数を作成します。
起動時の処理では、jpsコマンドを実行して、監視対象となるJavaプロセスのプロセスIDを取得します。
参考: https://www.elastic.co/guide/en/beats/libbeat/current/new-beat.html#setup-method
/beat/jstatbeat.go
type Jstatbeat struct {
interval string
name string
pid string
isAlive bool
}
func (jsb *Jstatbeat) Setup(b *beat.Beat) error {
cmd := exec.Command("jps")
stdout, err := cmd.StdoutPipe()
if err != nil {
logp.Err("Error get stdout pipe: %v", err)
return err
}
cmd.Start()
TODO
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
items := strings.Split(line, " ")
if len(items) == 2 && items[1] == jsb.name {
jsb.pid = items[0]
break
}
}
cmd.Wait()
if len(jsb.pid) == 0 {
logp.Err("No target process: %v", jsb.name)
return errors.New("No target process: " + jsb.name)
}
return nil
}
(1) Jstatbeatのフィールドとして pid を定義しておき
(2) Setup関数の中で
(3) jpsコマンドを実行して
(4) 設定で指定した name に一致するプロセスの pid を取得します
5. メインの処理を作る。
いよいよ、一番のメイン処理となるRun関数の実装です。
https://www.elastic.co/guide/en/beats/libbeat/current/new-beat.html#run-method
処理が少し長いので、いくつかのブロックに分けて説明します。
/beat/jstatbeat.go
func (jsb *Jstatbeat) Run(b *beat.Beat) error {
jsb.isAlive = true
cmd := exec.Command("jstat", "-gc", "-t", jsb.pid, jsb.interval)
stdout, err := cmd.StdoutPipe()
if err != nil {
logp.Err("Error get stdout pipe: %v", err)
return err
}
cmd.Start()
defer killProcess(cmd)
return nil
}
func killProcess(cmd *exec.Cmd) {
if cmd.Process != nil {
err := cmd.Process.Kill()
if err != nil {
logp.Err("Error killing jstat process: %v", err)
}
}
}
(1) jstatコマンドに-gcオプションと-tオプションをつけて実行します。
また、pidやintervalは、それぞれSetupとConfigで取得したものを渡します。
(2) コマンドを実行した後は、エラーなどで処理が止まってしまって良いように
deferでプロセス停止の関数を呼ぶようにしておきます。
/beat/jstatbeat.go
var blanks = regexp.MustCompile(`\s+`)
func (jsb *Jstatbeat) Run(b *beat.Beat) error {
jsb.isAlive = true
scanner := bufio.NewScanner(stdout)
var keys []string
var version string
for jsb.isAlive && scanner.Scan() {
line := scanner.Text()
values := blanks.Split(line, -1)
if len(values) > 2 && values[0] == "Timestamp" {
keys = values
if strings.Contains(line, "CCSC") {
version = "java8"
} else {
version = "java5"
}
continue
}
}
return nil
}
(3) jstatの結果を一行ずつ読み出しながら
(4) 正規表現を使って、空白で値を分割しています。
(5) 1行目(見出し行)の場合は、分割した値をkeysとして保持しておきます。
(6) また念のため、jstatで取れた情報から、JVMがJava5〜7なのか、Java8だったのかを判断しています。
この判断結果は、elasticsearchのtypeとして利用します。
/beat/jstatbeat.go
func (jsb *Jstatbeat) Run(b *beat.Beat) error {
for jsb.isAlive && scanner.Scan() {
line := scanner.Text()
values := blanks.Split(line, -1)
event := common.MapStr{
"@timestamp": common.Time(time.Now()),
"type": version,
}
for i, key := range keys {
if len(key) == 0 {
continue
}
event[key] = toFloat(values[i+1])
}
b.Events.PublishEvent(event)
}
return nil
}
func toFloat(str string) float64 {
value, err := strconv.ParseFloat(str, 64)
if err != nil {
logp.Err("Cannot parser to float. Ignore this value: %v", err)
return 0
}
return value
}
(7) jstat実行結果の2行目以降に対して、行に対応する内容をMapとして作成し、
(8) すべての項目に対して
(9) 項目 = 値となるように、Mapに格納しています。また値はstringではなくfloat64に変換してから渡しています。
(10) そして、それをlibbeatのPublishEvent関数を使って送信します。
要はRun関数では、Mapを作ってPublishEventする、ということです。
Java5とJava8の両方に対応させるため、若干、処理がややこしくなっていますが、
簡単なbeatを作るだけなら、ホントに十数行で済むんじゃないかと思います。
7. main関数の作成
最後にメイン関数の作成です。
Jstatbeatのインスタンスを作成して、それをlibbeatに渡すだけです。
参考: https://www.elastic.co/guide/en/beats/libbeat/current/new-beat.html#_the_main_function
/beat/jstatbeat.go
func New() *Jstatbeat {
return &Jstatbeat{}
}
/main.go
package main
import (
jsatbeat "./beat"
"github.com/elastic/libbeat/beat"
)
func main() {
beat.Run("jstatbeat", "0.1", &jsatbeat.Jstatbeat{})
}
(1) go言語お作法に従って、Jstatbeatのインスタンスを作成するNew関数を用意しておきます。
(2) また、beatという名前空間がlibbeat被るため、jstatbeatというエイリアスをつけておき
(3) main関数では、実質1行で処理を書いてしまいます。
このRunの第一引数で渡した"jstatbeat"は、設定ファイル名や、Elasticsearchのindex名などでも
利用するため重要です。index名で使う都合上、名前は英語の小文字だけにしてください。
これで本体ソースコードは完成です。
ほら、簡単でしょう?
9. ダッシュボード
最後に、kibanaでダッシュボードを作りましょう。
今回は、こんな感じのダッシュボードを作ってみました。
なお手動でダッシュボードを作ったあと、dashboardやvisualizationを
kibana上でexportしておくと、別の環境でもimportすることができます。
なのでexportしたものを配布物と一緒に提供するのが良いかと思います。