tkuchikiの日記

Linux やプログラミングについて書きます。

mackerel-plugin-rack-stats を例にした mackerel agent plugin の作り方

この記事は、Mackerel Advent Calendar 2015 16日目の記事です。

mackerel-agent-plugins には様々な plugin がありますが、無い場合は自作する必要がありますよね。
ということで、mackerel-plugin-rack-stats を例に mackerel agent plugin の作り方を紹介します。

mackerel-plugin-rack-stats

raindrops の stats を収集する plugin です。
raindrops が Linux でしか動かないようですので、
必然的にこのプラグインLinux でしか利用できません。

mackerel agent plugin の作り方

go-mackerel-plugin-helper を使うと簡単に実装することができます。
mackerel-agent-plugins の中には go-mackerel-plugin を使っているものもありますが、
1年近く更新されていないようですので、go-mackerel-plugin-helper を使うのが良さそうです。

それでは、順に説明していきます。

func main()

// mackerel-plugin-rack-stats
package main

import (
    "flag"
    "fmt"
    ...
    mp "github.com/mackerelio/go-mackerel-plugin-helper"
)

func main() {
    optAddress := flag.String("address", "http://localhost:8080", "URL or Unix Domain Socket")
    optPath := flag.String("path", "/_raindrops", "Path")
    optMetricKey := flag.String("metric-key", "", "Metric Key")
    optVersion := flag.Bool("version", false, "Version")
    optTempfile := flag.String("tempfile", "", "Temp file name")
    flag.Parse()

    if *optVersion {
        fmt.Println("0.2")
        os.Exit(0)
    }

    var rack RackStatsPlugin
    rack.Address = *optAddress
    rack.Path = *optPath
    rack.MetricKey = *optMetricKey
    helper := mp.NewMackerelPlugin(rack)

    if *optTempfile != "" {
        helper.Tempfile = *optTempfile
    } else {
        helper.Tempfile = fmt.Sprintf("/tmp/mackerel-plugin-rack-stats")
    }

    helper.Run()
}

オプション解析に標準モジュールの flag を使っていますが、
機能が充実したライブラリを使用しても良いと思います。
以降は、処理ごとに分割して説明します。

MackerelPlugin の生成

// mackerel-plugin-rack-stats
var rack RackStatsPlugin
rack.Address = *optAddress
rack.Path = *optPath
rack.MetricKey = *optMetricKey
helper := mp.NewMackerelPlugin(rack)

go-mackerel-plugin-helper.NewMackerelPluginMackerelPlugin 構造体を生成します。
MackerelPluginPlugin を変数に持っており、
PluginFetchMetrics()GraphDefinition() を実装する必要があります。
それぞれの定義は以下のとおりです。

// go-mackerel-plugin-helper
type Plugin interface {
    FetchMetrics() (map[string]interface{}, error)
    GraphDefinition() map[string]Graphs
}

type MackerelPlugin struct {
    Plugin
    Tempfile string
}

func NewMackerelPlugin(plugin Plugin) MackerelPlugin {
    mp := MackerelPlugin{plugin, "/tmp/mackerel-plugin-default"}
    return mp
}

FetchMetrics()GraphDefinition() の説明は後ほど行います。

GraphDefinition() の返り値である Graphs と、
その引数である Metrics の定義は以下のとおりです。

// go-mackerel-plugin-helper

type Graphs struct {
    Label   string    `json:"label"`
    Unit    string    `json:"unit"`
    Metrics []Metrics `json:"metrics"`
}

type Metrics struct {
    Name    string  `json:"name"`
    Label   string  `json:"label"`
    Diff    bool    `json:"diff"`
    Type    string  `json:"type"`
    Stacked bool    `json:"stacked"`
    Scale   float64 `json:"scale"`
}

GraphsMetrics の詳細にも触れておきましょう。

Graphs

  • Label

f:id:tkuchiki:20151213152248p:plain

赤枠で囲った memory の部分の設定です。

  • Unit

float, integer, percentage, bytes, bytes/sec, iops のいずれかを指定します。

Metrics

  • Name

カスタムメトリック名です。
ワイルドカードとして、*, # を使うことができます。

  • Label

f:id:tkuchiki:20151213152408p:plain

赤枠で囲った used, buffers などの設定です。
API仕様(v0)/api/v0/graph-defs/create に書いてありますが、

%1、%2.. のようにしてメトリック名の1つ目、2つ目.. のワイルドカードにマッチした部分を利用することができます。

とあるように、%N を指定することもできます。

  • Diff

グラフを差分値で出力する設定です(デフォルト false)。

  • Type

データの型です。
float64, uint32, uint64 のいずれかを指定します(デフォルト float64)。

  • Stacked

積み重ねグラフにする設定です(デフォルト false = 線グラフ)。

  • Scale

