MackerelでのOpenTelemetry対応パブリックベータの提供が開始したので、Mackerel CREの私も習熟すべくいろいろと実験をしています。
mackerel.io
ホストやミドルウェアのメトリックを取得しようというときにはOpenTelemetry CollectorのReceiverでメトリックを収集し、ExporterにMackerelのOTLPエンドポイントを示して投稿、というのが王道なのですが、今回はあえて「Mackerelの既存のメトリックプラグインの出力をOpenTelemetryのメトリックとして送ってみる」ということを試してみました。
結論から言えば、(加工は少し必要ですが)ContribにあるCarbon Receiverで実現できます。
Carbon Receiverとは
OpenTelemetry Collector ContribのReceiverにひととおり目を通した中で、「Carbon Receiver」というものがありました。
carbonreceiver: Carbon Receiver
beta、メトリクス
GraphiteのコンポーネントであるCarbonのプレインテキストプロトコルのメトリクスを取得する。
実はMackerelのメトリックはこのCarbon形式にほぼ同じで、「メトリック名 (タブ) メトリック値 (タブ) UNIX時間」という構成になっています(参照: 「ホストのカスタムメトリックを投稿する」)。
つまり、このCarbon Receiverを起動して待ち受けさせ、任意のMackerelメトリックプラグインの実行結果を送れば、OpenTelemetryメトリックへの変換および投稿をReceiverとCollectorに任せられるというわけですね。
OpenTelemetry 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側には何も反応がありません……おやおや?
Carbon形式といったな、あれは嘘だ
何かおかしいので、大元の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
Carbon Receiverを改変する
さて、スペース区切りで処理されているために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を実装してみるのも面白いかもしれませんね。
Happy Hacking!