Mackerelでログ監視によく使われるcheck-logプラグインは、毎分ごとにログを見て追記された中で、検出対象文字列を発見したときにアラートを発報し、発見されなくなったら正常に戻ったものと見て自動クローズする、というのがデフォルトの挙動となっている。つまり、ずっと出続けるメッセージのようなものでなければ、発報のだいたい1分後にそれがクローズされることになる。
これが困るというときには、prevent_alert_auto_close = true
をプラグイン設定に追加する方法がある(チェック監視項目を追加するを参照)。これは自動クローズをしないことを宣言するオプションで、クローズはユーザーの手動作業に委ねられる。
しかし、こうした手動対処すべきアラートが蓄積すると、アラートの表示が日常になって注意を払わなくなってしまう。
ということでシェル芸人として、prevent_alert_auto_close = true
が使われている環境向けに、発報から一定時間過ぎたアラートをばっさりクローズするLinux用シェルスクリプトを書いてみた。そんなに長くないので全文掲載。Gistにも置いている。
#!/bin/bash CLOSE_AFTER_MINUTES="60" CLOSE_CHECK_ONLY=true DRYRUN= if [ -z "$(mkr --version 2>/dev/null)" ]; then echo "ERROR: Missing mkr. Install mkr package." exit 1 fi if [ ! -f "/etc/mackerel-agent/mackerel-agent.conf" -a -z "$MACKEREL_APIKEY" ]; then echo "ERROR: Define MACKEREL_APIKEY." exit 1 fi if [ "$CLOSE_CHECK_ONLY" ]; then RESULT=$(mkr alerts -jq '.[] | select(.type == "check") | [.id, .openedAt] | @tsv') else RESULT=$(mkr alerts -jq '.[] | [.id, .openedAt] | @tsv') fi echo "$RESULT" | { CLOSES= while read line; do # alertID openedAt array=($line) # bash if [ "$(date +"%s")" -ge "$(expr ${array[1]} + $CLOSE_AFTER_MINUTES \* 60)" ]; then CLOSES="$CLOSES ${array[0]}" fi done if [ "$CLOSES" ]; then $DRYRUN mkr alerts close --reason "auto closed because more than ${CLOSE_AFTER_MINUTES} minutes passed." $CLOSES fi }
頭のほうで設定用の変数定義をしているので、必要に応じて適宜変更していただきたい(格好良くコマンドラインオプションにしてもよいだろう)。
CLOSE_AFTER_MINUTES="60"
: 単位は分。アラートがオープンされて以来この分数が過ぎていたらクローズする。1日なら1440。CLOSE_CHECK_ONLY=true
: チェック監視のアラートだけを対象にする。「CLOSE_CHECK_ONLY=
」とするとほかの監視ルールも含むすべてのアラートが対象になる。実際のところ、check-log以外のものはメトリック閾値なりチェック結果なりに問題があれば再発報されるはずなので、それでもよいのかもしれない。DRYRUN=
: ドライラン指定。実際にはクローズせず、実行内容を表示するだけにするには「DRYRUN=echo
」とする。実際の実行にはmkr
コマンドを使っている。現在の環境のものではなく別のオーガニゼーションに対して実行するのであれば、MACKEREL_APIKEY
環境変数も事前に指定しておこう。
$ ./deferclose.sh
または(別のオーガニゼーションに対して実行する場合)
$ MACKEREL_APIKEY=〜 ./deferclose.sh
棚卸しタイミングに限らず、適当にcronで回してもよいだろう。
以下は余談というかコードの説明。中身としてはだいぶシンプルで、少しシェル芸を込めている。
mkr alerts
コマンドでオープン中のアラート一覧JSONを取得する。mkr
コマンドはgo-jqを内包しているので、jq記法でJSONを整形できる。アラートIDと日付を取り出し(チェック監視だけの場合はtypeがcheckのもののみにフィルタする)、TSV化して出力する。
echo "$RESULT"
のように""
で囲んでいるのは改行を再現するため。
クローズにはmkr alerts close
を利用できるが、複数のアラートを一度でクローズするためにCLOSES
という変数を作っている。パイプ(|
)でつないだ先がサブシェルになるので、CLOSES
を構成して利用する部分はすべてその中で完結するよう{ }
でグループ化している。この挙動はわりとハマリポイントだねぇ(結局bash依存になっているので、lastpipe
指定すれば済むだけだったかも)。
array=($line)
でtsvから受けたタブ区切り文字列を分解している。ここがbash専用様になってしまった。TSVもCSVも微妙に扱いづらい(mkr jqのCSVには""
が含まれる)。
時間は、頻出テクニックのdate +"%s"
によるUnixエポック秒と、expr
コマンドを使った計算結果で比較している。expr
にはCLOSE_AFTER_MINUTES
変数の内容をそのまま展開させているので、実は式を変数内に含めてしまうこともできる(エスケープも考えないといけないが)。
シェル芸いろいろ便利だね。
MackerelでのOpenTelemetry対応パブリックベータの提供が開始したので、Mackerel CREの私も習熟すべくいろいろと実験をしています。
ホストやミドルウェアのメトリックを取得しようというときにはOpenTelemetry CollectorのReceiverでメトリックを収集し、ExporterにMackerelのOTLPエンドポイントを示して投稿、というのが王道なのですが、今回はあえて「Mackerelの既存のメトリックプラグインの出力をOpenTelemetryのメトリックとして送ってみる」ということを試してみました。
結論から言えば、(加工は少し必要ですが)ContribにあるCarbon Receiverで実現できます。
OpenTelemetry Collector ContribのReceiverにひととおり目を通した中で、「Carbon Receiver」というものがありました。
carbonreceiver: Carbon Receiver beta、メトリクス GraphiteのコンポーネントであるCarbonのプレインテキストプロトコルのメトリクスを取得する。
実はMackerelのメトリックはこのCarbon形式にほぼ同じで、「メトリック名 (タブ) メトリック値 (タブ) UNIX時間」という構成になっています(参照: 「ホストのカスタムメトリックを投稿する」)。
つまり、このCarbon Receiverを起動して待ち受けさせ、任意のMackerelメトリックプラグインの実行結果を送れば、OpenTelemetryメトリックへの変換および投稿をReceiverとCollectorに任せられるというわけですね。
Collectorセットアップについては「Mackerel で OpenTelemetry のラベル付きメトリックを使ってみよう」などに詳しいので、ここではざっくり説明のみに留めます。
CollectorはDockerイメージやdebパッケージなども提供されていますが、試行錯誤段階ではバイナリをフォアグラウンド実行するだけのほうがテストしやすいので、リリースページからlinux_amd64のtar.gzアーカイブを取得して展開して使うことにしました。
$ tar xf otelcol-contrib_0.97.0_linux_amd64.tar.gz $ ./otelcol-contrib components ... receivers: ... - name: carbon stability: logs: Undefined metrics: Beta traces: Undefined ...
次に、Mackerel投稿のためのYAMLファイルを書きます。ファイル名は何でもよいのですが、雑にconfig.yamlとして進めます。
receivers: carbon: endpoint: localhost:2003 transport: udp processors: batch: exporters: otlp/mackerel: endpoint: otlp.mackerelio.com:4317 compression: gzip headers: Mackerel-Api-Key: ${env:MACKEREL_APIKEY} debug: verbosity: detailed service: pipelines: metrics: receivers: [carbon] processors: [batch] exporters: [otlp/mackerel, debug]
Carbon Receiverのデフォルトでは、エンドポイント(endpoint
)が0.0.0.0:2003、トランスポートプロトコル(transport
)がtcpですが、これを変更しました。前者は単にローカルのホスト内でしか使わないためというだけなので、ほかのホストからも投稿するような構成であれば、適切なバインド範囲にするのがよいでしょう。TCPでなくUDPに変えたのは、このCarbon Receiverは送信者に対して何も反応を返さないし、メトリックの投稿保証はさほど意味がなく、投げっぱなしでさっさと終わらせたほうが妥当であろうという判断です。
batch
については本番ではバッチ処理にとりまとめることが推奨されていますが、今の時点では送られたらすぐにログ表示やMackerel上で見たいので、設定を空にしています。
exporters
ではMackerelのOTLPエンドポイントに送るotlp/mackerel
とともに、コンソールに状態を出力するdebug
を定義しています。
では、環境変数MACKEREL_APIKEY
にMackerelオーガニゼーションの書き込み可能APIキーを設定し、otelcol-contribを実行します。
$ export MACKEREL_APIKEY=<APIキー> $ ./otelcol-contrib --config config.yaml 2024-03-27T20:23:28.111+0900 info service@v0.97.0/telemetry.go:55 Setting up own telemetry... ... 2024-03-27T20:23:28.112+0900 warn localhostgate/featuregate.go:63 The default endpoints for all servers in components will change to use localhost instead of 0.0.0.0 in a future version. Use the feature gate to preview the new default. {"feature gate ID": "component.UseLocalHostAsDefaultHost"}
これでCarbon ReceiverがUDPポート2003で待ち受ける状態になりました。
$ lsof -i udp:2003 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME otelcol-c 3100447 kmuto 9u IPv4 14363690 0t0 UDP localhost:2003
ではさっそく投稿してみましょう。
Mackerelメトリックプラグインなら何でもよいですが、ContribのReceiverにはなさそうなものとして、Linuxのnf_conntrackのメトリックを取得するmackerel-plugin-conntrackを使ってみることにします。
$ mackerel-plugin-conntrack conntrack.count.used 71 1711049726 conntrack.count.free 262073 1711049726
ncコマンドで雑にこれをCarbon Receiverに向けます。
$ mackerel-plugin-conntrack | nc -u -w 1 localhost 2003
-u
でUDP、-w
でタイムアウトを指定しています(UDPだとCarbon Receiver側からクローズすることもないので、送信者側で終わらせます)。
しかし、これを実行してもCollector側には何も反応がありません……おやおや?
何かおかしいので、大元のGraphiteのサンプルで試してみることにします。
$ echo "local.random.diceroll 4 `date +%s`" | nc -u -w 1 localhost 2003
おやおや、これは正しく送られますね。
2024-03-27T20:36:49.324+0900 info MetricsExporter {"kind": "exporter", "data_type": "metrics", "name": "debug", "resource metrics": 1, "metrics": 1, "data points": 1} 2024-03-27T20:36:49.324+0900 info ResourceMetrics #0 Resource SchemaURL: ScopeMetrics #0 ScopeMetrics SchemaURL: InstrumentationScope Metric #0 Descriptor: -> Name: local.random.diceroll -> Description: -> Unit: -> DataType: Gauge NumberDataPoints #0 StartTimestamp: 1970-01-01 00:00:00 +0000 UTC Timestamp: 2024-03-27 11:36:49 +0000 UTC Value: 4 {"kind": "exporter", "data_type": "metrics", "name": "debug"}
ここで、最初に以下のように書いていました。
MackerelのメトリックはこのCarbon形式にほぼ同じで、「メトリック名 (タブ) メトリック値 (タブ) UNIX時間」という構成になっています
「ほぼ」が曲者で、GraphiteのCarbon形式はスペース区切りなのに対し、Mackerelはタブ区切りです。いかにも怪しいですね。
タブをスペースに変更する手段はいくつかありますが、ここではシンプルにexpandコマンドでタブ1文字を1スペースに変換してみましょう。
$ mackerel-plugin-conntrack | expand -t 1 | nc -u -w 1 localhost 2003
来ましたね。
2024-03-27T20:50:39.130+0900 info ResourceMetrics #0 Resource SchemaURL: ScopeMetrics #0 ScopeMetrics SchemaURL: InstrumentationScope Metric #0 Descriptor: -> Name: conntrack.count.used -> Description: -> Unit: -> DataType: Gauge NumberDataPoints #0 StartTimestamp: 1970-01-01 00:00:00 +0000 UTC Timestamp: 2024-03-27 11:50:39 +0000 UTC Value: 108 Metric #1 Descriptor: -> Name: conntrack.count.free -> Description: -> Unit: -> DataType: Gauge NumberDataPoints #0 StartTimestamp: 1970-01-01 00:00:00 +0000 UTC Timestamp: 2024-03-27 11:50:39 +0000 UTC Value: 262036 {"kind": "exporter", "data_type": "metrics", "name": "debug"}
PromQLの内容を「conntrack.count.used
」としたクエリグラフをダッシュボードに配置してみます。
投稿されたメトリックはconntrack.count.used、conntrack.count.freeという一見階層化された名前なのですが、OpenTelemetryのメトリックとして考えると、これは単にそれぞれ固有の名前なだけで関係性がありません。クエリグラフなどでの扱いが不便なので、OpenTelemetryのメトリックらしく多次元のラベルに分解することにします。
これはCarbon Receiverが機能として提供しており、メトリック名を正規表現で分解して任意のラベルにできます。
receivers: carbon: endpoint: localhost:2003 transport: udp parser: type: regex config: rules: - regexp: "(?P<key_svc>conntrack)\\.(?P<key_indicator>[^.]+)\\.(?P<key_allocation>.+)" name_prefix: "conntrack"
type: regex
で正規表現パーサを利用することを宣言し、設定をconfig:
の下のrules:
に並べていきます。ルールは最初にマッチしたものが採用されます。
各ルールではregexp
で正規表現、name_prefix
でOpenTelemetryのメトリック名プレフィクスを指定します。regexp
の中では?P<key_〜>
によってラベル名を表し、値はそのマッチ文字列となります(〜部分がラベル名と解釈されます)。つまり、この正規表現ではconntrack.count.usedは{svc='conntrack', indicator='count', allocation='used'}
というラベル・値に分割されます。
otel-contribを実行し直し、メトリックを投稿します。
Metric #0 Descriptor: -> Name: conntrack -> Description: -> Unit: -> DataType: Gauge NumberDataPoints #0 Data point attributes: -> svc: Str(conntrack) -> indicator: Str(count) -> allocation: Str(used)
それっぽく分割されているようです。Mackerelのクエリグラフも変更してみましょう。今度はPromQLの内容を「conntrack
」とします。allocation
ラベルの値であるusedとfreeの2つの系列が存在するグラフになるので、見やすくなるよう「凡例」の値に「allocation」、「表示」に「{{allocation}}」を指定しておきます。
見事に2つの系列が表示されました!
実際の定期投稿のためには、cronなどを使って1分ごとに実行するのがよいでしょう。コマンドライン上で簡単に試すのであれば、たとえば以下のようにsleepコマンドを使えます。
while /bin/true; do date mackerel-plugin-conntrack |expand -t 1 | nc -u -w 1 localhost 2003 sleep 60s done
さて、スペース区切りで処理されているためにexpandを挟むような特別な措置が必要でしたが、Carbon Receiverを改変してタブも許容するようにするのはいかにも簡単そうです。
どこかな、とコードを探ったところ、以下のようにどストレートな処理をすぐに見つけました(path_parser_helper.go)。
parts := strings.SplitN(line, " ", 4)
タブも含む任意スペース区切りに変えればよさそうですね。
re := regexp.MustCompile(`\s+`) parts := re.Split(line, 4)
OpenTelemetry Collectorのちょっとわかりにくい概念として(そして今回のようにCarbon Receiverを使ってみようとした動機として)、改変をするにはCollectorごとビルドし直す必要があります。
ついでにCarbon Receiverと最低限のコンポーネントだけになるようにしてみます。
まずはReceiverの該当コードを変更します。
$ git clone https://github.com/open-telemetry/opentelemetry-collector-contrib.git $ cd opentelemetry-collector-contrib (receiver/carbonreceiver/protocol/path_parser_helper.go に上記の改変を施す)
ビルドし直しについてはMackerelの「OpenTelemetry コレクターでホストメトリックを Mackerel に送信する」でも紹介がありますね。
OpenTelemetry Collector Builderをインストールします。
$ go install go.opentelemetry.io/collector/cmd/builder@latest
次にマニフェストファイルを用意します。こちらも名前は何でもよいのですが、manifest.yamlとしておき、opentelemetry-collector-contribフォルダ内に配置することにしました。
dist: name: otelcol-custom description: OTel Collector with Carbon Receiver modified for Mackerel output_path: ./dest receivers: - gomod: go.opentelemetry.io/collector/receiver/otlpreceiver latest - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/carbonreceiver latest processors: - gomod: go.opentelemetry.io/collector/processor/batchprocessor latest exporters: - gomod: go.opentelemetry.io/collector/exporter/otlpexporter latest - gomod: go.opentelemetry.io/collector/exporter/debugexporter latest replaces: - github.com/open-telemetry/opentelemetry-collector-contrib/receiver/carbonreceiver => ../receiver/carbonreceiver
dist:
で生成物類を設定し、receivers:
、processors:
、exporters:
にはCollectorに含めたいコンポーネントを指定します。
そして、replaces:
ではcarbonreceinverフォルダの実際の場所を変更しています。「..
」としているのは、Collectorがdist
で指定しているoutput_path
のフォルダ内でビルドされる都合上、そこからの相対位置で見る必要があるからです。
ビルドはこのYAMLファイルを指定するだけです。
$ builder --config manifest.yaml
destフォルダにotelcol-customバイナリができます。otelcol-contribのほうは終了させて、otelcol-customバイナリでconfig.yamlを使って試してみましょう(以下ではホームにconfig.yamlがあるものとしています)。
$ dest/otelcol-custom --config ~/config.yaml
この版のCollectorであれば、タブ区切りのまま送信しても、メトリックとして受け付けられます!
$ mackerel-plugin-conntrack | nc -u -w 1 localhost 2003
パッチとしてPR提出しようかと思ったものの、タブ区切りのCarbon形式を使っているほかのサービスが思いつかないので、二の足を踏んでいます。いっそforkしてmackerelreceiverにしてしまうほうがよいのかもしれません。
ということで、Carbon Receiverを使えば、Mackerelメトリックプラグインの出力をOpenTelemetryのエコシステム(Mackerelも含む)に乗せられます。作り込んだメトリックプラグインが存在する場面での手法の一案としていかがでしょうか。
なお、mackerel-agentの出力ごと持っていけないものかというチャレンジも少ししてみたのですが、APIサーバーに問い合わせるロジック(idファイル生成など)がいくつか存在するため、そのあたりをうまく吸収できないと難しそうではありました。mackerelagentreceiverを実装してみるのも面白いかもしれませんね。