TL;DR
自分の調査能力が甘すぎて完全に無駄な開発と記事作成をしてしまったので供養のために。読む価値はあまりありません。
Mackerel の plugin は 公式リポジトリ だけじゃなく plugin registry も探しましょう。
あと、今は公式リポジトリではなく plugin registry に PR を出すものらしいです。
ちゃんと公式のドキュメントを読んで、既存のプラグインを探してから開発しましょう。
背景
Go で Slack に常駐する bot をいくつか書いたり、mackerel にカスタムメトリックを投稿するスクリプトを書いたりということができるようになってきたあたりで、passenger のリソース使用量を監視する必要が生じた。
普段は自社サービス・サーバへゴリゴリにロックインした雑なカスタムメトリックばかり作っていたので特に OSS へ commit するなんて考えてこなかったが、passenger なら(たぶん)一般的だし、まだ公式 plugin リポジトリにも入ってないし、自分にもチャンスがあるんじゃないか、と思い立った。
(注:書いてから気づいたけど Passenger プラグインはもうあります)
準備
まずは公式 README を読む。
- see The official plugin registry and Pull requests to the existing central repository
- fork it
- develop the plugin you want
- create a pullrequest!
とのことらしい。プログラマじゃないから何もわからん。
とりあえずやってみる。
(注:書いてから気づいたけど、もっとちゃんと読みましょう)
やってみる
とはいえ、マジで何をしたらいいのかわからん。
こういうときは、とりあえず先人の偉業を確認してみる。
一番上にあるし、何よりとてもコンパクトなので、きっとこれを真似ていけば動くものはできそう。
main を見てみる。
https://github.com/k-nishigaki/mackerel-agent-plugins/blob/master/mackerel-plugin-accesslog/main.go
package main
import "github.com/mackerelio/mackerel-agent-plugins/mackerel-plugin-accesslog/lib"
func main() {
mpaccesslog.Do()
}
終わった。なるほど、これだけなのか。
ほかを見ても大体こんな感じだったので、最終的に mppassenger.Do()
を持つ main.go を作ればよいのだろうと思われる。
使ったことのあるプラグインも、最終的にはバイナリを指定して、という形になっているし、きっとこれは最後の作業なのだろう。うん、たぶん。
本体のありそうな mackerel-plugin-accesslog/lib
を覗いてみる。
% ls mackerel-plugin-accesslog/lib
accesslog.go accesslog_test.go testdata
シンプルだ。accesslog.go
が本体なんだろうな。
Do があった。
色々 flag とかが書いてあるが、最終的には
mp.NewMackerelPlugin(&AccesslogPlugin{
prefix: *optPrefix,
file: flag.Args()[0],
posFile: *optPosFile,
noPosFile: *optNoPosFile,
parser: parser,
}).Run()
が実行されて、結果が Mackerel の metric として正しい形で stdout に出されれば、あとは agent がよしなに投稿してくれる、はず。
mp ってのは mp "github.com/mackerelio/go-mackerel-plugin"
として import されているから、こいつに素敵にいい感じな情報を渡して Run() させられればきっと mackerel-agent-plugin として動くようになるのだろう。たぶん。
apache2 とかの plugin なんかを見てみると、入り口自体は違うが、
https://github.com/k-nishigaki/mackerel-agent-plugins/blob/master/mackerel-plugin-apache2/lib/apache2.go#L229-L241
最終的には helper.Run() している。
https://github.com/k-nishigaki/mackerel-agent-plugins/blob/master/mackerel-plugin-apache2/lib/apache2.go#L92-L109
go-mackerel-plugin と go-mackerel-plugin-helper の違いはわからないが、どうやら次は「passenger のリソース使用量をいい感じに出すやつを作って .Run() で起動する」とこを目指すとよさそう。
が、気になるので何が違うのかググってみた。
https://mackerel.io/ja/docs/entry/advanced/go-mackerel-plugin-helper
現在、プラグイン作成には、ここで紹介しているgo-mackerel-plugin-helperよりも、go-mackerel-pluginを利用することを推奨しています。go-mackerel-pluginを利用したプラグイン作成方法の詳細は https://mackerel.io/ja/docs/entry/advanced/go-mackerel-plugin をご覧ください。
とあるので、ここは素直に go-mackerel-plugin を利用することにしよう。
go-mackerel-plugin で起動するだけのスクリプトを作る
もうマジで何をしていいのかわからないので、まずはとにかく起動させたい。
しかしあんまり遠回りしたくもない。
ので「passenger のリソース使用量はいい感じに出ることを前提」としたうえで、とにかく同じ値を出し続けるガワだけ作ってみる。
使用量の取得そのものには passenger-memory-stats を使うことを想定しているので、そこから得られる
- Apache プロセス数・メモリ使用量の総量
- Nginx プロセス数・メモリ使用量の総量
- Passenger プロセス数・メモリ使用量の総量
をメトリックとして出すことにしたいと思う。
まずはドキュメントを読んで見る。
https://mackerel.io/ja/docs/entry/advanced/go-mackerel-plugin
とても丁寧に色々書いてあるので一つずつやっていく。
プラグイン用 struct の定義
struct を定義して、mp.PluginWithPrefix の interface を満たしてやるといいらしい。
// PassengerPlugin is mackerel plugin
type PassengerPlugin struct {
Prefix string
}
とりあえず prefix だけ定義。
必要なものは後から追加する。
加えて、
また、このプラグイン用 struct は mp.PluginWithPrefix の interface を満たす必要があります。 interface の定義は以下のようになっています。
type PluginWithPrefix interface {
FetchMetrics() (map[string]float64, error)
GraphDefinition() map[string]mp.Graphs
MetricKeyPrefix() string
}
とのことなので、interface を満足するために FetchMetrics()
と GraphDefinition()
と MetricKeyPrefix()
を作る必要がありそう。
help の順番にやっていく。
GraphDefinition を作成
普段は面倒で省いている Graph 定義の部分。
まずは go-mackerel-plugin の help ページからパクって一つだけ作ってみる。
// GraphDefinition interface for mackerelplugin
func (p *PassengerPlugin) GraphDefinition() map[string]mp.Graphs {
labelPrefix := strings.Title(p.MetricKeyPrefix())
return map[string]mp.Graphs{
"": {
Label: labelPrefix,
Unit: "integer",
Metrics: []mp.Metrics{
{Name: "processes", Label: "Processes"},
},
},
}
}
絶対狙い通りに動かないが、まずはこれでコンパイルを通す。
FetchMetrics の定義
mackerel-plugin が投稿する metric を取得する部分。いわば核となる部分か。
これもまずはコンパイルを通すために、とにかく値だけ返すやつを作る。
// FetchMetrics interface for mackerel-plugin
func (p PassengerPlugin) FetchMetrics() (map[string]float64, error) {
// TODO passenger の値をいい感じに取得する
return map[string]float64{"processes": 10.0}, nil
}
次。
MetricKeyPrefix の定義
mackerel-plugin が投稿する metric の名前空間 prefix を出す部分。
普段 shell で適当にやってたまにかぶって変なグラフタイトルになるところ。
これも参考ページからパクって適当に。
// MetricKeyPrefix interface for mackerel-plugin
func (p PassengerPlugin) MetricKeyPrefix() string {
if p.Prefix == "" {
p.Prefix = "passenger"
}
return p.Prefix
}
main の定義
Run() するための main を作る。
func main() {
optPrefix := flag.String("metric-key-prefix", "passenger", "Metric Key Prefix")
optTempfile := flag.String("tempfile", "tmp-mackerel-plugin-passenger", "Temp file name")
flag.Parse()
p := PassengerPlugin{
Prefix: *optPrefix,
}
plugin := mp.NewMackerelPlugin(p)
plugin.Tempfile = *optTempfile
plugin.Run()
}
build して実行してみる
% go build .
% ls
mackerel-plugin-passenger main.go
% ./mackerel-plugin-passenger
passenger.processes 10 1673424084
おぉ、実行された。
次は passenger のリソース使用量をいい感じに投稿するやつを作る。
passenger のリソース使用量を mackerel に投稿するためにいい感じに出すやつ
まずは passenger-status から process 数を取れるようにしてみようと思う。
passenger-status
の出力はこんな感じになっている。
$ sudo /path/to/passenger-status
Version : ないしょ
Date : 2023-01-11 17:05:26 +0900
Instance: ないしょ
----------- General information -----------
Max pool size : 75
App groups : 1
Processes : 10
Requests in top-level queue : 0
----------- Application groups -----------
ないしょ
App root: ないしょ
Requests in queue: 0
* PID: 104663 Sessions: 1 Processed: 1773 Uptime: 1h 29m 33s
CPU: 4% Memory : 584M Last used: 49m 38s
* PID: 61961 Sessions: 0 Processed: 1750 Uptime: 1h 15m 7s
CPU: 2% Memory : 298M Last used: 0s ago
* PID: 62003 Sessions: 0 Processed: 1862 Uptime: 1h 15m 7s
CPU: 2% Memory : 282M Last used: 8s ago
* PID: 62045 Sessions: 0 Processed: 1438 Uptime: 1h 15m 6s
CPU: 2% Memory : 290M Last used: 8s ago
* PID: 62089 Sessions: 0 Processed: 1013 Uptime: 1h 15m 6s
CPU: 16% Memory : 232M Last used: 1m 11s ag
* PID: 62132 Sessions: 0 Processed: 626 Uptime: 1h 15m 6s
CPU: 2% Memory : 285M Last used: 1m 11s ag
* PID: 76772 Sessions: 0 Processed: 646 Uptime: 1h 12m 42s
CPU: 1% Memory : 220M Last used: 1m 10s a
* PID: 89757 Sessions: 0 Processed: 371 Uptime: 1h 10m 19s
CPU: 2% Memory : 181M Last used: 1m 31s a
* PID: 39421 Sessions: 0 Processed: 73 Uptime: 11m 53s
CPU: 1% Memory : 219M Last used: 2m 33s ago
* PID: 39444 Sessions: 0 Processed: 90 Uptime: 11m 53s
CPU: 1% Memory : 176M Last used: 2m 35s ago
弊社のサービスでは Application は一個だけなんだけど、起動すらしてない場合(0 の場合)もありえるし、App groups
も取得しよう。
passenger-status
がインストールされる場所は環境によって(rbenv 使ってたり使ってなかったりとかで)変わると思うので、path の prefix を入れられるようにしつつ、↓の情報を metric として切り出す。
----------- General information -----------
Max pool size : 75
App groups : 1
Processes : 10
Requests in top-level queue : 0
PID 別の情報もあったら便利だとは思うけど、起動数に比例してグラフが見づらくなる未来しか見えないのでまずはスルー。
grep してもいいんだけど、せっかく xml option があるのでそちらでやってみる。
$ sudo /path/to/passenger-status --show=xml
<?xml version="1.0" encoding="iso-8859-1"?>
<info version="3">
<passenger_version>ないしょ</passenger_version>
<group_count>1</group_count>
<process_count>3</process_count>
<max>75</max>
<capacity_used>3</capacity_used>
<get_wait_list_size>0</get_wait_list_size>
<supergroups>
(以下略)
もしバージョンによる差異があっても info version とかで分岐するように作れば汎用性が出るだろう。
対応はこうか
raw | xml |
---|---|
Max pool size | max |
App groups | group_count |
Processes | process_count |
Requests in top-level queue | get_wait_list_size |
max ってシンプルすぎないか
また、各プロセスの情報も取得されている。(長いので適当に切り出し)
<process>
<pid>515</pid>
<sessions>1</sessions>
<processed>1406</processed>
<cpu>0</cpu>
<rss>383740</rss>
<pss>377435</pss>
<private_dirty>374768</private_dirty>
<swap>0</swap>
<real_memory>374768</real_memory>
<vmsize>1001928</vmsize>
</process>
せっかくなので全プロセスがどれくらい CPU を食ってるのか、どれくらいメモリを食ってるのかも出してみる。
XML を読み込む部分
なんかすごい苦労した・・・
後から XML 形式が変わったりしたときに手を入れる場所が分かりやすくなるよう関数で切り出した。
type Info struct {
XMLName xml.Name `xml:"info"`
Max float64 `xml:"max"`
Group float64 `xml:"group_count"`
ProcessCount float64 `xml:"process_count"`
Queue float64 `xml:"get_wait_list_size"`
Processes []Process `xml:"supergroups>supergroup>group>processes>process"`
}
type Process struct {
XMLName xml.Name `xml:"process"`
CPU float64 `xml:"cpu"`
RealMemory float64 `xml:"real_memory"`
VirtualMemory float64 `xml:"vmsize"`
}
func getPassengerStatus(pathPrefix string) (Info, error) {
info := Info{}
cmd := exec.Command(path.Join(pathPrefix, "passenger-status"), "--show=xml")
out, err := cmd.CombinedOutput()
if err != nil {
log.Println("passenger-status failed")
return info, err
}
reader := bytes.NewReader(out)
decoder := xml.NewDecoder(reader)
decoder.CharsetReader = charset.NewReaderLabel
err = decoder.Decode(&info)
if err != nil {
log.Println("passenger status decode failed")
return info, err
}
return info, nil
}
XML の Encoding が iso-8859-1(<?xml version="1.0" encoding="iso-8859-1"?>
)になっていると、何やら Unmarshal に失敗する(というか、encoding がローカルと違うと失敗する?)ようで、CharsetReader なるものをゴニョゴニョする必要があるらしい。
なんもわからんので Stack Overflow に全て従った。
FetchMetrics もいい感じに改造。
func (p PassengerPlugin) FetchMetrics() (map[string]float64, error) {
metric := map[string]float64{}
info, err := getPassengerStatus(p.PassengerPath)
if err != nil {
log.Println("fetch metrics failed")
return metric, err
}
totalCPU := 0.0
totalRealMemory := 0.0
totalVirutalMemory := 0.0
for _, p := range info.Processes {
totalCPU += p.CPU
totalRealMemory += p.RealMemory
totalVirutalMemory += p.VirtualMemory
}
metric["process.count"] = info.ProcessCount
metric["process.total.cpu"] = totalCPU
metric["process.total.real_memory"] = totalRealMemory
metric["process.total.virtual_memory"] = totalVirutalMemory
metric["max_pool_size"] = info.Max
metric["app_count"] = info.Group
metric["queue_length"] = info.Queue
return metric, nil
}
もちろん GraphDefinition も改造。
func (p PassengerPlugin) GraphDefinition() map[string]mp.Graphs {
labelPrefix := strings.Title(p.MetricKeyPrefix())
return map[string]mp.Graphs{
"": {
Label: labelPrefix,
Unit: "integer",
Metrics: []mp.Metrics{
{Name: "process.count", Label: "Processes"},
{Name: "process.total.cpu", Label: "CPU Usage"},
{Name: "process.total.real_memory", Label: "Real Memory Usage", Scale: 1024},
{Name: "process.total.virtual_memory", Label: "Virtual Memory Usage", Scale: 1024},
{Name: "max_pool_size", Label: "Max Processes"},
{Name: "app_count", Label: "App Groups"},
{Name: "queue_length", Label: "Waiting Queue Length"},
},
},
}
}
real_memory と virtual_memory は計算してみたら KB で出力されていたので、Scale で 1024 倍している。
ここの Name と FetchMetrics で返すときの key を揃えておかないと出力されないことに気づくのに無駄な時間を過ごした・・・。
passenger-status までの path を指定できるように main も改造。
func main() {
optPrefix := flag.String("metric-key-prefix", "passenger", "Metric Key Prefix")
optTempfile := flag.String("tempfile", "tmp-mackerel-plugin-passenger", "Temp file name")
optPassengerPath := flag.String("passenger-path", "", "passenger-status and passenger-memory-stats path prefix")
flag.Parse()
p := PassengerPlugin{
Prefix: *optPrefix,
PassengerPath: *optPassengerPath,
}
plugin := mp.NewMackerelPlugin(p)
plugin.Tempfile = *optTempfile
plugin.Run()
}
build して実行。
% ./mackerel-plugin-passenger --passenger-path=/path/to/passenger-status-dir/
passenger.process.count 3 1673513164
passenger.process.total.cpu 0 1673513164
passenger.process.total.real_memory 920309760 1673513164
passenger.process.total.virtual_memory 2805604352 1673513164
passenger.max_pool_size 75 1673513164
passenger.app_count 1 1673513164
passenger.queue_length 0 1673513164
いい感じになったと思う。
PR を出してみようとした
動いたし、そろそろ PR を出してみよう、と思ってどうやって出せばいいのか今一度確認。
see The official plugin registry and Pull requests to the existing central repository
・・・おや?
Up until now, it was acceptable to register a new plugin with the above-mentioned central repository, but as a general rule from now on, adding new plugins this way will no longer be accepted. Adding a feature to an existing plugin or fixing a bug is of course welcome.
If you create a new plugin, you should create your own plugin repository on GitHub and register it in the official plugin registry.
If you would like to incorporate a registered plugin into Mackerel’s official plugin package distributed by rpm or deb, please contact us through a separate issue etc. Conversely, the Mackerel development team might reach out to plugin creators to ask for permission to incorporate registered packages into the official plugin package. In such a case, we would appreciate your cooperation.
(意訳:昔は直接 mackerel-agent-plugins に PR 出してもらってたけど、今は自分とこで作って体裁整えたあとに plugin registry に PR 出してね)
おやおやおや・・・?
もうあるじゃん!
なぜオレはあんなムダな時間を……
というわけで、完全に車輪の再発明をしてしまいました。
ちゃんとリンク先まで読まないと駄目ですね。本当に反省しています。
passenger-plugin 便利です。CPU とかは出ないけどこれで十分です。ありがとうございます。
Go で XML 使う練習になったと思うことにします。
こんなドジなインフラエンジニアと一緒に働いてみたいと思った方は是非ご応募ください。
インフラエンジニア以外にも色々募集しています。
https://www.green-japan.com/company/2706/job/138717