Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

MackerelでGitHubのイシュー数推移を記録してみた

この記事はMackerelアドベントカレンダー2018の18日目です。

Mackerelはサーバ管理・監視サービスですが、取得する数値はサーバに限ったものではなく、例えば体重など、数値なら比較的なんでも記録することができて、記録した値の推移を眺めることができます。個人的にGitHubを使っていて積極的に参加していきたいと思っているので、活動した数値を可視化するプラグインを作ってみました。

f:id:lufiabb:20181218165122p:plain
作ったグラフ

この記事では、担当したイシューの残っている数と閉じた数を扱っていますが、GitHub API v3で取得できる値ならなんでも良いと思います。

プラグインを作る前に

プラグインは、mackerel-agentから1分ごとに呼ばれるコマンドです。Goが一番馴染んでいるのでGoを使ってプラグインを書きますが、ただのコマンドなので何で書いても良いと思います。

Goで書く場合、現在、プラグイン用の公式パッケージは2種類あります。

go-mackerel-plugin-helper のREADMEに、

We recommend to use go-mackerel-plugin instead of go-mackerel-plugin-helper to create mackerel agent plugin.

とあるので、今は go-mackerel-plugin を使う方が良さそうです。

プラグインを実装する

go-mackerel-plugin を使う場合は以下のインターフェイスどちらかを実装する必要があります。MetricKeyPrefix()があればユーザが設定ファイルでプラグインの名前を変更できるようになるので、新しく作る場合はPluginWithPrefixを実装する方が良いと思います。

package mackerelplugin

type Plugin inteface {
    // メトリック名やラベル、単位などを返すメソッド。
    GraphDefinition() map[string]Graphs

    // サーバから取得したメトリクスを返すメソッド。
    // マップのキーはGraphDefinitionで返したメトリック名に対応する。
    FetchMetrics() (map[string]float64, error)
}

type PluginWithPrefix interface {
    Plugin

    // プラグインの名前を返す。
    // 同じプラグインを異なる環境で使いたい場合に設定する。
    // (例えばGitHub.comとGHEで分けるなど)
    MetricKeyPrefix() string
}

例えばGitHubのイシューをopenとclosedで分けて収集したい場合、プラグインは以下のようなメトリクスを返すように書きます。ここで、github-issuesMetricKeyPrefix()で返した値となり、1545103883 はメトリックを取得した時刻です。中央の数値は FetchMetrics()が返す値です。

custom.github-issues.open   20   1545103883
custom.github-issues.closed 40   1545103883

go-mackerel-plugin で書く場合、メトリック名は以下の要素が.で連結されたものです。

  • custom (固定)
  • MetricKeyPrefix()の値
  • GraphDefinition()で返したマップのキー名
  • GraphDefinition()で返したマップのMetrics[].Name

そのため、上の例と同じメトリック定義を返す場合は以下のような実装になります。

import mp "github.com/mackerelio/go-mackerel-plugin"

func (g *GitHubPlugin) GraphDefinition() map[string]mp.Graphs {
    return map[string]mp.Graphs{
        "": {
            Metrics: []mp.Metrics{
                {Name: "open", Label: "Open", Stacked: true},
                {Name: "closed", Label: "Closed", Stacked: true},
            },
        },
    }
}

リポジトリごとにメトリクスを分けたい場合

上の例では、custom.github-issues.opencustom.github-issues.closed の2つしか値を返していませんが、GitHubは複数のリポジトリを持っているので、リポジトリ単位で分けられたらいいな、と思いました。イメージとしては以下のようなメトリックです。

custom.github-issues.repos.taskfs.open      20   1545103883
custom.github-issues.repos.taskfs.closed    40   1545103883
custom.github-issues.repos.plan9port.open   1    1545103883
custom.github-issues.repos.plan9port.closed 2    1545103883

