tkuchikiの日記

新ブログ https://blog.tkuchiki.net

プロセスのメモリ使用量とCPU使用率などが取れる mackerel-plugin-linux-proc-stats をリリースしました

プロセスのメモリ使用量と CPU 使用率などのメトリクスが取れる、
mackerel-plugin-linux-proc-stats をリリースしました。

Installation

https://github.com/tkuchiki/mackerel-plugin-linux-proc-stats/releases
からバイナリをダウンロードして解凍してください。

Usage

$ ./mackerel-plugin-linux-proc-stats --help
Usage of ./mackerel-plugin-linux-proc-stats:
  -follow-child-processes
        Follow child processes
  -metric-key-prefix string
        Metric key prefix
  -pid string
        PID
  -pidfile string
        PID file
  -tempfile string
        Temp file name
  -version
        Version

このプラグイン独自のオプションを説明すると、

  • pid : PID を指定
  • pidfile: PIDファイルの path
  • follow-child-processes: 子プロセスまで集計するか

となっています。
follow-child-processes について説明するために実行例を示します。
nginx の master と worker プロセスが各1つずつ起動しているとします。
その状態で master プロセスの情報だけ取得する場合は、以下のように実行します。

$ ./mackerel-plugin-linux-proc-stats -pidfile /var/run/nginx.pid
nginx_process.cpu.usage 0.000000        1458808477
nginx_process.memory.vsize      107450368.000000        1458808477
nginx_process.memory.rss        3399680.000000  1458808477
nginx_process.num.running       0.000000        1458808477
nginx_process.num.processes     1.000000        1458808477
nginx_process.num.threads       1.000000        1458808477

master と worker プロセス両方の情報を合算して取得する場合は、以下のように実行します。

$ ./mackerel-plugin-linux-proc-stats -pidfile /var/run/nginx.pid -follow-child-processes
nginx_process.num.running       0.000000        1458808496
nginx_process.num.processes     2.000000        1458808496
nginx_process.num.threads       2.000000        1458808496
nginx_process.cpu.usage 0.000095        1458808496
nginx_process.memory.vsize      215269376.000000        1458808496
nginx_process.memory.rss        7966720.000000  1458808496

子プロセスまでしか追わないので、孫プロセスは対象外です(実装するか検討中)。

メトリクスについて

このプラグインは、/proc/PID/stat を parse しています。

  • num.running: 3番目が R かどうか
  • num.processes: 指定したプロセスのプロセス数(follow-child-processes をつけた場合は子プロセス含む)
  • num.threads: 20番目の値
  • cpu.usage: 15, 16 , 17, 22番目の値 とプロセスの起動時間、uptimegetconf CLK_TCK の値を使って計算
  • memory.vsize: 23番目の値
  • memory.rss: 24番目の値

num.running については、実行時間が長い処理を行わない限りは R にならないので、0 の場合がほとんどです。

/proc/PID/stat の例を載せておきます。

10440 (nginx) S 1 10440 10440 0 -1 4202816 71 0 0 0 0 0 0 0 20 0 1 0 364149088 107450368 830 18446744073709551615 1 1 0 0 0 0 0 1073746048 402745863 18446744073709551615 0 0 17 0 0 0 0 0 0 0 0 0 0 0 0 0 0

IAM の Switch Role を捗らせる

AWS のクロスアカウントアクセスを利用すると、
IAM ユーザを AWS アカウントごとに作成しなくても良くなり管理が容易になるのですが、
Switch Role する アカウントが多いと、履歴が 5 件しかないのでアカウントとロールを再入力しなくてはならないことが増えてきます。

ロールを切り替えるユーザーアクセス権限の付与 を見ると、

https://signin.aws.amazon.com/switchrole?account=YourAccountIDorAliasHere&roleName=pathIfAny/YourRoleNameHere

とあります。
Alias は以下を例にすると Your_AWS_Account_ID です(参考: AWS アカウント ID とその別名)。

https://Your_AWS_Account_ID.signin.aws.amazon.com/console/

https://signin.aws.amazon.com/switchrole?YourAccount=hogehoge&roleName=YourRole にアクセスすると、
アカウントとロールが入力された状態でアクセスすることができます。

f:id:tkuchiki:20160222121321p:plain

また、ロールの切り替え(AWS マネジメントコンソール) を見ると、