文字通り目盛りで、Mackerel に送る値を調整してくれます。
例えば、free -k の実行結果を parse して、Mackerel に送る plugin があったとします。
free -k の実行結果は単位が KB なので、Bytes にする場合 1024 を乗算する必要があります。
Scale を 1024 とした場合、1024 を乗算して Mackerel にデータを送ってくれます。

Tempfile

if *optTempfile != "" {
    helper.Tempfile = *optTempfile
} else {
    helper.Tempfile = fmt.Sprintf("/tmp/mackerel-plugin-rack-stats")
}

差分値の出力に使います。
Tempfile は差分値の出力以外では使っていなさそうでした。
mp := MackerelPlugin{plugin, "/tmp/mackerel-plugin-default"} となっているので、
Tempfileの設定をしないと/tmp/mackerel-plugin-defaultに実行結果のJSONが出力されます。

plugin 実行

helper.Run()

でグラフの設定とメトリック収集処理を実行します。
他のプラグインを見ると、ほとんどが以下のような実装になっています。

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

しかし、MackerelPlugin.Run() は内部で同じことを行っているため、
Run() を使うのが良いでしょう。
なぜ、2015-12-16 現在、ほとんどのプラグインで使われていないかのかというと、
Run() が実装されたのが da2aa407cda88bca4a1159f32bfdcee078ad20fc のコミットで、
2015-12-03 に実装されたばかりだからですね。

mackerel-agent が、環境変数 MACKEREL_AGENT_PLUGIN_META をつけて実行した場合は、
OutputDefinitions() が実行され、グラフ定義の json が出力されます。
この時、GraphDefinition() が実行されます。
環境変数がついていない場合は、OutputValues() が実行され、
メトリックが出力されます。
こちらは、FetchMetrics()GraphDefinition() の両方が実行されます。

FetchMetrics() (map[string]interface{}, error)

メトリックの収集処理です。
mackerel-plugin-rack-stats では、 GET /_raindrops のレスポンスを parse して map[string]interface{} を生成します。
細かい処理は省略しますが、map[string]interface{} は以下のようになっています。

map[string]interface{
    "active": N.nnnnnn,
    "queued": N.nnnnnn,
    "calling": N.nnnnnn,
    "writing": N.nnnnnn,
}

このとき、map[string]interface{} のキーが、
GraphDefinition()map[string](mp.Graphs)Metrics.Name
対応している必要があります(この例では、active, queued, calling, writing)。

GraphDefinition() map[string]Graphs

func (u RackStatsPlugin) GraphDefinition() map[string](mp.Graphs) {
        scheme, path, err := parseAddress(u.Address)
        if err != nil {
                log.Fatal(err)
        }

        var label string
        if u.MetricKey == "" {
                switch scheme {
                case "http":
                        _, port, _ := net.SplitHostPort(path)
                        u.MetricKey = port
                        label = fmt.Sprintf("Rack Port %s Stats", port)
                case "unix":
                        u.MetricKey = strings.Replace(strings.Replace(path, "/", "_", -1), ".", "_", -1)
                        label = fmt.Sprintf("Rack %s Stats", path)
                }
        } else {
                label = fmt.Sprintf("Rack %s Stats", u.MetricKey)
        }

        return map[string](mp.Graphs){
                fmt.Sprintf("%s.rack.stats", u.MetricKey): mp.Graphs{
                        Label: label,
                        Unit:  "integer",
                        Metrics: [](mp.Metrics){
                                mp.Metrics{Name: "queued", Label: "Queued", Diff: false},
                                mp.Metrics{Name: "active", Label: "Active", Diff: false},
                                mp.Metrics{Name: "writing", Label: "Writing", Diff: false},
                                mp.Metrics{Name: "calling", Label: "Calling", Diff: false},
                        },
                },
        }
}

1 ホストに複数設定できるように、map[string](mp.Graphs) のキーを変更できるようになっているため処理が複雑になっていますが、
スルーして return している map[string](mp.Graphs) に注目してください。
わかりやすくするために、fmt.Sprintf で生成しているキーや label を展開した状態の map[string](mp.Graphs) を以下に示します。

map[string](mp.Graphs){
    "8080.rack.stats": mp.Graphs{
        Label: "Rack Port 8080 Stats",
        Unit:  "integer",
        Metrics: [](mp.Metrics){
            mp.Metrics{Name: "active", Label: "Active", Diff: false},
            mp.Metrics{Name: "queued", Label: "Queued", Diff: false},
            mp.Metrics{Name: "calling", Label: "Calling", Diff: false},
            mp.Metrics{Name: "writing", Label: "Writing", Diff: false},
        },
    },
}

前述のとおり、Metrics.Name の値が、
FetchMetrics() が返す map[string]interface{} のキーと
対応している必要があります(この例では、active, queued, calling, writing)。

Metrics.Nameワイルドカードを使う場合

