masarasi.blog

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

check-logでマッチしたログを1行ごとにアラートにするラッパーを作ってみた

この記事はMackerel Advent Calendar 202519日目の記事になります。

Mackerel CREチームでテクニカルサポートを担当している五十嵐(id:masarasi)です。

📝 概要

通常のcheck-logではパターンにマッチするログが複数ある場合に、1件のアラートしか発生しませんが、この記事でご紹介するcheck-log-per-lineを使うことで、パターンにマッチしたログを1行ごとに個別のアラートとして発生させることができます。

check-log-per-lineはcheck-logプラグインのラッパースクリプトなので、監視条件にcheck-logのオプションをそのまま使用できます。詳しいことや注意事項をREADMEに書いてあるので、利用する際はご一読ください。

なお、Mackerel公式の提供ではないため、サポート窓口にお問い合わせいただいてもサポートできない点はご留意ください。 ご意見などはリポジトリissueにお寄せください🙇🏻‍♂️

🛠️ 利用方法

mackerel-agent.confにチェック監視として設定します。MACKEREL_CHECK_NAMEに設定した名前が、個別に発生するアラートの監視ルール名になります。

[plugin.checks.check-log-per-line]
command = ["/path/to/check-log-per-line.sh", "--file", "/path/to/log", "--pattern", "ERROR"]
env = { MACKEREL_CHECK_NAME = "messages-error" }

▶️ 動作の様子

以下は実際のアラートの様子です。ちなみにcheck-log-per-lineによるアラートは自動でクローズされないので、手動でクローズする必要があります。

check-log-per-lineのアラート

(参考)以下はcheck-logによるアラートの様子です。画像では3件のログがまとめられてアラートが発生していることがわかります。

check-logのアラート

  • ホスト名のLinuxtypoしていて恥ずかしい

🤔 どういう時に役立つの?

check-logはパターンにマッチするログがあればアラートを発生させ、次回以降の実行でマッチするログがない場合、アラートがクローズされます。check-logの仕組みにおいてこれは正しい動作です。

しかし、たとえばこんなシチュエーションがあるとします。

check-logによるアラートが発生。確認すると、マッチしたログは重大な問題に関わる内容だった。このログはその性質から頻繁にログに出力されるものではない。そして1度でも出力されると危険なものだ。1分後、check-logはまた実行されたが、パターンにマッチするログは出力されておらず、アラートクローズの通知が届く。しかし、問題はまだ解消していない。

このような場合、すぐにアラートがクローズしてしまうことによって、アラートを見逃してしまったり、問題が解消されたという誤解を生んでしまったり、といったリスクが考えられます。

すぐにアラートがクローズしてしまう点については、prevent_alert_auto_close(自動クローズを無効化するオプション)で解決できるケースもあるでしょう。しかし、アラートがオープンのままになるということは、その監視ルール(同じ名前の監視ルール)によって新規のアラートが発生しないということにもなります。新たにマッチするログがあった場合、すでにオープンになっているアラートの一部として扱われ、チェック監視の性質上、ステータスに変化がなければ通知もされません。そのため、たとえばアラートの対応に時間がかかる場合には、新規のアラートを見逃すリスクが上がります。*1

また、複数のログが1件のアラートにまとめられることで、それぞれのログ(=問題)が別の原因で生じている場合に、インシデント管理がしづらいという面もあります。

check-log-per-lineはログを1行ごとにアラートとして発生させることができますので、これらの課題解決に役立ちます。

☕️ あとがき

check-log-per-lineの作成にはclaudeを利用しました。planモードで要件を固めてから作成してもらったので、初回から結構精度のいいものができました。プロトタイプを使って検証し、check-logがもともと出すLOG CRITICAL: 3 〜というアラートは冗長に感じたので出力を抑制したり、手動でテストしたい時にアラートが発生する*2とやりづらそうなので、手動実行時は--dry-runを強制したり、気付いた点をいくつか改善して完成させました。

以上、check-logでマッチしたログを1行ごとにアラートにするcheck-log-per-lineのご紹介でした。

*1:check-log-per-lineも監視ルール名に数字を1から採番するという仕組み上、名前が被ることはあるので、同じようなリスクはあるものの、1件しかアラートがオープンしないcheck-logよりかはマシなはず。監視ルール名をユニークにすることは可能ですが、一度登録された監視ルールは内部的に残り続けるので、たとえばダウンタイムの絞り込み条件の監視ルールリストに表示され続けてしまいます。そこまで大きな影響ではないですが、1度しか使わない監視ルールが増えていくのは悩ましいところです。