https://signin.aws.amazon.com/switchrole?account=account_id_number&roleName=role_name&displayName=text_to_display

とあり、 displayName を指定すると表示名も入力した状態にできます。
また、color=カラーコード で色を指定することもできます。
指定できる色は左から順に、

  • F2B0A9 (何も指定しなくてもこれになります)
  • FBBF93
  • FAD791
  • B7CA9D
  • 99BCE3

です(右端の黒は指定方法がわかりませんでした)。
color についてはドキュメントに書いていないようでしたので、 変更される可能性があります。

最後に、account, roleName, displayName, color を全部指定した場合の例を示します。

https://signin.aws.amazon.com/switchrole?account=YourAccount&roleName=YourRole&displayName=YourDisplayName&color=99BCE3 にアクセスすると、

f:id:tkuchiki:20160222121332p:plain

すべて入力済みになります。
この URL をブックマークしておくと、各項目を入力しなおさなくても良くなるので便利です。

まとめ

Switch Role の URL の query string に、

  • account = alias または account ID
  • roleName = ロール名
  • displayName = 表示名
  • color = カラーコード

を指定すると、各項目を入力済みの状態にできるので便利です。

setusergroups の RPM spec ファイル

setusergroups については、作者の tokuhirom さんのブログを御覧ください。

supplementary groups をサポートする setuidgid であるところの setusergroups.c 書いた

supplementary groups をサポートした setuidgid が欲しいケースがある。

これを相談されたので RPM の spec ファイルを書きました。
以下を ~/rpmbuild/SPECS/setusergroups.spec として保存して、

curl -L https://github.com/tokuhirom/setusergroups/archive/master.zip > ~/rpmbuild/SOURCES/master.zip
rpmbuild -ba ~/rpmbuild/SPECS/setusergroups.spec

RPM を作成できます。
autoconf のバージョンが 2.68 以上でないと build できないので、
CentOS 6 で build する場合は base repo 以外のものを使う必要があります。

norikra-listener-zabbix 0.2.0 をリリースしました

norikra-listener-zabbix (rubygems) 0.2.0 をリリースしました。

変更点

group の記述を変更

port を廃止し、ZABBIX_SERVER:[PORT] と記述するようになりました。
-- group ZABBIX(localhost:10051, zabbix host) のように記述します。

IPv6 サポート

IPv6 に対応しました。
-- group ZABBIX([::1]:10051, zabbix host) のように、[ ]IPアドレスをくくらないとエラーなります。

groupprefix_item_key を省略可能に

prefix の指定が必須ではなくなりました。
指定しない場合は、列名がそのままアイテムキーとして使用されます。
列の別名に . を使いたい場合、$ を指定すると . に置き換えてアイテムキーとして使用します。 なぜこのような仕様になっているかというと、

Identifiers cannot contain the "." (dot) character, i.e. "vol.price" is not a valid identifier for the rename syntax.

とあるように、. が使えないので、Zabbix のアイテムキーとして使えない文字列かつ列の別名に使える記号として $ を採用しています(あまり美しくないですが...)。

例は以下のとおりです。

SELECT sum(foo) AS `bar$foo$sum`, avg(foo) AS `bar$foo$avg` FROM test_table.win:time_batch(1 min)

参考

Norikra の集計結果を直接 Zabbix に送る norikra-listener-zabbix をリリースしました

追記(2016/02/08 15:30):0.2.0 で group の記述方式を変更しましたので、
norikra-listener-zabbix 0.2.0 をリリースしました をご確認ください。

Norikra から直接 Zabbix にデータを送る norikra-listener-zabbix (rubygems) をリリースしました。

Zabbix へデータを送信するコードのほとんどは、
fujiwara/fluent-plugin-zabbix を使わせていただきました。ありがとうございます。

Installation

gem install norikra-listener-zabbix

Norikra をインストールした gem で gem install すれば自動で有効になります。

Usage

Group を以下のように設定します。

ZABBIX(zabbix_server, zabbix_host, preifx_item_key, [port=10051])
  • zabbix_server: localhost
  • zabbix_host: test server
  • prefix_item_key: nginx

の場合は、以下のとおりです。

ZABBIX(localhost, test server, nginx)

prefix_item_key については、アイテムキーの prefix です。
port は Zabbix Server デフォルトの 10051 を使っている場合は省略できます。

