mackerel-pluginの作り方

遅くなってごめんなさい。

こんにちはこんにちは！
みなさん元気に監視してますか？

Mackerelはご存知の通り（？）監視の手間を減らしてくれる大変便利なサービスなのですが、
今回はその機能のひとつであり、ぼくのお気に入りの機能である、plugin機能とその作り方を紹介しようかと思います。
Mackerelはマネージドサービスなのですが、それのpluginって作れるの？と思う方もいらっしゃるかもしれませんので今回はこれを紹介することにしました。

Mackerelそのものについて詳しく知りたい方はMackerelのWebサイトをご覧になってもらったほうがよいでしょう。
https://mackerel.io/ja/

本記事ではMackerelそのものについては、簡単な解説と自分がなぜMackerelに魅了されたかを語るにとどめます。

## Mackerelの構成

Mackerelは各マシンのメトリクスと稼働状況を収集してそれを可視化するサービスと捉えることができるでしょう。
Mackerelは所謂push型の構成になっており、各マシンのメトリクスと稼働状況を収集してそれを送信する `mackerel-agent`（各マシンにインストール・自前でホスト） と、情報をホストして可視化するMackerelサーバー（マネージド・はてながホスト）に分けられます。

### mackerel-agent

この `mackerel-agent` はOSSとして開発されており、[Github](https://github.com/mackerelio/mackerel-agent)にソースが公開されています。
これまでにもユーザーのPull Requestによって追加された機能がありますし、当然ながらリポジトリをForkして独自patchを当ててコンパイルすることで、自分だけの `mackerel-agent` を作ることもできます。

ソースが公開されていることのメリットは極めて大きく、たとえば、問題が起きたときのトラブルシューティングもソースが公開されているのでデバッグコードを簡単に仕込むことが可能です。当然ながら監視に不要な情報が送られていないかもソースを読めば検証することができます。（おそらく利便性のために）各OSの公式パッケージリポジトリも用意されていますが、それすら使わずに自前でコンパイルすることもできるので、危険なコードが入っていないこと検証済のソースからコンパイルしたバイナリで運用することもできます。

また、プラグイン機能を備えており、独自メトリクスの収集や、独自の死活監視を追加することも容易になっています。
このため、 `mackerel-agent` は大変自由度が高いエージェントになっています。

### Mackerelサーバー

Mackerelサーバーの設定も式によるカスタムグラフ定義をサポートしたり、WebHookがあったり、当然ながら既成のIntegrationがたくさんあったりと、自由度が大変高いといえるでしょう。
ぼくは今回はpluginの話に注力するので割愛します。きっと誰かがIntegrationやWebHookなどについて書いてくれるでしょう。 :)

## mackerel-agent のプラグイン機能

