Mackerel プラグインを書いてみよう

こんにちは。斎藤です。

logo.png

日本発としては数少ないサーバモニタリングサービス「Mackerel」。はてなさんが開発し、先月から正式にサービスが開始されました。このブログを読まれている方で、利用されている方もいらっしゃるのではないでしょうか。

さて、Mackerelの特徴に、自分自身でプラグインを開発すれば、カスタムメトリックとして自由にメトリックを追加できる事があります。最近、私もプラグインを書いて本家のプラグイン集リポジトリにマージいただきました。せっかくですので、その時に私が確認したお話をまとめておこうかと思います。4節「プラグインが行うこと」「ヘルパーライブラリを活用しよう」「コーディングの型」そして「その他のポイント」に分けてお話しします。

本記事は、Golangでの開発を1度でも行った事がある、または"A Tour of Go"を通じてGolangを学習した事がある方を対象とします。Golangは1.3以上を利用し、開発環境を整えて下さい。また、Gitの知識があるとより良いかと思います。

※2014/10時点での内容です。最新の情報は、GitHubのリポジトリやドキュメントを確認下さい。

プラグインが行うこと

Mackerelプラグインが行う事は3つあります。

  1. メトリックの出力
  2. メトリック定義の出力
  3. メトリックの差分情報の保持

1は、最も基本となる機能です。Sensu Metrics出力形式に沿って、プラグインからmackerel-agentへメトリックを返します。最低限、この機能を作り込んでおけば、Mackerelにてメトリックを蓄積する事ができます。もちろん、Sensuに対しても利用可能です。

2は、MackerelのWeb管理画面から都度手動でグラフ定義に「表示名」「ラベル名」「表示順」を入れなくとも、プラグイン側で予め定義を行う事ができます。実際には、mackerel-agentの起動時、利用する設定を行った全てのプラグインに対し"MACKEREL_AGENT_PLUGIN_META"環境変数をセットした上で起動を試みます。その際、プラグイン側は環境変数がセットされていた時だけ、プラグインが返すメトリックの定義を返すよう実装します。そうすると、Web管理画面上にその定義が自動的に登録されます。後で参照しやすくなりますので、ぜひ組み込みたい所です。

3は、値の変化を捉える必要があるメトリックが存在する場合に必要です。例えば、Linuxの"/proc/stat"にある"ctxt"(コンテキストスイッチ)の回数を計測したい場合、前回のメトリック取得時との差分を算出し値を返す事となります。その際に必要となる前回のメトリックは、プラグイン側で保存しなければなりません。多くの場合、"/tmp"に保存します。

ヘルパーライブラリを活用しよう

先の節で、プラグインが行うことを述べました。実は、これを全てスクラッチから実装する必要はありません。はてなさんにて"go-mackerel-plugin"というヘルパーライブラリが用意されています。こちらを利用すると、ぐっと手軽にプラグインを実装する事ができます。

何をしてくれるのか、先ほどの1〜3に対してどのような施しが行われるのか確認します。

1に対しては、ヘルパーライブラリがSensu Metrics形式に整形してmackerel-agentへ値を返す事ができます。

2に対しても、1と同様にヘルパーライブラリが整形してくれます。

3に対しては、前回取得時の履歴を保存、そして比較をするロジックがヘルパーライブラリに実装されています。自分で書く際には、データソースからメトリックを取得するだけでかまいません。

では、早速開発環境を準備しましょう。冒頭で述べました通り、Golangの開発環境は既に整っている前提で記載します。

$ go get github.com/mackerelio/go-mackerel-plugin
$ cd ${GOPATH}/src/github.com/mackerelio/go-mackerel-plugin

あっという間に終わりました。すぐに整うってのは最高ですね!

コーディングの型

ヘルパーライブラリを活用するとより手軽に実装できるとお話ししましたが、実際にどのように使えば良いのでしょうか。

では、ヘルプの「ホストのカスタムメトリックを投稿する」と、付属のサンプルプログラム"memcached.go"(以下、サンプル)をもとに確認して行きます。

構造の概要