*2:check-logは手動実行とmackerel-agent経由での実行では参照するstateファイルが異なっていて、普段mackerel-agentで監視をしていても手動実行用のstateファイルは更新されません。そのため、前回の手動実行から時間が経っていると大量の差分をチェックしてアラートが大量に出る可能性があります(実体験。手動でクローズする必要があるので大変でした...🥺)。mackerel-agent経由の場合でも、--check-firstを有効にしている場合は初回チェックで大量のアラートが出る可能性があるので注意してください。

MackerelにGoのトレースを送信する〜自動計装〜

アプリケーションから直接Mackerelにトレースを送信するパターンと、OpenTelemetry Collectorを使ってトレースを送信するパターンを試す。

環境の準備

Dockerで簡単なGoのHTTPサーバーアプリケーションを動かす。

アプリケーションはGeminiにお願いしたところ、ルートパス(/)にアクセスすると "Hello, World!" と応答し、/greet/{name}の形式でアクセスすると、"{name} さん、こんにちは!" と応答するアプリケーションを作ってくれた。感謝。

ディレクトリ構造

app
└Dockerfile
└main.go
.env
docker-compose.yaml

Dockerfile

FROM golang:latest

WORKDIR /app
COPY main.go .
RUN go build -o hello-server main.go
ENTRYPOINT [ "/app/hello-server" ]

main.go

package main

import (
    "fmt"
    "log"
    "net/http"
    "regexp"
)

func rootHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Hello, World!\n"))
}

func greetHandler(w http.ResponseWriter, r *http.Request) {
    re := regexp.MustCompile(`/greet/([a-zA-Z0-9]+)`)
    match := re.FindStringSubmatch(r.URL.Path)
    if len(match) > 1 {
        name := match[1]
        response := fmt.Sprintf("%s さん、こんにちは!\n", name)
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(response))
    } else {
        http.NotFound(w, r)
    }
}

func main() {
    // ルートパスへのハンドラを登録
    http.HandleFunc("/", rootHandler)

    // /greet/ 以降のパスへのハンドラを登録
    http.HandleFunc("/greet/", greetHandler)

    // サーバーを起動してリクエストをリッスン
    port := ":8080"
    log.Printf("Server listening on port %s", port)
    err := http.ListenAndServe(port, nil) // デフォルトの ServeMux を使用
    if err != nil {
        log.Fatalf("Server failed to start: %v", err)
    }
}

.env

MACKEREL_API_KEY="<APIキー>"

docker-compose.yaml

services:
  hello-server:
    build: 
      context: ./app
      dockerfile: Dockerfile
    ports:
      - 8000:8000
    env_file: ./.env

計装

Mackerelのヘルプを参考に計装していく。

mackerel.io

1. go get

これを行う前にgo mod init hello-serverを実行しておく。

私はGoに不慣れなので、いきなりヘルプ通りgo getを実行したらgo: go.mod file not found in current directory or any parent directory.というエラーが出てしまった。

2. 初期設定

ヘルプに書かれている内容をmain.goに追記する。semconv.ServiceNameなどの値は任意で変更可能。

なお、ヘルプには書かれていないが、実際は以下のパッケージもimportする必要がある(initTracerProvider関数で使用しているため)。また、semconvはこの検証を行った時点でv1.24.0が出ていた。

  • go.opentelemetry.io/otel
  • go.opentelemetry.io/otel/propagation
  • go.opentelemetry.io/otel/sdk/resource

いくつかのパッケージでno required module provides packageが出ていたので、go mod tidyを実行した。

3. ミドルウェアの挿入

このパートは最初ヘルプを見ても何をやっているのかよくわからず、なかなか難易度が高かった。自力ではうまくいかなかったので、最終的にはGeminiに計装してもらった。

main()の差分は以下のとおり。

    func main() {
   +    ctx := context.Background()
   +    shutdown, err := initTracerProvider(ctx)
   +    if err != nil {
   +        log.Fatalf("Failed to initialize tracer provider: %v", err)
   +        return
   +    }
   +    defer shutdown(ctx)
   +
      // ルートパスへのハンドラを登録
   -    http.HandleFunc("/", rootHandler)
   +    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   +        otelhttp.NewHandler(http.HandlerFunc(rootHandler), "root").ServeHTTP(w, r)
   +    })
   
      // /greet/ 以降のパスへのハンドラを登録
   -    http.HandleFunc("/greet/", greetHandler)
   -
   -    // サーバーを起動してリクエストをリッスン
   +    http.HandleFunc("/greet/", func(w http.ResponseWriter, r *http.Request) {
   +        otelhttp.NewHandler(http.HandlerFunc(greetHandler), "greet").ServeHTTP(w, r)
   +    })
   +

