Grafana Alloy で OpenTelemetryシグナルを収集する
はじめに
平素は大変お世話になっております。
クイックガードのパー子です。
先日、Grafana Labs から Grafana Alloy という OpenTelemetryコレクタがリリースされました。
これは従来のコレクタ実装である Grafana Agent Flow の後継とされています。
(Alloy のリリースに伴い、Agent は Deprecated となりました。)
早速 Alloy を使って OpenTelemetry の シグナル である Traces、Metrics、Logs を収集してみました。
OpenTelemetryバックエンド
テレメトリ・データの格納先として、SigNoz を使います。
Docker の Composeファイル が用意されているので、これを使ってシュッと立ち上げます。
$ docker compose -f docker/clickhouse-setup/docker-compose.yaml up --scale hotrod=0 --scale load-hotrod=0
補足
この Composeファイルは Hot R.O.D. というテレメトリ・データ生成用のデモ・アプリを含んでいます。
起動すると延々と無駄なデータを投入し続けてうるさいので、--scale
を 0 にして起動しないようにしています。
代わりに自分でデータ生成用アプリを作成することにします。
起動後、http://localhost:3301/ にアクセスすると Adminアカウントの作成を求められます。
適当に作成しましょう。
なお、データ・ストアである signoz-clickhouse
コンテナがちょいちょい Warning や Error を吐いていますが、検証用途なので気にしないことにします。
Alloy
Alloy もお手軽に Docker で動かします。
compose.yaml
---
services:
alloy:
image: 'grafana/alloy:v1.0.0'
command: [
'run',
'--server.http.listen-addr', '0.0.0.0:12345',
'--storage.path', '/var/lib/alloy/data',
'--stability.level', 'public-preview',
'/etc/alloy/config.alloy'
]
ports:
- '12345:12345'
configs:
- source: 'alloy'
target: '/etc/alloy/config.alloy'
configs:
alloy:
file: './config.alloy'
デモ・アプリケーション
テレメトリ・データを生成するアプリケーションを用意します。
(特に理由はありませんが Rails で作成してみます。)
OpenTelemetry Ruby SDK を Getting Started のとおりに組み込み、これを Alloy と同じ Docker-composeプロジェクトで動かします。
サービス名は app
とし、Alloy から見て http://app:3000/
でアクセスできるようにしておきます。
(さらに、利便性のためホストにもポート3000番を公開しておきます。)
また、Rails を developmentモードで動かした場合、DNSリバインディング対策として Hostヘッダに 強めの制限 が課されます。
これを緩和して Alloy からアクセスできるように、環境変数 RAILS_DEVELOPMENT_HOSTS='app'
をセットしておきます。
Traces
まずは Tracesシグナルを収集してみましょう。
デモ・アプリをいじって、サイコロの出目を Tracesシグナルに乗せるようにします。
app/controllers/dice_controller.rb
class DiceController < ApplicationController
def roll
roll_num = rand(6) + 1
# 出目を Tracesシグナルに乗せる。
OpenTelemetry::Trace.current_span.add_attributes(
'roll' => roll_num
)
render json: roll_num.to_s
end
end
補足
Attributes を追加する際、キーを Symbol にすると怒られてしまいます。
OpenTelemetry::Trace.current_span.add_attributes(
roll: roll_num # NG!
)
キーは文字列でないといけません。
(Ruby的にはちょっと気持ち悪いですが…。)
シグナルを Alloy に送信するには opentelemetry-exporter-otlp
Gem を使用します。
送信先は環境変数 OTEL_EXPORTER_OTLP_ENDPOINT
で指定します。
OpenTelemetry はトランスポート・レイヤとして gRPC と HTTP をサポートしますが、この Gem は HTTP でデータを送っているようです。
トランスポートの種類によって受信ポートが分かれており、HTTP は 4318番を指定します。
(ちなみに gRPC は 4317番です。)
アプリから見た Alloy のホスト名は alloy
なので、当該環境変数は以下のようになります。
OTEL_EXPORTER_OTLP_ENDPOINT='http://alloy:4318'
補足
Exporterリスト には gRPC用の Gem も見られますが、開発中でしょうか? それとも Deprecated なのでしょうか?
RubyGems.org では Yanked となってしまっています。
続いて Alloy側を設定します。
使用するコンポーネントは以下です。
コンポーネント | 用途 |
---|---|
otelcol.receiver.otlp | デモ・アプリからシグナルを受け取る。 |
otelcol.processor.memory_limiter | 使用メモリ量を一定値以下に抑制し、Out of Memory を防ぐ。 |
otelcol.processor.batch | 収集したシグナルを一定量まとめてバッチ処理する。 |
otelcol.exporter.otlp | SigNoz へシグナルを送る。 |
config.alloy
1otelcol.exporter.otlp "default" {
2 client {
3 endpoint = "http://host.docker.internal:4317"
4
5 tls {
6 insecure = true
7 }
8 }
9}
10
11otelcol.processor.batch "default" {
12 timeout = "1s"
13
14 output {
15 traces = [
16 otelcol.exporter.otlp.default.input,
17 ]
18 }
19}
20
21otelcol.processor.memory_limiter "default" {
22 check_interval = "1s"
23
24 limit = "200MiB"
25
26 output {
27 traces = [
28 otelcol.processor.batch.default.input,
29 ]
30 }
31}
32
33otelcol.receiver.otlp "default" {
34 http {}
35
36 output {
37 traces = [
38 otelcol.processor.memory_limiter.default.input,
39 ]
40 }
41}
SigNoz は Alloy とは別の Dockerネットワークで動いているので、ホストを経由してシグナルを送ることにします。
今回は Docker for Mac を使用しているので、コンテナから見たホストのアドレスは host.docker.internal
となります。
また、HTTPS ではないため、tls.insecure = true
として TLS接続を無効にしています。
コンポーネント同士の繋がりを可視化すると以下のようになります。
(http://localhost:12345/graph で確認できます。)
http://localhost:3000/rolldice に何度かアクセスして Tracesシグナルを発行すると、そのデータが SigNoz に蓄積されるようになります。
Services:
Traces:
サイコロの出目もしっかり記録されています。
Metrics
次に、デモ・アプリのヘルスチェックを Metricsシグナルとして取得してみることにします。
最近の Rails であれば、ヘルスチェックのエンドポイントとしてデフォルトで /up
が用意されています。
app/config/routes.rb
Rails.application.routes.draw do
...
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
...
end
このエンドポイントを Prometheus の Blackbox exporter で叩きます。
コンポーネント | 用途 |
---|---|
prometheus.exporter.blackbox | ヘルスチェック・エンドポイントを叩く。 |
prometheus.scrape | エクスポータを駆動してメトリクスを収集する。 |
otelcol.receiver.prometheus | Prometheusメトリクスを OpenTelemetryシグナルへ変換する。 |
併せて、すでに定義済みの OpenTelemetry系コンポーネントの output
に Metricsシグナルを追加します。
config.alloy#diff
--- a 2024-03-26 09:31:03
+++ b 2024-03-26 12:57:08
@@ -13,10 +13,14 @@
output {
traces = [
otelcol.exporter.otlp.default.input,
]
+
+ metrics = [
+ otelcol.exporter.otlp.default.input,
+ ]
}
}
otelcol.processor.memory_limiter "default" {
check_interval = "1s"
@@ -25,10 +29,14 @@
output {
traces = [
otelcol.processor.batch.default.input,
]
+
+ metrics = [
+ otelcol.processor.batch.default.input,
+ ]
}
}
otelcol.receiver.otlp "default" {
http {}
@@ -37,5 +45,33 @@
traces = [
otelcol.processor.memory_limiter.default.input,
]
}
}
+
+otelcol.receiver.prometheus "default" {
+ output {
+ metrics = [
+ otelcol.processor.memory_limiter.default.input,
+ ]
+ }
+}
+
+prometheus.exporter.blackbox "default" {
+ config = "{ modules: { http_2xx: { prober: http, timeout: 5s } } }"
+
+ target {
+ name = "app"
+ address = "http://app:3000/up"
+ module = "http_2xx"
+ }
+}
+
+prometheus.scrape "default" {
+ scrape_interval = "10s"
+
+ targets = prometheus.exporter.blackbox.default.targets
+
+ forward_to = [
+ otelcol.receiver.prometheus.default.receiver,
+ ]
+}
設定ファイル全体は以下のとおりです。
(展開してご覧ください。)
config.alloy
1otelcol.exporter.otlp "default" {
2 client {
3 endpoint = "http://host.docker.internal:4317"
4
5 tls {
6 insecure = true
7 }
8 }
9}
10
11otelcol.processor.batch "default" {
12 timeout = "1s"
13
14 output {
15 traces = [
16 otelcol.exporter.otlp.default.input,
17 ]
18
19 metrics = [
20 otelcol.exporter.otlp.default.input,
21 ]
22 }
23}
24
25otelcol.processor.memory_limiter "default" {
26 check_interval = "1s"
27
28 limit = "200MiB"
29
30 output {
31 traces = [
32 otelcol.processor.batch.default.input,
33 ]
34
35 metrics = [
36 otelcol.processor.batch.default.input,
37 ]
38 }
39}
40
41otelcol.receiver.otlp "default" {
42 http {}
43
44 output {
45 traces = [
46 otelcol.processor.memory_limiter.default.input,
47 ]
48 }
49}
50
51otelcol.receiver.prometheus "default" {
52 output {
53 metrics = [
54 otelcol.processor.memory_limiter.default.input,
55 ]
56 }
57}
58
59prometheus.exporter.blackbox "default" {
60 config = "{ modules: { http_2xx: { prober: http, timeout: 5s } } }"
61
62 target {
63 name = "app"
64 address = "http://app:3000/up"
65 module = "http_2xx"
66 }
67}
68
69prometheus.scrape "default" {
70 scrape_interval = "10s"
71
72 targets = prometheus.exporter.blackbox.default.targets
73
74 forward_to = [
75 otelcol.receiver.prometheus.default.receiver,
76 ]
77}
これでヘルスチェックの結果が SigNoz に蓄積されるようになりますが、このメトリクスを表示するには明示的にダッシュボードを作成する必要があります。
ダッシュボードは SigNoz のサイドメニュー “Dashboards” から作成できます。
Logs
最後に Logsシグナルを収集します。
OpenTelemetry Ruby SDK はデフォルトでデモ・アプリの標準出力を拾って Logsシグナルとして投げつけてくる (※) ようですが、敢えて明示的にログ・ファイルを読み取ってみます。
※ Rails の起動ログやアクセス・ログ、Rails.logger
などで吐いた任意のメッセージが勝手に拾われます。
まずは適当なメッセージをファイルに吐くようにします。
app/controllers/dice_controller.rb
class DiceController < ApplicationController
def roll
roll_num = rand(6) + 1
# 出目を Tracesシグナルに乗せる。
OpenTelemetry::Trace.current_span.add_attributes(
'roll' => roll_num
)
# 雑にファイルに吐く。
Logger.new(
File.join(Rails.root, 'log/dice.log')
).info("サイコロの出目は #{roll_num.to_s} でした。")
render json: roll_num.to_s
end
end
ログ・ファイルのパスは RAILS_ROOT/log/dice.log
です。
Alloy がこのファイルに到達できるように、Alloyコンテナに /logs/app/dice.log
としてマウントしておきます。
Alloy でログ・ファイルを読み込むには Loki系コンポーネントを使用します。
コンポーネント | 用途 |
---|---|
loki.source.file | ファイルからログ・エントリを読み込む。 |
otelcol.receiver.loki | Loki形式のログ・エントリを OpenTelemetryシグナルへ変換する。 |
既存の OpenTelemetry系コンポーネントの output
に Logsシグナルを追加します。
config.alloy#diff
--- a 2024-03-26 12:57:08
+++ b 2024-03-26 15:26:04
@@ -17,10 +17,14 @@
]
metrics = [
otelcol.exporter.otlp.default.input,
]
+
+ logs = [
+ otelcol.exporter.otlp.default.input,
+ ]
}
}
otelcol.processor.memory_limiter "default" {
check_interval = "1s"
@@ -33,10 +37,14 @@
]
metrics = [
otelcol.processor.batch.default.input,
]
+
+ logs = [
+ otelcol.processor.batch.default.input,
+ ]
}
}
otelcol.receiver.otlp "default" {
http {}
@@ -73,5 +81,25 @@
forward_to = [
otelcol.receiver.prometheus.default.receiver,
]
}
+
+otelcol.receiver.loki "default" {
+ output {
+ logs = [
+ otelcol.processor.memory_limiter.default.input,
+ ]
+ }
+}
+
+loki.source.file "default" {
+ targets = [
+ {
+ __path__ = "/logs/app/dice.log",
+ },
+ ]
+
+ forward_to = [
+ otelcol.receiver.loki.default.receiver,
+ ]
+}
設定ファイル全体は以下のとおりです。
config.alloy
1otelcol.exporter.otlp "default" {
2 client {
3 endpoint = "http://host.docker.internal:4317"
4
5 tls {
6 insecure = true
7 }
8 }
9}
10
11otelcol.processor.batch "default" {
12 timeout = "1s"
13
14 output {
15 traces = [
16 otelcol.exporter.otlp.default.input,
17 ]
18
19 metrics = [
20 otelcol.exporter.otlp.default.input,
21 ]
22
23 logs = [
24 otelcol.exporter.otlp.default.input,
25 ]
26 }
27}
28
29otelcol.processor.memory_limiter "default" {
30 check_interval = "1s"
31
32 limit = "200MiB"
33
34 output {
35 traces = [
36 otelcol.processor.batch.default.input,
37 ]
38
39 metrics = [
40 otelcol.processor.batch.default.input,
41 ]
42
43 logs = [
44 otelcol.processor.batch.default.input,
45 ]
46 }
47}
48
49otelcol.receiver.otlp "default" {
50 http {}
51
52 output {
53 traces = [
54 otelcol.processor.memory_limiter.default.input,
55 ]
56 }
57}
58
59otelcol.receiver.prometheus "default" {
60 output {
61 metrics = [
62 otelcol.processor.memory_limiter.default.input,
63 ]
64 }
65}
66
67prometheus.exporter.blackbox "default" {
68 config = "{ modules: { http_2xx: { prober: http, timeout: 5s } } }"
69
70 target {
71 name = "app"
72 address = "http://app:3000/up"
73 module = "http_2xx"
74 }
75}
76
77prometheus.scrape "default" {
78 scrape_interval = "10s"
79
80 targets = prometheus.exporter.blackbox.default.targets
81
82 forward_to = [
83 otelcol.receiver.prometheus.default.receiver,
84 ]
85}
86
87otelcol.receiver.loki "default" {
88 output {
89 logs = [
90 otelcol.processor.memory_limiter.default.input,
91 ]
92 }
93}
94
95loki.source.file "default" {
96 targets = [
97 {
98 __path__ = "/logs/app/dice.log",
99 },
100 ]
101
102 forward_to = [
103 otelcol.receiver.loki.default.receiver,
104 ]
105}
ログ・ファイルに吐かれるメッセージが SigNoz に蓄積されるようになります。
まとめ
Grafana Agent の後継である Grafana Alloy を用いて各種OpenTelemetryシグナルを収集してみました。
使用感は従来の Agent (Flowモード) と変わらず、Terraformライクな様式の設定ファイルやデバッグ用の UI もそのままです。
Alloy のリリースに伴い Agent は Deprecated となったため、弊社も順次 Alloy へ移行する予定です。
今後ともよろしくお願い申し上げます。