プラグインのソースは、主に5つのブロックで構成されます。

  • メトリック定義の記述
  • プラグインの挙動を決める変数の定義
  • メトリックの取得
  • メトリック定義の取得
  • main()関数

では、それぞれ確認して行きましょう。

メトリック定義の記述

サンプルの15行目に着目して下さい。ここに、メトリックを定義します。いくつか定義されている事を確認できるかと思います。

では、どのような定義とすれば良いか、サンプルの16行目にある"memcached.connections"に注目してみましょう。説明のため、引用したソースにはコメントを付加しています。

    "memcached.connections": mp.Graphs{
        Label: "Memcached Connections",  // グラフのタイトル ドキュメントの" graphs.{graph}.label "に相当
        Unit:  "integer",   // このメトリックが返す数値の型 ドキュメントの"graphs.{graph}.unit"に記載された
        Metrics: [](mp.Metrics){  // プラグインに返すメトリックの定義 ドキュメント"graphs.{graph}.metrics"に相当
            mp.Metrics{Name: "curr_connections", Label: "Connections", Diff: false},   // ※1
        },
    },

※1のメトリック定義、次の通りです。

  • Name: メトリックの名前を定義します。これは、Sensu Metric形式でmackerel-agentへ値を返す時と、後述するデータを蓄積するmap型の変数に収容された値を参照する際のキーに利用されます。
  • Label: グラフ上の表示名です。
  • Diff: 前回取得時とメトリックを比較した結果をグラフ化したい場合はtrueとします。それ以外の場合はfalseまたは未定義とします。
  • Stacked: このソースにはありません。積み上げグラフにしたい場合はtrueとします。falseまたは未定義だと折れ線グラフとなります。

プラグインの挙動を決める変数の定義

サンプルの82行目に目を移してください。ここで、プラグイン全体の挙動を決める構造体を定義します。

type MemcachedPlugin struct {
    Target   string
    Tempfile string
}

サンプルでは2種類定義されていますが、最低限"Tempfile"は実装して下さい。ヘルパーライブラリが必要とします。その他は、main()関数から引き渡す必要のある変数をコンテキスト変数として持たせておくと良いでしょう。

メトリックの取得

サンプルの87行目に目を移してみましょう。FetchMetrics()メソッドは、プラグインで最も重要となるメトリックの取得を行う処理を記述します。プラグインを書く人が一番がんばる所、とも言えます。

