はじめに
私の所属しているVASILYでは非同期処理を行うためにsidekiq(http://sidekiq.org/ )というライブラリを利用しています。
sidekiqでは非同期処理用のジョブをRedis上のキューで管理します。
このキューにどのくらいの数のジョブが入っているのかという情報をmackerelのカスタムメトリクスとして管理するためのプラグインを書いたのでご紹介します。
mackerel-agentはgoで書かれており、その影響か公式のプラグインもgoで書かれています。
https://github.com/mackerelio/mackerel-agent-plugins
公式ライブラリを幾つか見るとgoの練習にちょうどよさそうな難易度でしたので、goの勉強も兼ねてmackerelプラグインを書いてみることにしました。
このプラグインのソースコードはhttps://github.com/vasilyjp/mackerel-plugin-sidekiq に、
バイナリはhttps://bintray.com/shio-phys/mackerel-plugin-sidekiq/mackerel-plugin-sidekiq/view#files に配置してあります。
プラグインを作るのに便利なライブラリたち
カスタムメトリクスを投稿する方法はmackerelの公式ヘルプに書いてあります。
http://help-ja.mackerel.io/entry/advanced/custom-metrics
それによると、標準出力に以下のフォーマットで1メトリクス1行で出力すればいいそうです。
{metric name}\t{metric value}\t{epoch seconds}
(\tはタブ文字)
また、環境変数MACKEREL_AGENT_PLUGIN_META
が1
の時にグラフ定義を出力するようにするとmackerel側がグラフのタイトルや凡例を自動的にセットしてくれるみたいです。
グラフ定義は先頭行を# mackerel-agent-plugin
から始めてその後にグラフ定義本体のJSONを続けます。
グラフ定義の仕様はmackerel API仕様のグラフ定義の投稿の部分に書かれています。
http://help-ja.mackerel.io/entry/spec/api/v0#graphdef-post
これらの機能を全部自分で実装してしまっても良かったのですが、プラグインを作るためのヘルパーライブラリがmackerel公式から出ていたので、それを利用してプラグインを作ってみました。
https://github.com/mackerelio/go-mackerel-plugin
また、goのRedisクライアントとしてredigoを利用しました。
https://github.com/garyburd/redigo
これらのライブラリのインストールはgo言語に標準添付されているパッケージマネージャーを利用して行いました。
# 事前に環境変数GOPATHを設定しておく必要あり
# https://golang.org/doc/install#testing
go get github.com/mackerelio/go-mackerel-plugin
go get github.com/garyburd/redigo/redis
グラフの定義
まずはこのプラグインを表す構造体を定義します。
type SidekiqPlugin struct {
Target string
Database string
Namespace string
}
この構造体の各フィールド(Target, Database, Namespace)はRedisに接続するための情報です。
詳細についてはRedisに接続をする部分で説明します。
次にこの構造体にPluginインターフェースを実装していきます。
Pluginインターフェースはgo-mackerel-pluginの中で次のように定義されているインターフェースです。
// mackerel-plugin.go:28
type Plugin interface {
FetchMetrics() (map[string]float64, error)
GraphDefinition() map[string]Graphs
}
グラフ定義を返すためのメソッドGraphDefinition
と、メトリクスの情報を返すためのメソッドFetchMetrics
があります。
最初にGraphDefinition
メソッドを実装してみましょう。
このメソッドの返り値はグラフ名とをキーとして、そのグラフの定義(Graphs
構造体)をバリューとするmapオブジェクトです。
グラフ名はグラフ内の各メトリクス名のプリフィックスになります。
Graphs
構造体は次のように定義されています。
type Graphs struct {
Label string `json:"label"`
Unit string `json:"unit"`
Metrics []Metrics `json:"metrics"`
}
LabelフィールドでMackerelで表示されるグラフのタイトル(下図)を指定します。
Unitフィールドでこのグラフで表示するメトリクスの単位を指定します。
Web UIからグラフの設定を行うときの「単位」に相当します。
Metricsでこのグラフに表示するメトリクスの一覧を指定します。
Metrics構造体は以下のように定義されています。
//mackerel-plugin.go:14
type Metrics struct {
Name string `json:"name"`
Label string `json:"label"`
Diff bool `json:"diff"`
Stacked bool `json:"stacked"`
Scale float64 `json:"scale"`
}
Nameフィールドはメトリクス名の後ろ半分を示し、Graphs
構造体の中のNameと合わせて完全なメトリクス名となります。
Diffフィールドはこのメトリクスの値を差分値として扱うか否かを指定します。
このフィールドにtrueを指定すると、前回の値との差分が投稿されます。
Stackedフィールドはこのメトリクスを積み上げグラフとして表示するか否かを指定します。
trueを指定すると積み上げグラフ、falseを指定すると折れ線グラフとして表示されます。
Scaleはメトリクスのスケールを指定します。
ここに値0以外の値を指定するとFetchMetrics
メソッドで取得した値にScaleの値を乗じた値が投稿されます。
では実際に、Sidekiq用のグラフ定義を作ってみましょう。
sidekiqのキューとして、queue1〜queue3があることを想定しています。
import (
mp "github.com/mackerelio/go-mackerel-plugin"
)
func (s SidekiqPlugin) GraphDefinition() map[string](mp.Graphs) {
return map[string](mp.Graphs){
"sidekiq.enqueued": mp.Graphs{
Label: "Sidekiq enqueued",
Unit: "integer",
Metrics: [](mp.Metrics){
mp.Metrics{Name: "queue1", Label: "queue1", Diff: false, Stacked: true},
mp.Metrics{Name: "queue2", Label: "queue2", Diff: false, Stacked: true},
mp.Metrics{Name: "queue3", Label: "queue3", Diff: false, Stacked: true},
},
},
}
}
ここでは1つのグラフ(Sidekiq enqueued)とそこに対応する3つのメトリクス(queue1, queue2, queue3)を定義しています。
そして、それらの値を積み上げグラフとして表示するように指定しています。
キューの種類が前もって分かっているのならばこのままでもいいのですが、せっかくなのでキューの種類をRedisから取得し、そこからメトリクスも動的に生成してみましょう。
キューの種類はRedisのqueuesにセット型として入っているので、以下のようなメソッドを作ってキューの種類を取得します。
なお、ここでs.Target
には接続先のRedisのIPアドレスが、s.Database
にはsidekiqの情報が入っているDB番号が入っていると想定しています。
また、s.Namespace
にはsidekiqの情報のキーのprefixが入っていると想定しています。RedisNamespace(https://rubygems.org/gems/redis-namespace/ )を使用している場合はこのフィールドに値をセットする必要があります。
func (s SidekiqPlugin) GetQueues() ([](string), error) {
c, err := redis.Dial("tcp", s.Target)
if err != nil {
return nil, err
}
defer c.Close()
prefix := ""
if s.Namespace != "" {
prefix = s.Namespace + ":"
}
_, err = c.Do("SELECT", s.Database)
if err != nil {
return nil, err
}
queues, err := redis.Strings(c.Do("SMEMBERS", prefix+"queues"))
if err != nil {
return nil, err
}
return queues, nil
}
ここからMetricsオブジェクトのsliceを生成するためには以下のような関数を定義しました。
func (s SidekiqPlugin) GetEnqueuedMetrics() ([](mp.Metrics), error) {
metrics := make([](mp.Metrics), 0)
queues, err := s.GetQueues()
if err != nil {
return nil, err
}
for _, queue := range queues {
metrics = append(metrics, mp.Metrics{Name: "queue." + queue, Label: queue, Diff: false, Stacked: true})
}
return metrics, nil
}
これを使用すると、先ほどのGraphDefinition
は以下のように書き換えられます。
func (s SidekiqPlugin) GraphDefinition() map[string](mp.Graphs) {
enqueued_metrics, err := s.GetEnqueuedMetrics()
if err != nil {
return nil
}
return map[string](mp.Graphs){
"sidekiq.enqueued": mp.Graphs{
Label: "Sidekiq enqueued",
Unit: "integer",
Metrics: enqueued_metrics,
},
},
}
}
メトリクスの取得
メトリクスの値の投稿のためにFetchMetrics
メソッドをSidekiqPlugin構造体に実装します。
FetchMetrics
メソッドは複数のメトリクスを取得してその値をmap[string]float64
型で返すメソッドです。
返り値のmapのキー名はGraphDefinition
メソッドで指定した名前を指定します。
今回のケースではqueue.<キュー名>
になります。
GraphDefinition
と同様にキューの種類を動的に取得しています。
そしてそれらのキューごとに、エンキューされているJOBの数を取得してmetricsに格納しています。
func (s SidekiqPlugin) FetchMetrics() (map[string]float64, error) {
c, err := redis.Dial("tcp", s.Target)
if err != nil {
return nil, err
}
defer c.Close()
prefix := ""
if s.Namespace != "" {
prefix = s.Namespace + ":"
}
_, err = c.Do("SELECT", s.Database)
if err != nil {
return nil, err
}
queues, err := redis.Strings(c.Do("SMEMBERS", prefix+"queues"))
if err != nil {
return nil, err
}
metrics := make(map[string]float64)
for _, queue := range queues {
enqueued, err := redis.Int(c.Do("LLEN", prefix+"queue:"+queue))
if err != nil {
return nil, err
}
metrics["queue."+queue] = float64(enqueued)
}
return metrics, nil
}
メイン関数
あとは、上記のメソッドにコマンドラインオプションをパースする処理や、MackerelPluginオブジェクトを設定する処理を追加すればプラグインの完成です。
接続先のRedisの情報やプラグインが利用する一時ファイルの設定をコマンドラインオプションとして取得できるようにしています。
標準出力に対して出力を行う時には環境変数MACKEREL_AGENT_PLUGIN_META
の値をチェックし、グラフ定義の出力か、メトリクスの値の出力かを切り替えています。
package main
import (
"flag"
"os"
)
func main() {
optHost := flag.String("host", "127.0.0.1", "Hostname")
optPort := flag.String("port", "6379", "port")
optTempfile := flag.String("tempfile", "", "Temp file name")
optDB := flag.String("db", "0", "Database")
optNamespace := flag.String("namespace", "", "Namespace")
flag.Parse()
var sidekiq SidekiqPlugin
sidekiq.Target = *optHost + ":" + *optPort
sidekiq.Database = *optDB
sidekiq.Namespace = *optNamespace
helper := mp.NewMackerelPlugin(sidekiq)
if *optTempfile != "" {
helper.Tempfile = *optTempfile
} else {
helper.Tempfile = "/tmp/mackerel-plugin-sidekiq-" + *optHost + "-" + *optPort
}
if os.Getenv("MACKEREL_AGENT_PLUGIN_META") != "" {
helper.OutputDefinitions()
} else {
helper.OutputValues()
}
}
mackerel-agentへの組み込み
あとは、作成したプラグインをmackerel-agentに組み込めば、WEB UIからグラフを見ることができます。
/etc/mackerel-agent/mackerel-agent.conf
に以下の行を追加します。
[plugin.metrics.sidekiq]
command = "/path/to/mackerel-plugin-sidekiq -port=6379 -db=0"
その後、mackerel-agentの再起動を行うと設定ファイルが読み込まれ、カスタムメトリクスの投稿が始まります。
グラフ定義の時にStacked: true
を指定しているので、積み上げグラフとして表示されます。
バイナリの配布(おまけ)
goはクロスコンパイルが容易なので、クロスコンパイルをしてバイナリを配布してみました。
注:mackerel-pluginの作成を行うためにはこの作業は必須ではありません。
mackerel-agentはLinuxとWindowsの32/64ビット環境で動作するので、これらの環境のバイナリを作ってみることにします。
http://help-ja.mackerel.io/entry/overview#support-environments
クロスコンパイルをするためには環境変数GOOS
とGOARCH
を適切なものに設定したのちにgo build
を行えばようです。
複数環境のバイナリをコンパイルするシェルスクリプトを自分で書いてもいいですが、クロスコンパイルを簡単に行うためのライブラリであるgoxを利用してみました。
gox自身もgoで書かれているので、インストールにはgo標準のパッケージマネージャーを使います。
go get github.com/mitchellh/gox
あとは以下のコマンドを叩くだけで複数環境用のバイナリを生成できます。
gox -os="linux windows" -arch="386 amd64"
クロスコンパイル後のバイナリはbintrayに配置し、ダウンロードできるようにします。
bintrayはcurlでREST APIを叩くだけでバイナリのアップロードを行うことができるので、CUIだけで全ての操作を完結させることができて便利です。
https://bintray.com/docs/api/#_content_uploading_publishing
まとめ
わずか200行足らずのコードでmackerelのカスタムメトリクスの投稿ができました。
go tutorialを終えた直後にやると丁度良いくらいの難易度なので、goの練習問題としても最適だと思います。
ユニットテストをもう少し充実させたら公式のプラグイン集にたいしてPRを投げてみたいと思います。
明日は @mackee_wさんです
参照
http://help-ja.mackerel.io/entry/advanced/custom-metrics
https://github.com/mperham/sidekiq/wiki
https://bintray.com/docs/api/