masarasiの日記

Mackerelのテクニカルサポートをやっています

プログラミングほぼ未経験の私がプラグインにオプションを追加した話

この記事は Mackerel Advent Calendar 2024 18 日目の記事です。

こんにちは。Mackerel CRE の id:masarasi です。Mackerel のテクニカルサポートを担当しています。

本記事のタイトルの通り、私はプログラミング経験がほとんどありません。前職まではサーバーやネットワーク機器を管理する仕事をしていたので、業務におけるプログラミング経験は皆無と言えるほどです。

そんな私ですが、テクニカルサポート業務を通して、Go 言語で書かれた mackerel-agent や各種プラグインソースコードをそれなりには解読できるようになり、最近ではプラグインにオプションを追加するというはじめての経験をしたので、本記事ではその様子を紹介します。

mackerel-plugin-jvmのオプションの挙動がわかりづらい

Java アプリケーションに関するメトリックを投稿する、mackerel-plugin-jvm というプラグインがあります。

このプラグインは、--javaname オプションに指定した名前のアプリケーションを対象に、jstat などのコマンドを実行した結果を元にメトリックとして投稿します。また、--javaname に指定した名前はメトリック名やグラフ名にも使用されます。

--javaname に指定した名前のアプリケーションが複数ある場合はどうすればよいか?--pidfile オプションで pid ファイルを指定すれば、同じ名前のアプリケーションが複数あっても別々に監視できます。

でも、それだとメトリック名やグラフ名がかぶるからダメなのでは?と思うかもしれませんが、--pidfile を指定する場合、--javaname オプションの役割はメトリック名やグラフ名を指定するだけになるので、全然関係のない文字列を指定しても問題ありません。

3 年ぐらい Mackerel のテクニカルサポートをやっているものの、この挙動については私も最近知ったのですが、それぐらい想像もしなかった挙動でした。--pidfile の有無によって --javaname オプションの挙動が変わるというのは、そういうものだと言ってしまえばまぁそれまでなのですが、直感的ではないし、たとえば --pidfile を指定しない場合に、メトリック名やグラフ名に --javaname に指定した名前以外の別名を付けたいケースもあるだろうと思って、それを実現するためのオプションを追加することにしました。

やったこと

mackerel-plugin-jvm に下記のオプションを追加しました。

  • --metric-name
    • メトリック名に使用する文字を指定できるオプション
  • --metric-label
    • グラフ名に使用する文字を指定できるオプション

※この記事の公開時点ではまだリリースされていません。追加したオプションは後日利用可能になります。

ソースコードのおおまかな変更内容としては下記の2つです。

  • オプションの追加
  • オプションの引数をグラフ定義に反映する

作業にあたって以下のヘルプを参考にしました。

mackerel.io

作業の様子

オプションの追加

以下のようにソースコードを変更しました。

プラグイン用 struct の定義