しかしGitHub上のリポジトリは増えたり減ったりするので、最初のGraphDefinition()では決まった名前を返すことができません。この場合、メトリック名に1箇所だけワイルドカード(# または *)を含めることができるので、リポジトリ名の部分をワイルドカードにすると対応できるようです。

リポジトリ名の部分にワイルドカードを使ったGraphDefinition()です。

import mp "github.com/mackerelio/go-mackerel-plugin"

func (g *GitHubPlugin) GraphDefinition() map[string]mp.Graphs {
    return map[string]mp.Graphs{
        "repos.#": {
            Metrics: []mp.Metrics{
                {Name: "open", Label: "Open", Stacked: true},
                {Name: "closed", Label: "Closed", Stacked: true},
            },
        },
    }
}

ただし、ホストメトリック#グラフ定義の投稿によるとワイルドカードは1箇所だけしか使えません。

またワイルドカード # は一つまでしか使えません。 メトリック名全体は ^custom(\.([-a-zA-Z0-9_]+|[*#]))+$ のようになります。

メトリックの値を収集する

これはGitHub APIを使って収集するだけなので簡単ですね。

func (g *GitHubPlugin) FetchMetrics() (map[string]float64, error) {
    metrics := make(map[string]float64)
    var opt github.IssueListOptions
    opt.State = "all"
    for {
        a, resp, err := g.c.Issues.List(g.ctx, true, &opt)
        if err != nil {
            return nil, err
        }
        for _, p := range a {
            metrics["repos."+*p.Repository.Name+"."+*p.State]++
        }
        if resp.NextPage == 0 {
            break
        }
        opt.Page = resp.NextPage
    }
    return metrics, nil
}

アクセストークンなどの管理

Mackerelプラグインでアクセストークンなどのシークレットを扱う場合、どうするのが正しいのかわかりませんでしたが、環境変数プラグインに渡すのが良さそうです。

s := os.Getenv("GITHUB_ACCESS_TOKEN")
token := &oauth2.Token{AccessToken: s}
ts := oauth2.StaticTokenSource(token)
c := github.NewClient(oauth2.NewClient(ctx, ts))

動作確認

一通り実装したらメトリックが取れているか確認しましょう。go-mackerel-plugin を使っているならそのまま実行すれば取得したメトリックを標準出力に書き出すので、これで確認することができます。ここで出力されない場合、GraphDefinition()のメトリック名とメトリック値の名前が食い違っていることが多いです。

$ go run path/to/plugin/main.go
github-issues.repos.zipcode.open    4   1545117743
github-issues.repos.taskfs.open     1   1545117743
github-issues.repos.pin.closed      1   1545117743

また、MACKEREL_AGENT_PLUGIN_META 環境変数に何かセットすると、グラフ定義をJSONで確認することができます。(以下の例は整形しています)

$ MACKEREL_AGENT_PLUGIN_META=1 go run path/to/plugin/main.go
# mackerel-agent-plugin
{
  "graphs": {
    "github-issues.repos.#": {
      "label": "GitHub Issues",
      "unit": "integer",
      "metrics": [
        {
          "name": "open",
          "label": "Open",
          "stacked": true
        },
        {
          "name": "closed",
          "label": "Closed",
          "stacked": true
        }
      ]
    }
  }
}

プラグインの組み込み

mackerel-agent.confプラグインの実行コマンドを追加してエージェントを再起動すればメトリックが収集されるようになります。下ではテストのためにgo runしていますが、通常はビルドしたコマンドを使いましょう。

[plugin.metrics.github]
command = "go run path/to/plugin/main.go"

他のサンプル

mackerel-agent-pluginsにいっぱいあるので参考になりました。

グラフの調整

上のプラグインでopen, closedのイシューをリポジトリ単位で取れるようになりましたが、このままだとopen/closedが全部積み重なって表示されるため少し読みづらいです。

f:id:lufiabb:20181218165439p:plain
オープン・クローズドが混ざったグラフ

終わったものと残っているものの推移を知りたいので、式を使ったグラフで対応しました。

  1. カスタムダッシュボードでグラフを追加
  2. グラフのタイプを 式グラフ に変更
  3. 式を書く
stack(
  group(
    alias(sum(host(3u5u9mHFmFS, custom.github-issues.repos.*.closed)), 'closed issues'),
    alias(sum(host(3u5u9mHFmFS, custom.github-issues.repos.*.open)), 'open issues')
  )
)

最終的にオープン・クローズドを分けてどれだけ消化したのかを見られるようになりました。上の式では全部のリポジトリをまとめて集計していますが、特定のリポジトリだけ取り出すことも簡単にできそうですね。

f:id:lufiabb:20181218165726p:plain
最終的なダッシュボード

式はカスタマイズしたグラフを表示するが分かりやすかったです。

悩んだところ

グラフ定義を変更したらエージェント再起動が必要?

正確には分かってませんが、開発中にグラフ定義をよく変更していました。このとき、エージェントを起動したままプラグインから返すグラフ定義を変更すると、変更した後に取得したメトリックの単位がfloatになっていたり、ワイルドカードを使ってもまとまらなかったりしました。

何かおかしいなと思ったらエージェントを再起動してみましょう。

グラフ定義を削除したい

上のように、間違ったグラフ定義が作られてしまった場合、不要な定義がいっぱい作られてしまうので、不要ならhttps://mackerel.io/my/graph-defs を開くと不要なグラフ定義を削除できるようです。