前半のdefer shutdown(ctx)まではヘルプの通り。

出来上がったコードを見るとなんとなく雰囲気がわかるが、やっていることとしては、本来実行される関数(rootHandlerなど)をOpenTelemetryの関数でラップすることでトレースを取得している。

Dockerfileの調整

もとのアプリケーションのコードから必要なパッケージが増えたのでgo mod関連のコマンドを追記。

FROM golang:latest

WORKDIR /app
COPY main.go .
### 追記ここから
COPY go.mod .
RUN go mod tidy
### 追記ここまで
RUN go build -o hello-server main.go
ENTRYPOINT [ "/app/hello-server" ]

アプリケーションからトレースを送信

docker compose builddocker compose upを実行し、ブラウザでhttp://localhost:8000/にアクセスしたところ、Hello, World! が表示されることを確認。

Mackerel側のトレース画面を見たところ、無事トレースも送信されていることを確認した。

OpenTelemetry Collectorを使ってトレースを送信

OpenTelemetry CollectorもDockerで稼働させる。Collectorの設定方法は下記のヘルプを参考にする。

mackerel.io

OpenTelemetry Collectorの追加

docker-compose.yaml

services:
  hello-server:
    build: 
      context: ./app
      dockerfile: Dockerfile
    ports:
      - 8000:8000
    # Collectorで送信するのでAPIキーの読み込みは不要
    # env_file: ./.env
  # 以下を追記
  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    volumes:
      - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml
    ports:
      - 4318:4318
    env_file: ./.env

以下をdocker-compose.yamlなどと同じ階層に作成する。

otel-collector-config.yaml

receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 500
    spike_limit_mib: 100
  batch:
    send_batch_size: 5000
    send_batch_max_size: 5000

exporters:
  otlphttp/mackerel:
    endpoint: "https://otlp-vaxila.mackerelio.com"
    headers:
      Accept: "*/*"
      "Mackerel-Api-Key": ${env:MACKEREL_API_KEY}
  # デバッグ用。スパンの情報が標準出力されるので便利
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp/mackerel, debug]

アプリケーションのトレースの送信先を変更