func (m MemcachedPlugin) FetchMetrics() (map[string]float64, error) {

mは、ヘルパーライブラリ側が保持していたコンテキスト変数が格納されています。後述するmain()部分で適切に値をセットしていれば、ここで利用する事ができます。

返す値として2つの変数"map[string]float64"と"error"を用意しなければなりません。前者は取得したメトリックを格納した値を返します。値のセット方法は後述します。後者は、エラーが発生した場合はその内容を返します。ヘルパーライブラリ側でエラーがハンドリングされます。何も無ければ"nil"とすればOKです。

さて、前述した返す値の中の"map[string]float64"は、FetchMetrics()中では"stat"という変数として用意されています。ここに、先の「メトリック定義の記述」の"Metrics"に定義した"Name"の値に沿って、メトリックをセットします。サンプルでは、memcachedからメトリックを正常に取得しきれたときに、100行目の"return stat, nil"が呼ばれます。それ以外のreturnはエラーが発生した時に呼ばれます。

return後、ヘルパーライブラリがメトリクスをmackerel-agentに返します。

メトリック定義の取得

サンプルの117行目に目を移してみましょう。GraphDefinition()メソッドでは、グラフ定義を取得します。通常は、先の「メトリック定義の記述」に定義した内容をそのまま返せば良いため、コードを書く事はほとんどありません。

func (m MemcachedPlugin) GraphDefinition() map[string](mp.Graphs) {
    return graphdef
}

ただし、環境によってメトリックの定義が動的に変化する場合、例えばサーバが認識しているストレージの数に応じて項目が動的に変わる場合はプログラムを書く必要が出てきます。動的にメトリックの定義を増減させて、適切なメトリック定義を返せるように実装します。

main()関数

サンプルの121行目は、おなじみのmain()関数です。ここで行う事が3つあります。

  1. 引数の取得
  2. ヘルパーライブラリ利用のための準備
  3. プラグイン内部の処理の実行開始

10は、122行目以降を見て下さい。こちらは、どのコンソールアプリケーションでは必要となる処理です。これは、サンプルの方法以外にもやり方があります。私は、mackerel-plugin-apache2にて"codegangsta/cli"を利用して取得しました。"tcnkms/cli-init"を活用して作るのもいいかもしれません。

11は、127行目以降を見て下さい。82行目で定義した構造体を利用し、ヘルパーライブラリ利用のための準備を行います。また、132行目でテンポラリファイルのパスを定義している部分があります。こちらは、前回取得したメトリックを保存するファイルを指定しています。そうです、先のメトリック定義で"Diff: true"にしたときに利用するファイルです。

    var memcached MemcachedPlugin

    memcached.Target = fmt.Sprintf("%s:%s", *optHost, *optPort)
    helper := mp.NewMackerelPlugin(memcached)

    if *optTempfile != "" {
        helper.Tempfile = *optTempfile
    } else {
        helper.Tempfile = fmt.Sprintf("/tmp/mackerel-plugin-memcached-%s-%s", *optHost, *optPort)
    }

12は、プラグインがmackerel-agentから呼ばれる際、定義を出力する"MACKEREL_AGENT_PLUGIN_META"環境変数付きで起動されたか、そうでないメトリック取得のために起動したかを確認し、実際にプラグイン内部の処理を始める関数を実行します。

    if os.Getenv("MACKEREL_AGENT_PLUGIN_META") != "" {
        helper.OutputDefinitions()
    } else {
        helper.OutputValues()
    }

実行結果のサンプル

サンプルを実行すると、私の開発環境では次のメトリックが返ってきます。なお、ローカルにmemcachedサーバが立ち上がっている前提で実行しています。テストサーバが開発環境とは別にある場合は、"--host"オプションを付加してサーバを指定して下さい(不用意に本番に対して実行したらダメですからね!)。


$ cd ${GOPATH}/src/github.com/mackerelio/go-mackerel-plugin/_example
$ go build
$ $ MACKEREL_AGENT_PLUGIN_META=1 ./_example
{
  "graphs": {
    "memcached.unfetched": {
      "metrics": [
        {
          "stacked": false,
          "diff": true,
          "label": "Expired unfetched",
          "name": "expired_unfetched"
        },
(中略)
      "unit": "bytes",
      "label": "Memcached Traffics"
    }
  }
}

$ ./_example   # 1回目なのでデータは取れない
$ sleep 60     # Mackerelは1分間隔データを取得するので1分待つ
$ ./_example   # 2回目だからデータが取れる
memcached.rusage.rusage_user    0.002727        1412672707
memcached.rusage.rusage_system  0.002727        1412672707
memcached.bytes.bytes_read      16.363636       1412672707
(後略)

皆さんの手元でもお試し下さい。

その他のポイント

本家のプラグイン集リポジトリにPull Requestを申請した際、私は次の点に留意しました。

  • Readme をつける
  • テストコードを書く

どちらもよくある話ですので、GitHubベースの開発に慣れている方ならおなじみの事かと思います。Readmeを書くのは、私にとっては英文を書く良い機会にもなりました。

あと、積極的にプラグインを開発されたい方は、Issueに「書きます!」という宣言をすると、他の方と開発がバッティングしなくて良いかもしれません。実際、書かれようとされているプラグインがIssueにリストアップされている事を確認しています。

おわりに

ここまで、Mackerelプラグインの開発に際し、私が確認した事柄を、「プラグインが行うこと」「ヘルパーライブラリを活用しよう」「コーディングの型」そして「その他のポイント」の4つに分けてご紹介しました。

実際に書いてみないとわからない事はあるかと思いますが、ソースを読んで理解を深める際の手がかりとしてこれらの情報を活用いただければ幸いです。

みんなで使って、さらにプラグインをコントリビュートすることで、国産サーバモニタリングサービス"Mackerel"をより盛り上げて行きましょう〜!!!

それでは皆様、ごきげんよう。

参考文献