type JVMPlugin struct {
    Remote      string
    Lvmid       string
    JstatPath   string
    JinfoPath   string
    JavaName    string
    Tempfile    string

    ※以下を追加
    MetricKey   string
    MetricLabel string

mackerel-plugin-jvmプラグイン用 struct に、オプションの引数を格納するための変更を加えます。mackerel-plugin-jvm においては JVMPlugin という名前の struct(構造体)です。すでに既存のオプションのための項目があります。ここに今回追加する 2 つのオプションのための項目を追加しました(名前は任意です)。

オプションの定義

func Do() {
    // Prefer ${JAVA_HOME}/bin if JAVA_HOME presents
    pathBase := "/usr/bin"
    if javaHome := os.Getenv("JAVA_HOME"); javaHome != "" {
        pathBase = javaHome + "/bin"
    }
    〜(省略)〜
    optTempfile := flag.String("tempfile", "", "Temp file name")

    ※以下を追加
    optMetricKey := flag.String("metric-key", "", "Specifying the Name field in the Graph Definition")
    optMetricLabel := flag.String("metric-label", "", "Specifying the Label field in the Graph Definition")
    flag.Parse()

変数名は他のオプションの変数名にあわせて optMetricKeyoptMetricLabel としました。flag.String() の中のカンマで区切られた 1 つ目が実際にコマンドとして使用する際のオプション名になり、 3 つ目が -h(ヘルプ)で表示される際の description になります。今回は設定していませんが 2 つ目はデフォルト値になります。

オプションの引数をプラグイン用 struct に格納する

    jvm.JavaName = *optJavaName

    ※以下を追加
    jvm.MetricKey = *optMetricKey
    jvm.MetricLabel = *optMetricLabel

プラグインには以下の設定がされていて、JVMPluginjvm という名前の変数で扱えるようにしています(これを構造体の初期化と呼ぶらしい)。こうすることで、jvm.MetricKey という形で JVMPlugin の中身が扱えるようになります。

   var jvm JVMPlugin

これで、--metric-key オプションの引数として渡された文字列が optMetricKey に代入され、最終的に JVMPluginMetricKey に格納されます。--meric-label も同じ仕組みです。

続けて、オプションの引数で渡した文字列をメトリック名やグラフ名に反映する設定を行います。

オプションの引数をメトリック名およびグラフ名に反映する

前提として、Mackerel では グラフ定義 という仕組みによって投稿されたメトリックがグラフとして表示されます。プラグインを設定するだけでいい感じにホストの画面にグラフが表示されるのは、あらかじめプラグインにグラフ定義がされているからです。まずはその仕組みから見ていきます。

グラフ定義出力メソッド(GraphDefinition)を読み解く

グラフ定義は go-mackerel-plugin-helperGraphDefinition() 関数によって定義されます。

元々の設定は下記の通り。

func (m JVMPlugin) GraphDefinition() map[string]mp.Graphs {
    rawJavaName := m.JavaName
    lowerJavaName := strings.ToLower(m.JavaName)
    return map[string]mp.Graphs{
        fmt.Sprintf("jvm.%s.gc_events", lowerJavaName): {
            Label: fmt.Sprintf("JVM %s GC events", rawJavaName),
            Unit:  "integer",
            Metrics: []mp.Metrics{
                {Name: "YGC", Label: "Young GC event", Diff: true},
                {Name: "FGC", Label: "Full GC event", Diff: true},
                {Name: "CGC", Label: "Concurrent GC event", Diff: true},
            },
        },
        fmt.Sprintf("jvm.%s.gc_time", lowerJavaName): {
            Label: fmt.Sprintf("JVM %s GC time (sec)", rawJavaName),
            〜(省略)〜

5行目以降、fmt.Sprintf("jvm.%s.gc_events", lowerJavaName): { 〜 } のようなまとまりがメトリックごとに存在します。これはメトリックごとのグラフ定義です。メトリックの元になる情報は go-mackerel-plugin-helperFetchMetrics() が取得しています。グラフ定義は FetchMetrics() で取得した情報 1 つ 1 つに、メトリック名やどのグラフに表示するかを紐づける設定です。

5〜13行目は jvm.%s.gc_events という名前のメトリックのグラフ定義になっています。%s には lowerJavaName の値が代入されます。lowerJavaName は関数内の lowerJavaName := strings.ToLower(m.JavaName) の通り、m.JavaName を小文字(ToLower)にした文字列になります。m.JavaNamefunc (m JVMPlugin) の通り、JVMPluginJavaName を参照します。JavaNameDo() 関数の中で jvm.JavaName = *optJavaName となっていて、*optJavaName--javaname オプションのことです。つまり、--javaname オプションの引数として渡された文字列がメトリック名に使用されます。

メトリックを表示するグラフの指定が6行目の Label: で、fmt.Sprintf("JVM %s GC events", rawJavaName) となっています。こちらの %s は関数内に rawJavaName := m.JavaName というコードがあることから、こちらもメトリック名と同様に --javaname の引数として渡された文字列が使用されます。

したがって、たとえば --javaname の引数に Bootstrap を指定した場合、メトリック名は jvm.bootstrap.gc_events.YGC になり、このメトリックは JVM Bootstrap GC events というグラフに表示されるという仕組みです。

なお、今回は変更を加えていませんが、7行目の Unit: はメトリック値の単位で、8〜12行目はメトリック名のドットで区切られた末尾の名前の定義です。この末尾の名前の直前のドットまでの名前が同じメトリックは、同じグラフに表示されます。

ということで、プラグインのグラフ定義の仕組みを理解できたので、実際にコードを変更していきます。

オプションの引数を反映する

メトリック名に関しては以下のように変更しました。

変更前

   lowerJavaName := strings.ToLower(m.JavaName)

変更後

   javaName := m.MetricKey
    if javaName == "" {
        javaName = m.JavaName
    }
    lowerJavaName := strings.ToLower(javaName)

メトリック名として使用される javaName には新たに追加する --metric-key オプションの引数が使用されます。--metric-key の指定がない場合は javaName の値が空になってしまいますが、その場合には従来通り m.JavaName の値が代入されるようにしてあるので、--javaname オプションの引数が使用されます。

グラフ名に関しては以下のように変更しました。

変更前

   rawJavaName := m.JavaName

変更後

   metricLabel := m.MetricLabel
    if metricLabel == "" {
        metricLabel = m.JavaName
    }

変数名はわかりやすいように metricLabel に変更しました。処理の内容としては先述のメトリック名と同じで、メトリック名には --metric-label の引数が使用され、--metric-label の指定がない場合は --javaname オプションの引数が使用されます。

また、rawJavaName は各メトリックのグラフ定義の Label: で使用されているので、こちらも変更しました。JVM %s GC time (sec) 以外のメトリックについてもすべて同じように対応しています。

変更前

   Label: fmt.Sprintf("JVM %s GC time (sec)", rawJavaName),

変更後

   Label: fmt.Sprintf("JVM %s GC time (sec)", metricLabel),

以上で作業は完了です。

使用方法

以下は設定例です。この設定では、メトリックの名前が custom.jvm.tomcat.memorySpace.* になり、グラフ名が JVM tomcat MemorySpace になります。--pidfile を指定しているので以下の設定においては --javaname は実質意味のないオプションになってしまうのですが、--javaname は必須オプションなので記述が必要です。

[plugin.metrics.jvm-tomcat]
command = ["mackerel-plugin-jvm", "--javaname", "Bootstrap", "--metric-key", "tomcat", "--metric-label", "tomcat", "--pidfile", "/usr/local/src/tomcat/temp/tomcat.pid"]

実際に mackerel-agent.conf に設定してみると、以下のようなグラフが表示されます。

JVM tomcat MemorySpace グラフ

JVM tomcat MemorySpace のグラフ定義

最後に

mackerel-plugin-jvm に限らず、このプラグインはこうだったらもっと便利なのにな、と思うことがあります。しかし、プログラミング経験のない私は自分でプラグインを改修するなんて無理だろうと思っていました。なので、いつもは開発チームへ要望を出すまでに留まっていたのですが、今の自分ならオプションの追加ぐらいならがんばればできそう、走り切るのは無理だったとしても勉強になるだろうと思い、チャレンジしてみました。

やってみると、案外コードを書き換えた箇所は多くなかったですし(テストコードは難しくて開発チームのメンバーに書いてもらいましたが)、テスト環境でプラグインをビルドして、自分の期待通りの動きをした時はうれしかったです。普段、プログラムを書いている人から見れば小さな一歩かもしれませんが、自分にとっては大きな一歩を踏み出せた気がします。