はじめに
Erlang/Elixir 向け OpenTelemetry のはじめにガイドへようこそ! このガイドでは、OpenTelemetry のインストール、設定、データのエクスポートの基本的な手順を説明します。
Phoenix
このパートでは、Phoenix Web フレームワークで OpenTelemetry を使い始める方法を紹介します。
前提条件
Erlang、Elixir、PostgreSQL(または任意のデータベース)、および Phoenix がローカルにインストールされていることを確認してください。 Phoenix のインストールガイドが、必要なものをすべてセットアップするのに役立ちます。
サンプルアプリケーション
以下の例では、基本的な Phoenix ウェブアプリケーションを作成し、OpenTelemetry で計装する手順を説明します。 参考までに、構築するコードの完全な例はこちらにあります: opentelemetry-erlang-contrib/examples/roll_dice。
追加の例は opentelemetry-erlang-contrib examples にあります。
初期セットアップ
mix phx.new roll_dice を実行します。
依存関係をインストールするために「y」と入力します。
依存関係
Phoenix に含まれていない追加の依存関係がいくつか必要です。
opentelemetry_api: コードを計装するために使用するインターフェイスが含まれています。Tracer.with_spanやTracer.set_attributeなどがここで定義されています。opentelemetry: API で定義されたインターフェイスを実装する SDK が含まれています。 これがないと、API のすべての関数は何も実行しません。opentelemetry_exporter: テレメトリーデータを OpenTelemetry Collector やセルフホストまたは商用サービスに送信できます。opentelemetry_phoenix: Phoenix が作成する Elixir の:telemetryイベントから OpenTelemetry スパンを作成します。opentelemetry_cowboy: Phoenix が使用する Cowboy ウェブサーバーが作成する Elixir の:telemetryイベントから OpenTelemetry スパンを作成します。
# mix.exs
def deps do
[
# その他のデフォルトの依存関係...
{:opentelemetry_exporter, "~> 1.8"},
{:opentelemetry, "~> 1.5"},
{:opentelemetry_api, "~> 1.4"},
{:opentelemetry_phoenix, "~> 2"},
{:opentelemetry_cowboy, "~> 1"},
{:opentelemetry_ecto, "~> 1.2"} # ecto を使用する場合
]
end
最後の2つは、アプリケーション起動時にセットアップする必要があります。
# application.ex
@impl true
def start(_type, _args) do
:opentelemetry_cowboy.setup()
OpentelemetryPhoenix.setup(adapter: :cowboy2)
OpentelemetryEcto.setup([:dice_game, :repo]) # ecto を使用する場合
end
また、endpoint.ex ファイルに以下の行が含まれていることを確認してください。
# endpoint.ex
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
さらに、プロジェクト設定に releases セクションを追加して、opentelemetry アプリケーションを temporary として設定する必要があります。
これにより、OpenTelemetry が異常終了した場合でも、roll_dice アプリケーションが終了しないことが保証されます。
# mix.exs
def project do
[
app: :roll_dice,
version: "0.1.0",
elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
releases: [
roll_dice: [
applications: [opentelemetry: :temporary]
]
],
aliases: aliases(),
deps: deps()
]
end
最後に必要なのはエクスポーターの設定です。
開発環境では、すべてが正しく動作していることを確認するために stdout エクスポーターを使用できます。
OpenTelemetry の traces_exporter を以下のように設定します:
# config/dev.exs
config :opentelemetry, traces_exporter: {:otel_exporter_stdout, []}
これで、新しい mix setup コマンドを使用して、依存関係のインストール、アセットのビルド、データベースの作成とマイグレーションを行えます。
試してみる
mix phx.server を実行します。
すべてがうまくいけば、ブラウザで localhost:4000 にアクセスでき、ターミナルに以下のような行が多数表示されるはずです。
(形式が少し見慣れないものでも心配しないでください。
スパンは Erlang の record データ構造で記録されており、otel_span.hrl が span レコード構造を説明し、各フィールドの意味を解説しています。)
{span,64480120921600870463539706779905870846,11592009751350035697,[],
undefined,<<"/">>,server,-576460731933544855,-576460731890088522,
{attributes,128,infinity,0,
#{'http.status_code' => 200,
'http.client_ip' => <<"127.0.0.1">>,
'http.flavor' => '1.1','http.method' => <<"GET">>,
'http.scheme' => <<"http">>,'http.target' => <<"/">>,
'http.user_agent' =>
<<"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36">>,
'net.transport' => 'IP.TCP',
'net.host.name' => <<"localhost">>,
'net.host.port' => 4000,'net.peer.port' => 62839,
'net.sock.host.addr' => <<"127.0.0.1">>,
'net.sock.peer.addr' => <<"127.0.0.1">>,
'http.route' => <<"/">>,'phoenix.action' => home,
'phoenix.plug' =>
'Elixir.RollDiceWeb.PageController'}},
{events,128,128,infinity,0,[]},
{links,128,128,infinity,0,[]},
undefined,1,false,
{instrumentation_scope,<<"opentelemetry_phoenix">>,<<"1.1.0">>,
undefined}}
これらは生の Erlang レコードであり、お好みのサービス用にエクスポーターを設定するとシリアライズされて送信されます。
サイコロを振る
次に、サイコロを振って 1 から 6 のランダムな数値を返す API エンドポイントを作成します。
# router.ex
scope "/api", RollDiceWeb do
pipe_through :api
get "/rolldice", DiceController, :roll
end
そして、計装なしのシンプルな DiceController を作成します。
# lib/roll_dice_web/controllers/dice_controller.ex
defmodule RollDiceWeb.DiceController do
use RollDiceWeb, :controller
def roll(conn, _params) do
send_resp(conn, 200, roll_dice())
end
defp roll_dice do
to_string(Enum.random(1..6))
end
end
お好みでルートを呼び出して結果を確認してください。
ターミナルにはまだテレメトリーが表示されます。
次は roll 関数を手動で計装してテレメトリーを充実させましょう。
DiceController では、ランダムな数値を生成するプライベートメソッド dice_roll を呼び出しています。
これはかなり重要な操作のようなので、トレースでキャプチャするためにスパンで囲む必要があります。
defmodule RollDiceWeb.DiceController do
use RollDiceWeb, :controller
require OpenTelemetry.Tracer, as: Tracer
# ...省略
defp roll_dice do
Tracer.with_span("dice_roll") do
to_string(Enum.random(1..6))
end
end
end
どの数値が生成されたかも知りたいので、ローカル変数として抽出し、スパンの属性として追加できます。
defp roll_dice do
Tracer.with_span("dice_roll") do
roll = Enum.random(1..6)
Tracer.set_attribute(:roll, roll)
to_string(roll)
end
end
ブラウザ、curl などで localhost:4000/api/rolldice にアクセスすると、レスポンスとしてランダムな数値が返され、コンソールに 3 つのスパンが表示されるはずです。
すべてのスパンを表示
*SPANS FOR DEBUG*
{span,224439009126930788594246993907621543552,5581431573601075988,[],
undefined,<<"/api/rolldice">>,server,-576460729549928500,
-576460729491912750,
{attributes,128,infinity,0,
#{'http.request_content_length' => 0,
'http.response_content_length' => 1,
'http.status_code' => 200,
'http.client_ip' => <<"127.0.0.1">>,
'http.flavor' => '1.1','http.host' => <<"localhost">>,
'http.host.port' => 4000,'http.method' => <<"GET">>,
'http.scheme' => <<"http">>,
'http.target' => <<"/api/rolldice">>,
'http.user_agent' =>
<<"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36">>,
'net.host.ip' => <<"127.0.0.1">>,
'net.transport' => 'IP.TCP',
'http.route' => <<"/api/rolldice">>,
'phoenix.action' => roll,
'phoenix.plug' => 'Elixir.RollDiceWeb.DiceController'}},
{events,128,128,infinity,0,[]},
{links,128,128,infinity,0,[]},
undefined,1,false,
{instrumentation_scope,<<"opentelemetry_cowboy">>,<<"0.2.1">>,
undefined}}
{span,237952789931001653450543952469252891760,13016664705250513820,[],
undefined,<<"HTTP GET">>,server,-576460729422104083,-576460729421433042,
{attributes,128,infinity,0,
#{'http.request_content_length' => 0,
'http.response_content_length' => 1258,
'http.status_code' => 200,
'http.client_ip' => <<"127.0.0.1">>,
'http.flavor' => '1.1','http.host' => <<"localhost">>,
'http.host.port' => 4000,'http.method' => <<"GET">>,
'http.scheme' => <<"http">>,
'http.target' => <<"/favicon.ico">>,
'http.user_agent' =>
<<"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36">>,
'net.host.ip' => <<"127.0.0.1">>,
'net.transport' => 'IP.TCP'}},
{events,128,128,infinity,0,[]},
{links,128,128,infinity,0,[]},
undefined,1,false,
{instrumentation_scope,<<"opentelemetry_cowboy">>,<<"0.2.1">>,
undefined}}
{span,224439009126930788594246993907621543552,17387612312604368700,[],
5581431573601075988,<<"dice_roll">>,internal,-576460729494399167,
-576460729494359917,
{attributes,128,infinity,0,#{roll => 2}},
{events,128,128,infinity,0,[]},
{links,128,128,infinity,0,[]},
undefined,1,false,
{instrumentation_scope,<<"dice_game">>,<<"0.1.0">>,undefined}}
生成されたスパン
上の出力には 3 つのスパンがあります。 スパンの名前はタプルの 6 番目の要素で、それぞれ以下の通りです:
<<"/api/rolldice">>
これはリクエストの最初のスパン、つまりルートスパンです。
スパン名の横にある undefined は、親スパンがないことを示しています。
2 つの非常に大きな負の数はスパンの開始時刻と終了時刻で、native 時間単位です。
興味があれば、以下のようにしてミリ秒単位の期間を計算できます:
System.convert_time_unit(-576460729491912750 - -576460729549928500, :native, :millisecond)。
phoenix.plug と phoenix.action は、リクエストを処理したコントローラーと関数を示します。
ただし、instrumentation_scope が opentelemetry_cowboy であることに気づくでしょう。
opentelemetry_phoenix の setup 関数に :cowboy2 アダプターを使用することを伝えたとき、追加のスパンを作成せず、既存の cowboy スパンに属性を追加するようにしました。
これにより、トレースでより正確なレイテンシーデータが得られます。
<<"HTTP GET">>
これは favicon のリクエストで、'http.target' => <<"/favicon.ico">> 属性で確認できます。
http.route がないため、汎用的な名前になっていると思われます。
<<"dice_roll">>
これは、プライベートメソッドに追加したカスタムスパンです。
設定した 1 つの属性 roll => 2 のみを持っていることがわかります。
また、<<"/api/rolldice">> スパンと同じトレース 224439009126930788594246993907621543552 に属しており、親スパン ID は 5581431573601075988 で、これは <<"/api/rolldice">> スパンのスパン ID です。
つまり、このスパンはそのスパンの子であり、お好みのトレーシングツールでレンダリングされると、その下に表示されます。
次のステップ
自身のコードベースの手動計装で、自動生成された計装を充実させましょう。 これにより、アプリケーションが出力するオブザーバビリティデータをカスタマイズできます。
また、1 つ以上のテレメトリーバックエンドにテレメトリーデータをエクスポートするために、適切なエクスポーターを設定することも必要です。
Elli
このセクションでは、OpenTelemetry と Elli HTTP サーバーを使い始める方法を紹介します。
前提条件
Erlang と rebar3 がローカルにインストールされていることを確認してください。
サンプルアプリケーション
以下の例では、基本的な Elli ウェブアプリケーションを作成し、OpenTelemetry で計装する手順を説明します。 参考までに、構築するコードの完全な例はこちらにあります: opentelemetry-erlang-contrib/examples/roll_dice_elli。 完全な例には、ここでは扱わない HTML インターフェイス用の追加コードがあることに注意してください。
追加の例は Erlang の例のドキュメントにあります。
初期セットアップ
rebar3 new release roll_dice_elli
依存関係
Elli に含まれていない追加の依存関係がいくつか必要です。
opentelemetry_api: コードを計装するために使用するインターフェイスが含まれています。Tracer.with_spanやTracer.set_attributeなどがここで定義されています。opentelemetry: API で定義されたインターフェイスを実装する SDK が含まれています。 これがないと、API のすべての関数は何も実行しません。opentelemetry_exporter: テレメトリーデータを OpenTelemetry Collector やセルフホストまたは商用サービスに送信できます。opentelemetry_elli: Elli ミドルウェアとして OpenTelemetry スパンを作成します。opentelemetry_api_experimental: メトリクスのサポートを含む API の不安定な部分です。opentelemetry_experimental: メトリクスのサポートを含む SDK の不安定な部分です。
これらはすべて elli と共に rebar3 の依存関係と、リリースに含めるアプリケーションに追加されます。
{deps, [elli,
recon,
opentelemetry_api,
opentelemetry_exporter,
opentelemetry,
opentelemetry_elli,
opentelemetry_api_experimental},
opentelemetry_experimental}
]}.
{shell, [{apps, [opentelemetry_experimental, opentelemetry, roll_dice]},
{config, "config/sys.config"}]}.
{relx, [{release, {roll_dice, "0.1.0"},
[opentelemetry_exporter,
opentelemetry_experimental,
opentelemetry,
recon,
roll_dice,
sasl]}
]}.
また、依存関係はアプリケーションの .app.src、src/roll_dice.app.src に含める必要があります。
{application, roll_dice,
[{description, "OpenTelemetry example application"},
{vsn, "0.1.0"},
{registered, []},
{mod, {roll_dice_app, []}},
{applications,
[kernel,
stdlib,
elli,
opentelemetry_api,
opentelemetry_api_experimental,
opentelemetry_elli
]},
{env,[]},
{modules, []},
{licenses, ["Apache-2.0"]},
{links, []}
]}.
設定
SDK と Experimental SDK は config/sys.config で設定します。
{opentelemetry,
[{span_processor, batch},
{traces_exporter, {otel_exporter_stdout, []}}]},
{opentelemetry_experimental,
[{readers, [#{module => otel_metric_reader,
config => #{export_interval_ms => 1000,
exporter => {otel_metric_exporter_console, #{}}}}]}]},
この設定により、完了したスパンと記録されたメトリクスが毎秒コンソールに出力されます。
Elli サーバー
HTTP サーバーは、アプリケーションのトップレベルスーパーバイザー src/roll_dice_sup.erl で起動されます。
init([]) ->
Port = list_to_integer(os:getenv("PORT", "3000")),
ElliOpts = [{callback, elli_middleware},
{callback_args, [{mods, [{roll_dice_handler, []}]}]},
{port, Port}],
ChildSpecs = [#{id => roll_dice_http,
start => {elli, start_link, [ElliOpts]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [roll_dice_handler]}],
{ok, {SupFlags, ChildSpecs}}.
ハンドラー roll_dice_handler には、GET リクエストを受け取りランダムなサイコロの出目を返す handle 関数が必要です。
handle(Req, _Args) ->
handle(Req#req.method, elli_request:path(Req), Req).
handle('GET', [~"rolldice"], _Req) ->
Roll = do_roll(),
{ok, [], erlang:integer_to_binary(Roll)}.
do_roll/0 は 1 から 6 のランダムな数値を返します:
-spec do_roll() -> integer().
do_roll() ->
rand:uniform(6).
計装
計装の最初のステップは、Elli の計装ライブラリ otel_elli_middleware を追加することです。
{callback_args, [{mods, [{otel_elli_middleware, []},
{roll_dice_handler, []}]}]},
次に、ハンドラーで、ハンドラーが作成したスパンの名前を HTTP のセマンティック規約に合わせて更新する必要があります。
handle('GET', [~"rolldice"], _Req) ->
?update_name(~"GET /rolldice"),
Roll = do_roll(),
{ok, [], erlang:integer_to_binary(Roll)}.
handle_event(_Event, _Data, _Args) ->
ok.
%%
-spec do_roll() -> integer().
do_roll() ->
?with_span(dice_roll, #{},
fun(_) ->
Roll = rand:uniform(6),
?set_attribute('roll.value', Roll),
?counter_add(?ROLL_COUNTER, 1, #{'roll.value' => Roll}),
Roll
end).
最後に必要なコードは、roll_dice_app.erl で計装 ROLL_COUNTER を作成することです。
-include_lib("opentelemetry_api_experimental/include/otel_meter.hrl").
start(_StartType, _StartArgs) ->
create_instruments(),
roll_dice_sup:start_link().
create_instruments() ->
?create_counter(?ROLL_COUNTER, #{description => ~"The number of rolls by roll value.",
unit => '1'}).
試してみる
rebar3 shell
ブラウザ、curl などで localhost:3000/rolldice にアクセスすると、レスポンスとしてランダムな数値が返され、コンソールに 3 つのスパンと 1 つのメトリクスが表示されるはずです。
すべてのスパンを表示
roll_counter{roll.value=1} 1
{span,319413853664572622578356032097465423781,9329051549219651155,
{tracestate,[]},
4483837830122616505,true,dice_roll,internal,-576460743866039000,
-576460743861510287, {attributes,128,infinity,0,#{'roll.value' => 1}},
{events,128,128,infinity,0,[]}, {links,128,128,infinity,0,[]},
undefined,1,false,
{instrumentation_scope,<<"roll_dice">>,<<"0.1.0">>,undefined}}
{span,120980994633230227841304483210494731701,17581728945491241369,
{tracestate,[]}, undefined,undefined,<<"GET /">>,server,-576460745567307647,
-576460745552778124, {attributes,128,infinity,0, #{<<"http.flavor">> =>
<<"1.1">>, <<"http.host">> => <<"localhost:3000">>, <<"http.method">> =>
<<"GET">>, <<"http.response_content_length">> => 428, <<"http.status">> =>
200,<<"http.target">> => <<"/">>, <<"http.user_agent">> => <<"Mozilla/5.0 (X11;
Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0">>, <<"net.host.ip">> =>
<<"127.0.0.1">>, <<"net.host.name">> => "rosa",<<"net.host.port">> => 3000,
<<"net.peer.ip">> => <<"127.0.0.1">>, <<"net.peer.name">> =>
<<"localhost:3000">>, <<"net.peer.port">> => 34112, <<"net.transport">> =>
<<"IP.TCP">>}}, {events,128,128,infinity,0,[]}, {links,128,128,infinity,0,[]},
{status,unset,<<>>}, 1,false,
{instrumentation_scope,<<"opentelemetry_elli">>,<<"0.2.0">>,undefined}}
{span,99954316162469909244758406078309269908,7583363800346194390,
{tracestate,[]}, undefined,undefined,<<"HTTP GET">>,server,-576460745388883955,
-576460745387339610, {attributes,128,infinity,0, #{<<"http.flavor">> =>
<<"1.1">>, <<"http.host">> => <<"localhost:3000">>, <<"http.method">> =>
<<"GET">>, <<"http.response_content_length">> => 457642, <<"http.status">> =>
200, <<"http.target">> => <<"/static/index.js">>, <<"http.user_agent">> =>
<<"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0">>,
<<"net.host.ip">> => <<"127.0.0.1">>, <<"net.host.name">> =>
"rosa",<<"net.host.port">> => 3000, <<"net.peer.ip">> => <<"127.0.0.1">>,
<<"net.peer.name">> => <<"localhost:3000">>, <<"net.peer.port">> => 34112,
<<"net.transport">> => <<"IP.TCP">>}}, {events,128,128,infinity,0,[]},
{links,128,128,infinity,0,[]}, {status,unset,<<>>}, 1,false,
{instrumentation_scope,<<"opentelemetry_elli">>,<<"0.2.0">>,undefined}}
{span,319413853664572622578356032097465423781,4483837830122616505,
{tracestate,[]}, 4897145615278856533,true,<<"GET
/rolldice">>,server,-576460743866475748, -576460743861225124,
{attributes,128,infinity,0, #{<<"http.flavor">> => <<"1.1">>, <<"http.host">> =>
<<"localhost:3000">>, <<"http.method">> => <<"GET">>,
<<"http.response_content_length">> => 1, <<"http.status">> => 200,
<<"http.target">> => <<"/rolldice">>, <<"http.user_agent">> => <<"Mozilla/5.0
(X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0">>, <<"net.host.ip">>
=> <<"127.0.0.1">>, <<"net.host.name">> => "rosa",<<"net.host.port">> => 3000,
<<"net.peer.ip">> => <<"127.0.0.1">>, <<"net.peer.name">> =>
<<"localhost:3000">>, <<"net.peer.port">> => 34112, <<"net.transport">> =>
<<"IP.TCP">>}}, {events,128,128,infinity,0,[]}, {links,128,128,infinity,0,[]},
{status,unset,<<>>}, 1,false,
{instrumentation_scope,<<"opentelemetry_elli">>,<<"0.2.0">>,undefined}}
次のステップ
より多くの手動計装で計装を充実させましょう。
また、1 つ以上のテレメトリーバックエンドにテレメトリーデータをエクスポートするために、適切なエクスポーターを設定することも必要です。
フィードバック
このページは役に立ちましたか?
Thank you. Your feedback is appreciated!
Please let us know how we can improve this page. Your feedback is appreciated!