利用例

秒間のリクエスト数を HTTP ステータスコードごとに集計する場合を考えます。

SQL

ステータスコードごとに COUNT して秒間のデータにします。

SELECT
  COUNT(1, 200 <= status AND status <= 299) / 60 AS rate_2xx,
  COUNT(1, 300 <= status AND status <= 399) / 60 AS rate_3xx,
  COUNT(1, 400 <= status AND status <= 499) / 60 AS rate_4xx,
  COUNT(1, 500 <= status AND status <= 599) / 60 AS rate_5xx
FROM nginx_status.win:time_batch(1 min)

Zabbix

アイテム

prefix_item_key とSQL の列名(rate_2xx など) を . で連結したアイテムを作成します。 prefix_item_key は nginx、列名は rate_2xx, rate_3xx, rate_4xx, rate_5xx です。

作成するアイテムは以下のようになります。

  • タイプ: Zabbix トラッパー
  • キー: nginx.rate_2xx, nginx.rate_3xx, nginx.rate_4xx, nginx.rate_5xx
  • データ型: 数値 (浮動小数点)

1 つの SQL で複数のアイテムにデータを送信することができます。

グラフ

グラフは適宜作成してください。

f:id:tkuchiki:20160205011732p:plain

2xx のデータしかないのであまり良くない例ですが、
それぞれのステータスコードのリクエストが送られてくれば集計されます。

その他

Zabbix 周りのコードはほとんど fujiwara/fluent-plugin-zabbix を使わせていただきましたが、
一部変更した部分があります。
また、実装する上でハマったところがあったのでそのことに触れておきます。

Zabbix 周りの改良点

Zabbix Server にデータを送信すると、

{
  "response" => "success",
  "info" => "Processed 2 Failed 0 Total 2 Seconds spent 0.000103"
}

のようなレスポンスが返って来ます。
このとき、info の Failed が 1 以上だったり、Processed が 0 で全て Failed というケースでも response は success になります。
Processed が 0 でなければ 1 つ以上データが送れていることになるので良いのですが、
全て Failed した場合は失敗にしたほうがわかり易いと思い、 warn でログを出すようにしています
(実装している時、ログに何も出ないのにデータが送れていなくて理解するのに時間がかかりました)。
全て Failed になるケースは、以下の 2 つを確認しています。

  • Zabbix ホスト名が間違っている
  • アイテムキーが間違っている

split(",") だけだと空白が含まれる

Norikra::Listener::Zabbix.initializeargument, で split するときに、
ZABBIX(zabbix_server, zabbix_host, preifx_item_key)(わかりにくいですが , の後ろにスペースがあります)と書いて、
zabbix host とアイテムキーにスペースが含まれて正常に動作しない問題で1時間くらい嵌まりました...
そこで、スペースを気にしなくても良いように split したあとに strip しています。

def self.parse_argument(args)
  args.split(",").map(&:strip)
end

Listener Plugin を書く方は、この処理をしておくと変なところで嵌らなくて良いのでおすすめです。

まとめ

Norikra Listener Plugin の norikra-listener-zabbix をリリースしました。
是非お使いください!

Consul Multiple Datacenters 検証結果 +α

この記事は、HashiCorp Advent Calendar 2015 17日目の記事です。

検証環境の Consul は、

$ consul --version
Consul v0.6.0
Consul Protocol: 3 (Understands back to: 1)

です。
検証環境は v0.6.0 ですが、v0.6.0 に依存した話しはほとんどないと思います。

筆者は、Consul を Multiple Datacenters(以降、Multi-DC) で運用しております。
Multi-DC で Consul を導入する際に検証した結果を記します。
トピックは以下のとおりです。

  • WAN
  • DC をまたいで DNS を引く
  • DC をまたいで KV のデータを取得する
  • DC をまたいで KV にデータを入れる
  • Consul Event は複数の DC に一斉に送れない
  • DNS forward に Unbound を使う
  • External Services

準備

Consul のバイナリを入手します。

$ curl -LO https://releases.hashicorp.com/consul/0.6.0/consul_0.6.0_linux_amd64.zip
$ unzip consul_0.6.0_linux_amd64.zip