変更前

  client := otlptracehttp.NewClient(
    otlptracehttp.WithEndpoint("otlp-vaxila.mackerelio.com"),
    otlptracehttp.WithHeaders(map[string]string{
      "Accept":         "*/*",
      "Mackerel-Api-Key": os.Getenv("MACKEREL_API_KEY"),
    }),
    otlptracehttp.WithCompression(otlptracehttp.GzipCompression),

変更後

   client := otlptracehttp.NewClient(
        otlptracehttp.WithEndpoint("otel-collector:4318"),
        otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
        otlptracehttp.WithInsecure(),
    )

otlptracehttp.WithEndpointdocker-compose.yamlに追加したCollectorのサービス名とポートを指定する。なお、otlptracehttp.WithEndpointはデフォルトだとhttpsの通信をするのだが、それだとCollectorへの送信に失敗したので、httpで送信するためにotlptracehttp.WithInsecure()を追記している。

ヘッダーやAPIキーはotel-collector-config.yamlで設定してあるので削除する。otlptracehttp.WithCompressionもCollectorの方に任せられそうだが、とりあえずCollectorを使って送信することが目的なのでここでは気にしない。

トレースの送信

コンテナが起動中であれば一旦停止して、docker compose builddocker compose upを実行し、ブラウザでhttp://localhost:8000/にアクセスしてMackerelにトレースが送信されていることを確認する。

Goの自動計装の難しいところ

これ以前に、RubyPython(flask)への自動計装をやったが、それらは計装用のコードを追加するだけでよかったのに対し、Goの場合は既存のコードの書き換えが必要になるので、計装の難易度がアプリケーションの作りやGoへの理解度に大きく依存するなと感じた。

この検証を始めた頃は、kmutoさんのブログにあったhello-serverというアプリケーションを使って計装を進めていたのだが、http.HandleFuncの処理をOpenTelemetryの関数でラップするのが難しく、別の関数として定義し直して、それをラップするという手順を踏む必要があった。これはアプリケーションの構造自体を変える行為なので影響が大きいし、それを行うにはGoへの理解がないと達成できない。

幸い、先述のkmutoさんのブログの通り、Goはzero-code計装という手段があるので、とりあえずやってみたいという場合には、まずはzero-code計装をやってみるのがよさそうだ。

MackerelにLambdaのトレースをADOTで送信する

Pythonのコードを実行するLambdaのトレースを、ADOT(AWS Distro for OpenTelemetry)を使用してMackerelに送信するための手順です。

Lambda関数の作成

簡単なPythonのコードを実行する関数を作成する。すでにある場合は不要。

関数の作成

関数を作成したら、設定画面のコードタブに適当なコードを貼り付けてDeployする。コードの内容は省くがGeminiに作ってもらった。

ADOTレイヤーの追加

トレースを送信するためのADOTレイヤーを関数に追加する。コードタブの下の方にレイヤーを追加する箇所がある。

レイヤーの追加

レイヤーの追加をクリックし、ARNを指定する。

レイヤーを選択

ここに指定するARNは AWS Distro for OpenTelemetry Lambda Support For Python で確認できる。arn:aws:lambda:ap-northeast-1:901920570463:layer:aws-otel-python-amd64-ver-1-29-0:2 という名前からわかるように、ap-northeast-1かつPython向けのものになっている。リージョンが違う場合はリージョンを置き換える。他の言語のものを使用する場合は Getting Started with AWS Lambda layers にあるリンクを参照すればよさそう。

Lambda サービストレースの有効化

AWS Distro for OpenTelemetry Lambda Support For PythonEnable auto-instrumentation for your Lambda function に記載の通り、アクティブトレースを有効にする。

設定タブ > モニタリングおよび運用ツール > その他の監視ツールの編集ボタンをクリックする。

その他の監視ツール

CloudWatch アプリケーションシグナルと AWS X-RayのLambda サービストレースの有効化にチェックを入れて保存する。

Lambdaサービストレース

環境変数の設定

トレースの送信先やMackerle APIキーなどを環境変数で設定する。設定タブの環境変数をクリックする。

環境変数

ここで以下の環境変数を設定する。

キー
AWS_LAMBDA_EXEC_WRAPPER /opt/otel-instrument
OTEL_EXPORTER_OTLP_PROTOCOL http/protobuf
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT https://otlp-vaxila.mackerelio.com/v1/traces
OTEL_EXPORTER_OTLP_TRACES_HEADERS Accept=*/*,Mackerel-Api-Key=<Mackerel APIキー>
OTEL_RESOURCE_ATTRIBUTES(任意) service.namespace=<サービスネームスペース名>

AWS_LAMBDA_EXEC_WRAPPERは言語によって異なる模様。Enable auto-instrumentation for your Lambda function に記載の通り、Pythonの場合は/opt/otel-instrumentになる。

なお、スパンに含まれるservice.nameはLambdaの関数名になる。OTEL_RESOURCE_ATTRIBUTESservice.nameを指定しても変わらなかったが、別の方法で変更できるのかもしれない。service.namespaceはちゃんと反映される。

テスト実行

テストタブでテストを実行する。

トレースを確認する

Mackerelにトレースが送信されていることを確認した。

トレース詳細画面

おまけ

同じVPC内のRDSにSELECTクエリを投げるだけのコードを実行した時のトレースの様子。SELECTのスパンが表示されているし、データベースパフォーマンス画面にもクエリが表示されている。

トレース詳細画面

データベースパフォーマンス画面

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

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

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

4年目

転職して早3年が過ぎた。

転職のきっかけはビズリーチのスカウトメールだった。

そんなビズリーチからドンペリが届いた。

5月に、"ビズリーチ15周年記念 「ドン ペリニヨン」進呈のお知らせ【メールを受け取った方限定】"というメールが来て、内容はアンケートにご協力くださいというものだった。

第一印象は「あやしい...」だったけど、SNSなどを調べてみるとガチっぽかったのでアンケートに回答したところ、ちゃんと届いたのである。

ありがたくいただこうと思う。

例のブツ

大腸カメラ検査を受けた

3行まとめ

  • 健康診断で便潜血反応陽性(D判定)だった
  • 検査自体は楽だけどそれまでの準備が大変
  • 脂はおいしい

健康診断で便潜血反応陽性(D判定)だった

予選通過ってやつです。大腸内にポリープができると便に血が混ざる可能性が高まって、1cm程度だと50%ぐらい、2cm程度になるとほぼ100%血が混ざるらしい。なので検便は2回採取するようになっているんだな。ポリープは放置すると大腸がんを発症してしまう。心がざわざわする。

検査3日前からの食事制限

消化器内科を受診した結果、大腸カメラ検査をやることになった。この検査は検査当日の3日前から脂っこいもの、繊維質なもの、消化に悪いものを控えなければいけない。自分は朝食にベースブレッドのパンを脳死で食べているのだけど、全粒粉のパンはよくないらしいので食べられなかった。朝食に何を食べたらいいのかわからなくなったので、食べるのをやめた(実際にはヨーグルトだけ食べた)。朝食以外は、白米、納豆、卵、まぐろの刺身、うどんを食べて過ごし、たまにプリンを食べた。炭水化物、タンパク質中心である。

何を食べてよいのかは看護師さんがある程度説明してくれるけど、一応このサイトも参考にさせてもらった。

www.yokohama-naishikyo.com

検査当日

検査は11:00からなのだけど、大腸の中を空っぽにするために、朝7時ぐらいから1.8Lの下剤を2時間程度かけて少しづつ飲む必要がある。粉の入った大きなパウチみたいなやつを渡され、自宅でそこに水を入れるとポカリみたいな液体ができあがる。それを10分間隔で200mlずつ飲んだ。味は悪くない。飲み始めて1時間ぐらいは何も変化がないのだけど、1時間経ったころから便意を催し、時間が経つにつれ、排便の頻度は上がっていく。便は徐々に固体から液体に変わるとともに、色が薄くなっていく。トイレに何度も行くので下剤を飲むペースが乱れる。家を出なければいけない時間までにおさまる気がしなくて焦ったけど、なんとか落ち着いたので病院へ向かう。夏の日差しがまぶしい。

検査の時のことはあまり覚えていない

病院に着いてからまずは血圧の検査。特に異常なし。その後、検査用の服に着替える。お尻の部分に穴の開いた紙のパンツを履く。下半身丸出しになる覚悟で来たけど、ここからカメラを入れるんだなと理解する。その後、腕にチューブの付いた針を刺される。なかなか痛い。これは検査時に静脈麻酔を入れるためのもの。

しばらくしていよいよ検査室に通され、ひととおり説明を受ける。ベッドに横になり、「麻酔を入れ始めたら部屋を暗くしますね~」と言われる。麻酔が入ってきた感じはしなかったけど、周りが暗くなったなぁと思ってからの記憶がない。気づけば検査は終わっていて、検査は10分ぐらいかかるという話だったけど、何も実感がない。

事前に、麻酔の影響でぼーっとするとは言われていたので、お酒に酔った感じかな?とか想像していたけど、ぼーっとするどころの話ではなかった。人によって効きやすさに違いはあるようだけど、自分にはめちゃくちゃ効くようだ。え、本当に終わったの?

検査の結果は問題なし

検査後は特にぼーっとする感じはなく、意識ははっきりとしていた。ちょっとお腹が張った感じがするけど、痛みはない。最後に先生と大腸カメラの写真を見ながら、ポリープなどはなく、何も問題ないことを告げられる。お尻を拭きすぎて肛門あたりが切れて、血が混ざるということはあるらしい。とりあえず何もなくてよかった。無事、決勝(?)敗退です。

エピローグ

検査が終わり、なんでも食べてよくなったので、お昼にミスタードーナツを食べた。夜はマクドナルドをウーバーイーツで頼んで食べた。脂中心である。脂はおいしい。

検索

はてなに入社して1年と7ヶ月ぐらい経つので、自分のMackerel利用歴もそれと同じぐらいなんだけど、最近になってはじめて知ったMackerelの機能がある。

いや、本当は知っていたけど忘れていたのかもしれない。

少なくともこの機能を使った記憶がない。

なんと、Webコンソールの左上には検索ボックスがあって、ここからホストをキーワードで検索できるのだ。

これだ。

試しに linux というキーワードで検索してみる。

ちゃんとホストの管理名にも反応してくれる。

便利だ。

ホスト一覧画面にはサービスやロールでフィルターをかける機能はある。

だけど、検索ボックスはない。あったらいいなと思っていた。

なぜ今まで気づかなかったんだ。