入门

欢迎来到 Erlang/Elixir 的 OpenTelemetry 入门指南!本指南将引导您完成安装、配置和导出 OpenTelemetry 数据的基础步骤。

Phoenix

本指南的这一部分将向您展示如何在 Phoenix Web Framework 中开始使用 OpenTelemetry。

先决条件

请确保您已在本地安装了 Erlang、Elixir、PostgreSQL(或您选择的数据库)以及 Phoenix。Phoenix 的 安装指南 将帮助您完成所有必需的设置。

示例应用程序

接下来的示例将引导您创建一个基本的 Phoenix Web 应用程序并使用 OpenTelemetry 进行插装。作为参考,您可以在此处找到您将构建的代码的完整示例:opentelemetry-erlang-contrib/examples/roll_dice

您可以在 opentelemetry-erlang-contrib 示例 中找到更多示例。

初始设置

运行 mix phx.new roll_dice。键入“y”以安装依赖项。

依赖项

我们需要 Phoenix 未自带的一些其他依赖项。

  • opentelemetry_api:包含您将用于插装代码的接口。例如 Tracer.with_spanTracer.set_attribute 定义在此处。
  • opentelemetry:包含实现 API 中定义的接口的 SDK。没有它,API 中的所有函数都将无效(no-ops)。
  • opentelemetry_exporter:允许您将遥测数据发送到 OpenTelemetry Collector 和/或自托管或商业服务。
  • opentelemetry_phoenix:从 Phoenix 创建的 Elixir :telemetry 事件创建 OpenTelemetry Span。
  • opentelemetry_cowboy:从 Phoenix 使用的 Cowboy Web 服务器创建的 Elixir :telemetry 事件创建 OpenTelemetry Span。
# mix.exs
def deps do
  [
    # other default deps...
    {:opentelemetry_exporter, "~> 1.8"},
    {:opentelemetry, "~> 1.5"},
    {:opentelemetry_api, "~> 1.4"},
    {:opentelemetry_phoenix, "~> 2"},
    {:opentelemetry_cowboy, "~> 1"},
    {:opentelemetry_ecto, "~> 1.2"} # if using ecto
  ]
end

最后两项也需要在您的应用程序启动时进行设置

# application.ex
@impl true
def start(_type, _args) do
  :opentelemetry_cowboy.setup()
  OpentelemetryPhoenix.setup(adapter: :cowboy2)
  OpentelemetryEcto.setup([:dice_game, :repo]) # if using ecto
end

另外,请确保您的 endpoint.ex 文件包含以下行

# endpoint.ex
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

我们还需要将 opentelemetry 应用程序配置为临时性的,方法是在项目配置中添加一个 releases 部分。这将确保即使它异常终止,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

(如果格式看起来有些陌生,请不用担心。Span 被记录在 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 记录,当您为首选服务配置导出器时,它们将被序列化并发送。

掷骰子

现在我们将创建 API 端点,允许我们掷骰子并返回 1 到 6 之间的随机数。

# 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 方法来生成我们的随机数。这似乎是一项相当重要的操作,因此为了在我们的跟踪中捕获它,我们需要将其包装在一个 Span 中。

defmodule RollDiceWeb.DiceController do
  use RollDiceWeb, :controller
  require OpenTelemetry.Tracer, as: Tracer

  # ...snip

  defp roll_dice do
    Tracer.with_span("dice_roll") do
      to_string(Enum.random(1..6))
    end
  end
end

最好也能知道它生成了什么数字,所以我们可以将其提取为一个局部变量并将其作为属性添加到 Span 中。

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/etc. 中指向 localhost:4000/api/rolldice,您应该会收到一个随机数作为响应,并在控制台中看到 3 个 Span。

查看完整 Span
*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}}
生成的 Span

在上方的输出中有 3 个 Span。Span 的名称是元组的第六个元素,每个 Span 的详细信息如下

<<"/api/rolldice">>

这是请求中的第一个 Span,也就是根 Span。Span 名称旁边的 undefined 表明它没有父 Span。两个非常大的负数是 Span 的开始和结束时间,以 native 时间单位表示。如果您好奇,可以这样计算毫秒为单位的持续时间:System.convert_time_unit(-576460729491912750 - -576460729549928500, :native, :millisecond)phoenix.plugphoenix.action 将告诉您处理请求的控制器和函数。然而,您会注意到 instrumentation_scope 是 opentelemetry_cowboy。当我们告诉 opentelemetry_phoenix 的 setup 函数我们想使用 :cowboy2 适配器时,它就知道不要创建额外的 Span,而是将属性附加到现有的 cowboy Span。这确保了我们的跟踪数据具有更准确的延迟。

<<"HTTP GET">>

这是对图标的请求,您可以在 'http.target' => <<"/favicon.ico">> 属性中看到。我认为它有一个通用的名称,因为它没有 http.route

<<"dice_roll">>

这是我们添加到私有方法的自定义 Span。您会注意到它只有一个我们设置的属性 roll => 2。您还应该注意到它与我们的 <<"/api/rolldice">> Span 属于同一个跟踪,224439009126930788594246993907621543552,并且其父 Span ID 是 5581431573601075988,这是 <<"/api/rolldice">> Span 的 Span ID。这意味着此 Span 是该 Span 的子项,在您选择的跟踪工具中渲染时将显示在其下方。

下一步

通过对您自己的代码库进行手动插装来丰富您的自动生成插装。这允许您自定义应用程序发出的可观察性数据。

您还需要配置一个合适的导出器,将您的遥测数据导出到一个或多个遥测后端。

Elli

本节展示了如何开始使用 OpenTelemetry 和 Elli HTTP 服务器。

先决条件

确保您已在本地安装了 Erlang 和 rebar3

示例应用程序

接下来的示例将引导您创建一个基本的 Elli Web 应用程序并使用 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 中的所有函数都将无效(no-ops)。
  • opentelemetry_exporter:允许您将遥测数据发送到 OpenTelemetry Collector 和/或自托管或商业服务。
  • opentelemetry_elli:作为 Elli 中间件创建 OpenTelemetry Span。
  • opentelemetry_api_experimental:API 的不稳定部分,包括对指标的支持。
  • opentelemetry_experimental:SDK 的不稳定部分,包括对指标的支持。

这些都已添加,与 elli 一起添加到 rebar3 依赖项中,并将应用程序添加到 Release 中

{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 和实验性 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, #{}}}}]}]},

通过此配置,完成的 Span 和记录的指标将每秒输出到控制台。

Elli 服务器

HTTP 服务器在应用程序的顶层 Supervisor 中启动,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, []}]}]},

然后在处理程序中,处理程序创建的 Span 的名称应更新为与 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).

我们需要最后一段代码来创建 instrument,位于 roll_dice_app.erlROLL_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/etc. 中指向 localhost:3000/rolldice,您应该会收到一个随机数作为响应,并在控制台中看到 3 个 Span 和 1 个指标。

查看完整 Span
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}}

下一步

通过手动插装来丰富您的插装。

您还需要配置一个合适的导出器,将您的遥测数据导出到一个或多个遥测后端。