mackerel-agent-plugin に Pull Request してみたかった

# TL;DR

自分の調査能力が甘すぎて完全に無駄な開発と記事作成をしてしまったので供養のために。読む価値はあまりありません。
Mackerel の plugin は [公式リポジトリ](https://github.com/mackerelio/mackerel-agent-plugins) だけじゃなく [plugin registry](https://github.com/mackerelio/plugin-registry) も探しましょう。
あと、今は公式リポジトリではなく plugin registry に PR を出すものらしいです。
ちゃんと公式のドキュメントを読んで、既存のプラグインを探してから開発しましょう。

# 背景

Go で Slack に常駐する bot をいくつか書いたり、mackerel にカスタムメトリックを投稿するスクリプトを書いたりということができるようになってきたあたりで、passenger のリソース使用量を監視する必要が生じた。
普段は自社サービス・サーバへゴリゴリにロックインした雑なカスタムメトリックばかり作っていたので特に OSS へ commit するなんて考えてこなかったが、passenger なら（たぶん）一般的だし、まだ公式 plugin リポジトリにも入ってないし、自分にもチャンスがあるんじゃないか、と思い立った。
[（注：書いてから気づいたけど Passenger プラグインはもうあります）](https://github.com/mackerelio/plugin-registry/tree/master/plugins)

# 準備

まずは公式 README を読む。

https://github.com/mackerelio/mackerel-agent-plugins/blob/master/README.md

* see The official plugin registry and Pull requests to the existing central repository
* fork it
* develop the plugin you want
* create a pullrequest!


とのことらしい。プログラマじゃないから何もわからん。
とりあえずやってみる。
（注：書いてから気づいたけど、もっとちゃんと読みましょう）


# やってみる

とはいえ、マジで何をしたらいいのかわからん。
こういうときは、とりあえず先人の偉業を確認してみる。

https://github.com/mackerelio/mackerel-agent-plugins/tree/master/mackerel-plugin-accesslog

一番上にあるし、何よりとてもコンパクトなので、きっとこれを真似ていけば動くものはできそう。

main を見てみる。
https://github.com/k-nishigaki/mackerel-agent-plugins/blob/master/mackerel-plugin-accesslog/main.go

```golang
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` を覗いてみる。

```zsh
 % ls mackerel-plugin-accesslog/lib
accesslog.go      accesslog_test.go testdata
```

シンプルだ。`accesslog.go` が本体なんだろうな。
Do があった。

https://github.com/mackerelio/mackerel-agent-plugins/blob/master/mackerel-plugin-accesslog/lib/accesslog.go#L197

色々 flag とかが書いてあるが、最終的には

```golang
	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 を満たしてやるといいらしい。

```golang:struct
// PassengerPlugin is mackerel plugin
type PassengerPlugin struct {
	Prefix string
}
```

とりあえず prefix だけ定義。
必要なものは後から追加する。

加えて、

> また、このプラグイン用 struct は mp.PluginWithPrefix の interface を満たす必要があります。 interface の定義は以下のようになっています。

```golang:PlutinWithPrefix
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 ページからパクって一つだけ作ってみる。

```golang:GraphDefinition
// 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 を取得する部分。いわば核となる部分か。
これもまずはコンパイルを通すために、とにかく値だけ返すやつを作る。

```go
// 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 で適当にやってたまにかぶって変なグラフタイトルになるところ。~~

これも参考ページからパクって適当に。

```go
// MetricKeyPrefix interface for mackerel-plugin
func (p PassengerPlugin) MetricKeyPrefix() string {
	if p.Prefix == "" {
		p.Prefix = "passenger"
	}

	return p.Prefix
}
```

### main の定義

Run() するための main を作る。

```go
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 して実行してみる

```zsh
% go build .
% ls
mackerel-plugin-passenger main.go
% ./mackerel-plugin-passenger
passenger.processes	10	1673424084
```

おぉ、実行された。
次は passenger のリソース使用量をいい感じに投稿するやつを作る。

## passenger のリソース使用量を mackerel に投稿するためにいい感じに出すやつ

まずは passenger-status から process 数を取れるようにしてみようと思う。

`passenger-status` の出力はこんな感じになっている。

```zsh
$ 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 ってシンプルすぎないか~~

また、各プロセスの情報も取得されている。（長いので適当に切り出し）

```xml
               <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 形式が変わったりしたときに手を入れる場所が分かりやすくなるよう関数で切り出した。

```go
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 に全て従った。](https://stackoverflow.com/questions/6002619/unmarshal-an-iso-8859-1-xml-input-in-go)

FetchMetrics もいい感じに改造。

```go
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 も改造。

```go
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](https://mackerel.io/blog/entry/feature/20171116#The-official-plugin-registry) and [Pull requests to the existing central repository](https://mackerel.io/blog/entry/feature/20171116#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 出してね）

おやおやおや・・・？


https://github.com/mackerelio/plugin-registry/blob/master/plugins/mackerel-plugin-passenger.json

もうあるじゃん！


# なぜオレはあんなムダな時間を……

というわけで、完全に車輪の再発明をしてしまいました。
ちゃんとリンク先まで読まないと駄目ですね。本当に反省しています。
[passenger-plugin](https://github.com/nabewata07/mackerel-plugin-passenger) 便利です。CPU とかは出ないけどこれで十分です。ありがとうございます。
Go で XML 使う練習になったと思うことにします。

こんなドジなインフラエンジニアと一緒に働いてみたいと思った方は是非ご応募ください。
インフラエンジニア以外にも色々募集しています。
https://www.green-japan.com/company/2706/job/138717