実験のために何台もサーバを立てるのは大変なので、
検証用に 1 台のサーバで consul を複数起動する - tkuchikiの日記 のような手順で、
1 台のサーバに Server x 3, Client x 1 のクラスタを 2セット作ります。

dc01

$ ./consul members
Node           Address         Status  Type    Build  Protocol  DC
consul01-dc01  10.0.2.15:8301  alive   server  0.6.0  2         dc01
consul02-dc01  10.0.2.15:8311  alive   server  0.6.0  2         dc01
consul03-dc01  10.0.2.15:8321  alive   server  0.6.0  2         dc01
consul04-dc01  10.0.2.15:8331  alive   client  0.6.0  2         dc01
$ curl http://127.0.0.1:8500/v1/status/leader
"10.0.2.15:8300"

dc02

$ ./consul members -rpc-addr=127.0.0.1:18400
Node           Address          Status  Type    Build  Protocol  DC
consul01-dc02  10.0.2.15:18301  alive   server  0.6.0  2         dc02
consul02-dc02  10.0.2.15:18311  alive   server  0.6.0  2         dc02
consul03-dc02  10.0.2.15:18321  alive   server  0.6.0  2         dc02
consul04-dc02  10.0.2.15:18331  alive   client  0.6.0  2         dc02
$ curl http://127.0.0.1:18500/v1/status/leader
"10.0.2.15:18300"

WAN

WAN は、すべての DC を含めた Consul クラスタ全体のことです。
consul members -wan は Multi-DC の Consul Server のみを返します。

$ ./consul members -wan
Node                Address          Status  Type    Build  Protocol  DC
consul01-dc01.dc01  10.0.2.15:8302   alive   server  0.6.0  2         dc01
consul01-dc02.dc02  10.0.2.15:18302  alive   server  0.6.0  2         dc02
consul02-dc01.dc01  10.0.2.15:8312   alive   server  0.6.0  2         dc01
consul02-dc02.dc02  10.0.2.15:18312  alive   server  0.6.0  2         dc02
consul03-dc01.dc01  10.0.2.15:8322   alive   server  0.6.0  2         dc01
consul03-dc02.dc02  10.0.2.15:18322  alive   server  0.6.0  2         dc02

client に向けて consul members -wan を実行すると、
何も出力せず、status code 2 を返します。

$ ./consul members -rpc-addr=127.0.0.1:8430 -wan
$ echo $?
2

DC をまたいで DNS を引く

dc01 の Consul に対して、dc02 の node を問い合わせます。

$ dig @127.0.0.1 -p 8630 consul04-dc02.node.dc02.consul A

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.37.rc1.el6_7.2 <<>> @127.0.0.1 -p 8630 consul04-dc02.node.dc02.consul A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19552
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;consul04-dc02.node.dc02.consul.        IN      A

;; ANSWER SECTION:
consul04-dc02.node.dc02.consul. 60 IN   A       10.0.2.15

;; Query time: 1 msec
;; SERVER: 127.0.0.1#8630(127.0.0.1)
;; WHEN: Tue Dec 15 17:58:09 2015
;; MSG SIZE  rcvd: 94

A レコードが返ってきます。
DNS Interface - Consul by HashiCorp に書いてありますが、

<node>.node[.datacenter].<domain> なので、
同一 DC にいる場合は、 [.datacenter] を省略することができます。

$ dig @127.0.0.1 -p 8630 consul04-dc01.node.consul A

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.37.rc1.el6_7.2 <<>> @127.0.0.1 -p 8630 consul04-dc01.node.consul A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46271
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;consul04-dc01.node.consul.     IN      A

;; ANSWER SECTION:
consul04-dc01.node.consul. 60   IN      A       10.0.2.15

;; Query time: 0 msec
;; SERVER: 127.0.0.1#8630(127.0.0.1)
;; WHEN: Tue Dec 15 18:05:00 2015
;; MSG SIZE  rcvd: 84

DC をまたいで KV のデータを取得する

まず、通常の KV にデータを PUT して取り出す場合です。
dc01 に PUT します。

$ curl -X PUT -d "dc01-test" http://127.0.0.1:8500/v1/kv/dc01
true
$ curl http://127.0.0.1:8500/v1/kv/dc01?raw
dc01-test
$ curl http://127.0.0.1:8510/v1/kv/dc01?raw
dc01-test
$ curl http://127.0.0.1:8520/v1/kv/dc01?raw
dc01-test
$ curl http://127.0.0.1:8530/v1/kv/dc01?raw
dc01-test