Metrics.Nameワイルドカードとして、*# を使うことができます。
ワイルドカードを使う場合、GraphDefinition()map[string](mp.Graphs) のキーと
FetchMetrics() が返す map[string]interface{}のキーを
連結したものにしなくてはなりません。
map[string](mp.Graphs) のキーが 8080.rack.stats だった場合、
map[string]interface{}

// FetchMetrics()
map[string]interface{
    "8080.rack.stats.active": N.nnnnnn,
    "8080.rack.stats.queued": N.nnnnnn,
    "8080.rack.stats.calling": N.nnnnnn,
    "8080.rack.stats.writing": N.nnnnnn,
}

となります。
ワイルドカードを使うと、GraphDefinition()map[string](mp.Graphs)
以下のように書き換えることができます。

// GraphDefinition()
return map[string](mp.Graphs){
    "8080.rack.stats" : mp.Graphs{
        Label: "Rack Port 8080 Stats",
        Unit:  "integer",
        Metrics: [](mp.Metrics){
            mp.Metrics{Name: "*", Label: "%1", Diff: false},
        },
    },
}

前述したとおり、ワイルドカードにマッチした部分を Label でつかうことができます。

map[string](mp.Graphs) のキー にワイルドカードを使う場合

ワイルドカードは、map[string](mp.Graphs) のキーにも使うことができます。
こちらは、グラフの凡例をグループ化する場合に使います。
mackerel-plugin-rack-stats には当てはまりませんが、
FetchMetrics() が以下のようなデータを返すとし、

// FetchMetrics()
map[string]interface{
    "rack.foo.stats.active": 10.000000,
    "rack.foo.stats.queued": 10.000000,
    "rack.foo.stats.calling": 10.000000,
    "rack.foo.stats.writing": 10.000000,
    "rack.hoge.stats.active": 5.000000,
    "rack.hoge.stats.queued": 5.000000,
    "rack.hoge.stats.calling": 5.000000,
    "rack.hoge.stats.writing": 5.000000,
}

GraphDefinition()map[string](mp.Graphs) が以下のような実装になっているとします。

// GraphDefinition()
map[string](mp.Graphs){
    "rack.#.stats": mp.Graphs{
        Label: "Rack test Stats",
        Unit:  "integer",
        Metrics: [](mp.Metrics){
            mp.Metrics{Name: "active", Label: "Active", Diff: false},
            mp.Metrics{Name: "queued", Label: "Queued", Diff: false},
            mp.Metrics{Name: "calling", Label: "Calling", Diff: false},
            mp.Metrics{Name: "writing", Label: "Writing", Diff: false},
        },
    },
}

その場合、グラフは

f:id:tkuchiki:20151213210005p:plain

となります。
foohoge でグループ化されていますね。

API仕様にも書いてありますが、

またグラフ定義のメトリック名にはワイルドカード(* または #)を 2つのドット(.)の間、または最後のドット(.)の後ろに単独で使用することができます。

とあるので、#.rack.stats のようには定義できないようです。
ちなみに、#* の使い分けについては、

ワイルドカード # を使った場合はメトリック名の# にマッチした部分でグラフの凡例がグループ化されます。

とありますし、色々なプラグインを見たところ、map[string](mp.Graphs) のキーは #Metrics.Name では * が使われていましたので、慣例に従うと良いと思います。

またワイルドカード # は一つまでしか使えません。メトリック名全体は ^custom(.[-a-zA-Z0-9_]+|[*#])+ のようになります。

という制約もありますのでご注意ください。

以上が mackerel agent plugin の作り方でした。
長々と説明しましたが、

  • 自作の Plugin 構造体
  • Plugin 構造体に FetchMetrics() (map[string]interface{}, error)
  • Plugin 構造体に GraphDefinition() map[string]Graphs

を実装するだけですのでお手軽ですね!

mackerel-plugin-rack-stats の使い方

mackerel-plugin-rack-stats の説明があまりできていなかったので軽く紹介します。

-address で指定した Port または Unix Domain Socket に、
-path で指定した(デフォルト /_raindrops) に GET リクエストを送ります。
1ホストに Rack server が複数起動している場合にも対応できるように、
-metric-key を指定すると、別のメトリックとして登録することができます。
-metric-key を指定しない場合は、
PORT.rack.stats_path_to_unicorn_sock.rack.stats のようなメトリック名になります。 config の例は以下のとおりです。

[plugin.metrics.rack_stats]
command = "/path/to/mackerel-plugin-rack-stats -address=unix:/path/to/unicorn.sock"
[plugin.metrics.rack_stats]
command = "/path/to/mackerel-plugin-rack-stats -address=http://localhost:8080"

他にも Rack の stats を収集する gem がありましたら教えて下さい...

まとめ

mackerel-plugin-rack-stats を例に mackerel agent plugin の作り方を紹介しました。
go-mackerel-plugin-helper を使うと簡単ですので、皆さんも実装してみてください!

17日目の担当は、@shiozaki さんです!