mackerel-agentのプラグイン機能の詳しい使い方は公式ドキュメント([メトリクスプラグイン](https://mackerel.io/ja/docs/entry/howto/mackerel-agent-plugins)/[チェック監視プラグイン](https://mackerel.io/ja/docs/entry/custom-checks))をご覧になったほうがよいでしょう。
基本的には[sensu](https://sensuapp.org/)互換の仕様になっていると理解してもらって良いかと思います。

ここでは簡単にプラグイン機能の勘所を説明したいと思います。

mackerel-agentにおけるプラグイン機能は設定ファイルに指定したコマンドを一定時間毎にshellを介して起動し、その結果を標準出力とプロセス終了コードから情報を得るという非常にシンプルな構成になっています。
それが故に、各プラグインは基本的に副作用を持ちません。つまり、プラグインとなるコマンドを手で実行することで簡単に動作確認ができるのです。

また、当然ながらshellを介しているので **ワンライナーが書けます**。書けるんです！
ちなみに、ワンライナーを設定ファイルに入れると見栄えや可読性が悪化しますが、workaroundには大変有用といえます。即ち、実用的な仕様といえます。
以前のMackerel Meetupで[KOWAZA for Mackerel](http://www.slideshare.net/karupanerura/kowaza-for-mackerel)という発表をした資料にその概要が書かれていますので興味があればご覧になってみてください。

また、公式プラグインもOSSとしてGithubに公開されており、各OSの公式パッケージリポジトリも提供されています。

https://github.com/mackerelio/mackerel-agent-plugins
https://github.com/mackerelio/go-check-plugins

これらはかなり充実しているため、実際には独自のプラグインを作って運用しなければならない場面は減りつつあります。

## mackerel-pluginの作り方

さて、やっと本題に入りましたが、ここまでくると語ることがほぼありません。
なんたって、仕様さえ満たせばどんな言語でだってプラグインが書けるのですから！

たとえば100までのランダムな数値を描画するメトリクスプラグインをPerlで書くとこんなかんじになります。

```perl
#!/usr/bin/perl
use strict;
use warnings;
use feature qw/say/;
use JSON::PP qw(encode_json);

if ($ENV{MACKEREL_AGENT_PLUGIN_META}) {
    say "# mackerel-agent-plugin";
    say encode_json {
        graphs => {
            'mymetric.mygrapth' => {
                label    => 'My Graph',
                unit     => 'integer',
                metrics  => [
                    {
                        name  => 'random',
                        label => 'Random Number',
                    },
                    {
                        name  => 'random2',
                        label => 'Random Number 2',
                    },
                ]
            }
        }
    };
}
else {
    my $time = time();
    say join "\t", 'mymetric.mygrapth.random',  int rand 100, $time;
    say join "\t", 'mymetric.mygrapth.random2', int rand 100, $time;
}
```

メトリクスプラグインは、少し複雑に見えるかもしれませんが、分かってしまえばとても簡単です。

mackerel-agentは最初にMACKEREL_AGENT_PLUGIN_METAという環境変数を1にセットしてプラグインを実行します。
この場合はこのプラグインで可視化するメトリクスをグラフにプロットするための定義情報を表現するJSONを要求しています。
グラフにどのような種類の値(name)を、どのような名前(label)を付けて、どのような単位(unit)として表現したいのかを求められているので、
それらを表現するJSONを標準出力に出力しましょう。

メトリクスグラフの定義情報を収集してmackerel-agentが完全に起動すると、通常のメトリクスと同様に1分毎にMACKEREL_AGENT_PLUGIN_METAが1ではない状態（セットされていない状態）でプラグインを実行します。
この場合は、グラフに描画したいメトリクスの値とそれを収集した時刻(epoch)を求められています。
メトリクスの値とは、たとえばミドルウェアへの接続数であればその数値、エラーの割合であればそのパーセンテージなどです。
名前は、MACKEREL_AGENT_PLUGIN_METAで指定した名前をフルネームで指定します。
このサンプルコードでは `mymetric.mygrapth` というグラフの `random` という名前で収集するので `mymetric.mygrapth.random` になります。それに値、epoch時刻と続けて出力します。

チェック監視プラグインはもっと簡単です。
この例では1/3の確率でCRITICALアラートが発生します。

```bash
#!/bin/bash
set -ue

exit_code=`expr $RANDOM % 3`

case $exit_code; in
  0) echo "OK: Random $exit_code"
  1) echo "WARNING: Random $exit_code"
  2) echo "CRITICAL: Random $exit_code"
esac

exit $exit_code
```

ちなみに、チェック監視スクリプトの標準出力はWebUI上で確認できるほか、各種インテグレーションによって送出されるメッセージ上でも確認できます。
どのような事由によってチェック監視がFAILしたかを出力しておくと迅速に問題の原因を把握することができます。

しかし、違う、違いますね。
我々は単にグラフを描画したいわけでも、単にアラートを上げたいわけでもなくて、監視がしたいわけです。

そのためにはミドルウェアに接続してステータス確認用のコマンドを実行したり、ファイルシステムを操作したり、他のサーバーと通信したりする必要があるわけです。
つまり、実用的なプラグインを実現するためには多くの場合、より複雑な処理を行う必要があります。

そのためには、一般的にはライブラリに頼ったほうがシンプルで保守性の高いコードが書けます。当然、ライブラリを活用したいと思うでしょう。
しかし、LLの場合は実行環境に依存ライブラリをインストールしておく必要があり、mackerel-agentのプラグインような全てのサーバーで実行するような類のものでは、全てのサーバーにライブラリをインストールし管理する必要があり、管理上の懸念となる場合もあると思います。
Javaなどで書いてfatpackしたjarを実行するのもひとつの手段ですが、都度プロセスを起動するmackerel-agentとは相性が良いとは思えません。
もちろん、外部ライブラリに依存しない、数十行程度のLLやシェルスクリプトなどでシンプルに監視を実現できるに越したことはないわけなのですが、外部ライブラリに依存しないが故に必要以上に複雑性になっている場合、いつかはその複雑性が問題となる日が来ることでしょう。

## 実用的なmackerel-pluginを書くための技術選択

この問題を解決するうちの手段のひとつがプラグインの開発言語にGo言語を採用することです。

Go言語の場合は、ライブラリは全て実行バイナリにstatic linkされるので、実行環境のライブラリには基本的に依存しません。また、多くのライブラリがオープンソースとして提供されています。
そのため、前述したようにな実行環境のライブラリインストール状況に依存しなくなるため、mackerel-agentのプラグインのためにそれを管理する必要性がなくなり、かつ自由にライブラリを活用することができます。
また、Go言語は強い静的型付けを持つプログラミング言語です。型制約で（ある程度の）プログラムの正しさを保証することができます。これはある程度以上複雑なプログラムを記述する際には極めて有用です。
その一方でGo言語はコンパイルが非常に速くデバッグのサイクルを回しやすい性質を併せ持ちます。Goのバイナリが実行環境のライブラリに依存しないとは言えど、mackerelのプラグインの性質上どうしても実行環境とのIOが発生せざるを得ません。そのため、プラグインの開発において副作用に起因するバグが発生することが容易に想像できます。
Go言語であれば開発中のプラグインも `go run` で簡単に即時にコンパイルして実行できるため、開発用の実行環境で動作するmackerel-agentの設定で `go run` 経由でプラグインを実行させることで、LLのように実行環境でのリアルタイムなデバッグ/開発も行うことができます。

Go言語をmackerel-pluginに採用することにはこれらの大きなメリットがあります。それはmackerelの公式プラグインでもGo言語が採用されていることからも明らかと言えるでしょう。

公式プラグインでもGo言語が採用されているため、もしあなたが自分の環境で使うためにGo言語で書いたプラグインがとても便利で他のみんなにも使ってほしいと思ったなら、公式リポジトリにpull-requestを行うことでcontributeを行うこともできます。
ぼくも7つほどpull-requestを行い、他の人が書いたプラグインのバグの修正も一部行いました。
このように、ひとつのソフトウェア資源をみんなで共有し、みんなで開発してそれぞれがコストを払い続ける代わりに、それぞれがその恩恵を受けることで、結果的にそれぞれのコストパフォーマンスが最適化されます。
公式リポジトリに積極的に[コントリビュート](https://github.com/mackerelio/mackerel-agent-plugins/blob/master/CONTRIBUTING.md
)していく姿勢を持つことで、よりMackerelの恩恵を得ることができるのです。
つまり、コントリビュートの手間を減らすためにも、Go言語は最適な選択肢のひとつと言えます。

また、公式プラグインを参考にすることはとても有効です。
公式プラグインはさまざまな環境で実行されており、いろんなケースに対処しています。
公式プラグインと同じGo言語を採用すれば、それらのノウハウを適用しやすくなるでしょう。
ただし、現在作業中のようですがディレクトリやパッケージ構成の変更を行っているようです。ディレクトリやパッケージ構成を参考にしたい場合はこれが反映されるのを待ったほうが良いかもしれません。

更に、Go言語であれば公式にプラグインの開発をサポートするライブラリも提供されています。

* メトリクスプラグイン用
  * https://github.com/mackerelio/go-mackerel-plugin-helper
  * https://github.com/mackerelio/go-mackerel-plugin
* チェックプラグイン用
  * https://github.com/mackerelio/checkers

現在はgo-mackerel-plugin-helperが推奨されており、主流ですが、いくつかの理由から今後は非推奨とされていたgo-mackerel-pluginが推奨されるかもしれません。
どちらも今後ともメンテナンスし続けられるそうですので、使いやすいほうを選択するか、公式のアナウンスを待つのも堅実な選択かもしれません。

これらのプラグインの詳しい使い方は公式ドキュメントを参照すると良いでしょう。

* https://mackerel.io/ja/docs/entry/advanced/checkers
* https://mackerel.io/ja/docs/entry/advanced/go-mackerel-plugin-helper

これらの大きなメリットがあるため、Go言語を書いたことがないという方でも、mackerel-agentのプラグインを書くためにGo言語を習得することはとても有効であると言えるでしょう。
実際、言語仕様自体は非常にコンパクトかつシンプルであり、簡単なmackerelのプラグインを書く程度であれば、[A Tour of Go](https://go-tour-jp.appspot.com/welcome/1)の知識さえあれば十分です。

## まとめ

mackerel-agentはプラグインを活用することで環境に柔軟に対応し、より堅牢な監視システムを構築することを手助けしてくれます。
また、プラグイン自体の仕様は非常に簡単で、どんな言語でも実装できるシンプルさを持っています。簡単なものであればshell scriptやLLでも十分な場合もあります。
そして、Go言語を採用することで堅牢かつ柔軟で運用しやすいプラグイン開発が実現できます。
もしGo言語を書いたことがない人でもぜひ一度Go言語に挑戦してプラグインを開発してみてください。

もし分からないことがあれば、[Mackerel Users Group](http://mackerel-ug.hatenablog.com/)の[Slack](https://mackerel-ug-slackin.herokuapp.com/)で質問してみると良いでしょう。
Mackerelを活用している多くの人が日々このSlack Teamで相談や雑談を行っています。プラグイン開発者も多くJoinしているので、プラグイン開発に関わる質問に回答できる人も多いでしょう。ぜひ活用してみてください。