はじめに

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_spanTracer.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.hrlspan レコード構造を説明し、各フィールドの意味を解説しています。)

{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.plugphoenix.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_spanTracer.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.srcsrc/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 つ以上のテレメトリーバックエンドにテレメトリーデータをエクスポートするために、適切なエクスポーターを設定することも必要です。