Help us understand the problem. What is going on with this article?

sidekiq用のmackerelプラグイン書きました

More than 3 years have passed since last update.

はじめに

私の所属しているVASILYでは非同期処理を行うためにsidekiq(http://sidekiq.org/ )というライブラリを利用しています。

スクリーンショット 2015-12-15 14.50.08.png

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_META1の時にグラフ定義を出力するようにすると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で表示されるグラフのタイトル(下図)を指定します。
スクリーンショット_2015-12-15_18_42_46.png

Unitフィールドでこのグラフで表示するメトリクスの単位を指定します。
Web UIからグラフの設定を行うときの「単位」に相当します。
スクリーンショット_2015-12-15_18_48_27.png

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と合わせて完全なメトリクス名となります。

Labelフィールドはメトリクスの凡例の名前を示します。
スクリーンショット_2015-12-15_19_33_25.png

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の再起動を行うと設定ファイルが読み込まれ、カスタムメトリクスの投稿が始まります。
スクリーンショット 2015-12-16 18.19.22.png

グラフ定義の時にStacked: trueを指定しているので、積み上げグラフとして表示されます。

バイナリの配布(おまけ)

goはクロスコンパイルが容易なので、クロスコンパイルをしてバイナリを配布してみました。
注:mackerel-pluginの作成を行うためにはこの作業は必須ではありません。

mackerel-agentはLinuxとWindowsの32/64ビット環境で動作するので、これらの環境のバイナリを作ってみることにします。
http://help-ja.mackerel.io/entry/overview#support-environments

クロスコンパイルをするためには環境変数GOOSGOARCHを適切なものに設定したのちに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/

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away