dc01 内で KV のデータが共有されていますね。
この状態で、dc02 の KV を参照すると、

$ curl http://127.0.0.1:18500/v1/kv/dc01?raw

データが入っていませんね。
これは、仕様なのでおかしい挙動ではありません。

追記: 2016-08-23 19:30

KV をまたいでデータを取得するためには、以下のように dc= をつければ OK です。訂正致します。

curl -s 'data' http://127.0.0.1:18500/v1/kv/data?dc=dc02&raw"

ちなみに、consul-replicate というものがあるので、
これを使えば KV のデータをレプリケーションすることができます。
しかし、

  • consul-replicate をどのサーバで動かすか
  • consul-replicate の HA構成はどうするか
  • consul-replicate のレプリケーションが一方向なので、DC 間での同期はどうするのか

といった問題があるので、使いどころが難しそうです。

DC をまたいで KV にデータを入れる

追記: 2016-08-23 19:30

curl -s -X PUT -d 'data' http://127.0.0.1:18500/v1/kv/data?dc=dc02"

のように、dc= をつければ datacenter をまたいで KV にデータをいれることができました。
間違ってた情報を書いていたので、そちらは削除します。

Consul Event は複数の DC に一斉に送れない

consul event には、-datacenter というオプションがありますが、
指定できる DC は一つだけです。
複数の DC に Event を送りたい場合は、DC ごとに実行するようです。

Unbound で DNS forward する

Dnsmasq で Consul DNS に forward して内部 DNS に使っている方が多いと思いますが、
Unbound でも forward できます。

# unbound.conf

server:
    access-control: 127.0.0.0/8 allow
    do-not-query-localhost: no

forward-zone:
    name: "consul"
    forward-addr: 127.0.0.1@8600

Unbound の設定に明るくなくて、
デフォルトの設定に上記設定を追加したらうまく forward できませんでした...
設定が最低限すぎるので、適宜設定を追加してお使いください。

External Services

Consul Service は、Consul クラスタ内のサービスを監視しますが、
External Services を使うと、外部のサービスを監視することができます。

$ curl -X PUT -d '{"Datacenter": "dc01", "Node": "google", "Address": "www.google.com", "Service": {"Service": "search", "Port": 80}}' http://127.0.0.1:8500/v1/catalog/register
true

External Services を使うためには、recursor または recursorsDNS サーバを指定しなくてはなりません。
consul01-dc01(port 8600) は recursor を設定せず、
consul02-dc01(port 8610) は設定しているとします。
その場合、以下の様な実行結果になります。

$ dig @127.0.0.1 -p 8600 search.service.consul

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.37.rc1.el6_7.2 <<>> @127.0.0.1 -p 8600 search.service.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 56306
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;search.service.consul.         IN      A

;; ANSWER SECTION:
search.service.consul.  15      IN      CNAME   www.google.com.

;; Query time: 0 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: Tue Dec 15 19:41:34 2015
;; MSG SIZE  rcvd: 88
$ dig @127.0.0.1 -p 8610 search.service.consul

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.37.rc1.el6_7.2 <<>> @127.0.0.1 -p 8610 search.service.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26004
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;search.service.consul.         IN      A

;; ANSWER SECTION:
search.service.consul.  15      IN      CNAME   www.google.com.
www.google.com.         149     IN      A       216.58.220.196

;; Query time: 11 msec
;; SERVER: 127.0.0.1#8610(127.0.0.1)
;; WHEN: Tue Dec 15 19:42:01 2015
;; MSG SIZE  rcvd: 118

recursor を設定した consul02-dc01 は、A レコードが返ってきます。
recursor の反映は consul reload ではできませんので、再起動する必要があります。

※追記
recursor を設定していない状態からの反映は consul reload ではできませんが、
recursor を設定している状態からは consul reload で変更を反映することができます。

まとめ

Consul の Multi-DC について検証した結果 +α を紹介しました。
使った設定ファイルは https://gist.github.com/tkuchiki/c4ea89df94c8e2a51b83 に置いてあります。

明日は、Multiple DNS recursors の対応ありがとうございます!(http://fstn.hateblo.jp/entry/2015/02/21/024400)
な、@foostan さんです!

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 さんです!