入门指南

欢迎使用Erlang/Elixir的OpenTelemetry入门指南!本指南将带领您逐步完成安装、配置和导出OpenTelemetry数据的基本步骤。

Phoenix

本部分指南将向您展示如何在Phoenix Web框架中开始使用OpenTelemetry。

准备工作

请确保在本地安装了Erlang、Elixir、PostgreSQL(或您选择的数据库)和Phoenix。您可以参考Phoenix的安装指南以获取所需的设置。

示例应用程序

以下示例使用基本的Phoenix Web应用程序。完整的代码示例可以在这里找到: opentelemetry-erlang-contrib/examples/dice_game。您可以克隆该项目或在浏览器中跟随教程。

其他示例可以在此处找到/docs/instrumentation/erlang/examples/

依赖项

我们将需要一些Phoenix没有预装的其他依赖项。

  • opentelemetry_api:包含了您用来为代码仪表化的接口。例如,Tracer.with_spanTracer.set_attribute等功能在这里定义。
  • opentelemetry:包含了实现API中定义的接口的SDK。如果没有此依赖项,API中的所有函数都将不起作用。
  • opentelemetry_exporter:允许您将遥测数据发送到OpenTelemetry收集器和/或自托管或商业服务。
  • opentelemetry_phoenix:从Phoenix创建Elixir的:telemetry事件创建OpenTelemetry跨度。
  • opentelemetry_cowboy:从Cowboy Web服务器(由Phoenix使用)创建OpenTelemetry跨度。
# mix.exs
def deps do
  [
    {:opentelemetry, "~> 1.3"},
    {:opentelemetry_api, "~> 1.2"},
    {:opentelemetry_exporter, "~> 1.6"},
    {:opentelemetry_phoenix, "~> 1.1"},
    {:opentelemetry_cowboy, "~> 0.2"},
  ]
end

最后两个依赖项还需要在应用程序启动时进行设置:

# application.ex
@impl true
def start(_type, _args) do
  :opentelemetry_cowboy.setup()
  OpentelemetryPhoenix.setup(adapter: :cowboy2)
end

如果您使用的是ecto,还需要添加OpentelemetryEcto.setup([:dice_game, :repo])

我们还需要通过向项目配置中添加releases部分将opentelemetry应用程序配置为临时应用程序。这将确保即使应用程序异常终止,dice_game应用程序也会终止。

# mix.exs
def project do
  [
    app: :dice_game,
    version: "0.1.0",
    elixir: "~> 1.14",
    elixirc_paths: elixirc_paths(Mix.env()),
    start_permanent: Mix.env() == :prod,
    releases: [
      dice_game: [
        applications: [opentelemetry: :temporary]
      ]
    ],
    aliases: aliases(),
    deps: deps()
  ]
end

现在我们可以使用新的mix setup命令来安装依赖项,构建资源文件,并创建和迁移数据库。

尝试一下

我们可以通过将stdout输出器设置为opentelemetry的traces_exporter,然后使用mix phx.server命令启动应用程序来确保一切正常运行。

# config/dev.exs
config :opentelemetry, traces_exporter: {:otel_exporter_stdout, []}

如果一切顺利,您应该能够在浏览器中访问localhost:4000,并在终端中看到很多类似于这样的行。

(如果格式看起来有点陌生,不要担心。跨度是以Erlang的record数据结构记录的。您可以在这里找到有关记录的更多信息,这个文件描述了span记录结构,并解释了不同字段的含义。)

*SPANS FOR DEBUG*
{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.DiceGameWeb.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之间的随机数。

在调用API之前,让我们在我们的DiceController中添加我们的第一个手动仪表化。在我们的dice_roll方法中,我们调用一个私有方法dice_roll来生成我们的随机数。这似乎是一个非常重要的操作,为了在我们的跨度中捕获它,我们需要将其包装在一个跨度中。

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

我们还想知道它生成的数字是多少,因此我们可以将其提取为一个局部变量,并将其作为属性添加到跨度中。

defp dice_roll 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个跨度。

查看完整的跨度
*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.DiceGameWeb.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}}
<<"/api/rolldice">>

这是请求中的第一个跨度,也称为根跨度。跨度名称旁边的undefined告诉您它没有父跨度。两个非常大的负数是跨度的开始时间和结束时间,以native时间单位表示。如果您感兴趣,可以计算以毫秒为单位的持续时间,方法是 System.convert_time_unit(-576460729491912750 - -576460729549928500, :native, :millisecond)phoenix.plugphoenix.action将告诉您处理请求的控制器和函数。但是请注意,仪表化范围是opentelemetry_cowboy。当我们告诉opentelemetry_phoenix的设置函数我们要使用:cowboy2适配器时,它会知道不要创建任何额外的跨度,而是将属性附加到现有的cowboy跨度中。这确保我们的跨度中有更准确的延迟数据。

<<"HTTP GET">>

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

<<"dice_roll">>

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

接下来的步骤

通过手动仪表化,丰富您自动生成的仪表化和自己的代码库。这样可以让您自定义应用程序发出的可观测数据。

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

创建一个新的 Mix/Rebar 项目

要开始使用本指南,请使用 rebar3mix 创建一个新的项目:

rebar3 new release otel_getting_started
mix new --sup otel_getting_started

然后,在刚刚创建的项目中,将 opentelemetry_apiopentelemetry 作为依赖添加进去。我们同时添加这两个依赖,因为这是一个我们将以发布版形式运行并从中导出跨度的项目。

{deps, [{opentelemetry_api, "~> 1.2"},
        {opentelemetry, "~> 1.3"}]}.
def deps do
  [
    {:opentelemetry_api, "~> 1.2"},
    {:opentelemetry, "~> 1.3"}
  ]
end

对于 Erlang,还需要将 API 应用程序添加到 src/otel_getting_started.app.src,将 relx 部分添加到 rebar.config。在 Elixir 项目中,需要在 mix.exs 添加一个 releases 部分:

%% src/otel_getting_started.app.src
...
{applications, [kernel,
                stdlib,
                opentelemetry_api]},
...

%% rebar.config
{relx, [{release, {otel_getting_started, "0.1.0"},
         [{opentelemetry, temporary},
          otel_getting_started]},

       ...]}.
# mix.exs
releases: [
  otel_getting_started: [
    version: "0.0.1",
    applications: [opentelemetry: :temporary, otel_getting_started: :permanent]
  ]
]

SDK opentelemetry 应尽早添加到发布版启动过程中,以确保在生成任何遥测之前它可用。在这里,它也被设置为 temporary,假设我们更倾向于运行的发布版不生成遥测而不是导致整个发布版崩溃。

除了 API 和 SDK,需要一个用于导出数据的导出器。SDK 内置一个用于调试目的的导出器,它将数据打印到标准输出,还有用于通过OpenTelemetry Protocol(OTLP)Zipkin 协议进行导出的单独的包。

初始化和配置

配置是通过应用程序环境操作系统环境变量进行的。SDK(opentelemetry 应用程序)使用配置来初始化跟踪提供程序、其跨度处理器导出器

使用控制台导出器

导出器是允许将遥测数据发送到某个位置的包 - 可以是控制台(就像这里所做的),也可以是远程系统或收集器,以进行进一步的分析和/或丰富处理。OpenTelemetry 通过其生态系统支持各种导出器,包括 Jaeger 和 Zipkin 等流行的开源工具。

要配置 OpenTelemetry 使用特定的导出器,例如 otel_exporter_stdout,需要在 opentelemetry 应用程序的应用环境中设置 exporter 用于 span 处理器 otel_batch_processor,它是一种在一段时间内批量处理多个跨度的 span 处理器类型:

%% config/sys.config.src
[
 {opentelemetry,
  [{span_processor, batch},
   {traces_exporter, {otel_exporter_stdout, []}}]}
].
# config/runtime.exs
config :opentelemetry,
  span_processor: :batch,
  traces_exporter: {:otel_exporter_stdout, []}

使用 Spans

现在,依赖项和配置都设置好了,我们可以创建一个带有 hello/0 函数的模块,该函数启动一些 spans:

%% apps/otel_getting_started/src/otel_getting_started.erl
-module(otel_getting_started).

-export([hello/0]).

-include_lib("opentelemetry_api/include/otel_tracer.hrl").

hello() ->
    %% 启动一个活动的 span 并运行一个本地函数
    ?with_span(operation, #{}, fun nice_operation/1).

nice_operation(_SpanCtx) ->
    ?add_event(<<"Nice operation!">>, [{<<"bogons">>, 100}]),
    ?set_attributes([{another_key, <<"yes">>}]),

    %% 启动一个活动的 span 并运行一个匿名函数
    ?with_span(<<"Sub operation...">>, #{},
               fun(_ChildSpanCtx) ->
                       ?set_attributes([{lemons_key, <<"five">>}]),
                       ?add_event(<<"Sub span event!">>, [])
               end).
# lib/otel_getting_started.ex
defmodule OtelGettingStarted do
  require OpenTelemetry.Tracer, as: Tracer

  def hello do
    Tracer.with_span :operation do
      Tracer.add_event("Nice operation!", [{"bogons", 100}])
      Tracer.set_attributes([{:another_key, "yes"}])

      Tracer.with_span "Sub operation..." do
        Tracer.set_attributes([{:lemons_key, "five"}])
        Tracer.add_event("Sub span event!", [])
      end
    end
  end
end

在这个示例中,我们使用了宏,在上下文传播和获取跟踪器方面使用了进程字典。

在我们的函数内部,我们使用 with_span 宏创建了一个名为 operation 的新 span。由于我们没有将上下文作为变量传递进去,宏将新的 span 设置为当前上下文中的 active span - 存储在进程字典中。

Span 可以有属性和事件,这些属性和事件是帮助您在事后解释跟踪的元数据和日志语句。第一个 span 有一个事件 Nice operation!,带有事件上的属性,以及在 span 本身上设置的属性。

最后,在此代码片段中,我们可以看到创建了一个当前活动 span 的子 span 的示例。当 with_span 宏启动新的 span 时,它将当前上下文的活动 span 作为父级。因此,运行此程序时,您将看到 Sub operation... span 被创建为 operation span 的子级。

要测试这个项目并查看创建的 spans,可以使用 rebar3 shelliex -S mix 运行,它们都会使用相应的配置,从而启动跟踪器和导出器。

$ rebar3 shell
===> Compiling otel_getting_started
Erlang/OTP 23 [erts-11.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Eshell V11.1  (abort with ^G)
1>
1> otel_getting_started:hello().
true
*SPANS FOR DEBUG*
{span,177312096541376795265675405126880478701,5706454085098543673,undefined,
      13736713257910636645,<<"Sub operation...">>,internal,
      -576460750077844044,-576460750077773674,
      [{lemons_key,<<"five">>}],
      [{event,-576460750077786044,<<"Sub span event!">>,[]}],
      [],undefined,1,false,undefined}
{span,177312096541376795265675405126880478701,13736713257910636645,undefined,
      undefined,operation,internal,-576460750086570890,
      -576460750077752627,
      [{another_key,<<"yes">>}],
      [{event,-576460750077877345,<<"Nice operation!">>,[{<<"bogons">>,100}]}],
      [],undefined,1,false,undefined}
$ iex -S mix
Erlang/OTP 23 [erts-11.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Compiling 1 file (.ex)
Interactive Elixir (1.11.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> OtelGettingStarted.hello()
true
iex(2)>
*SPANS FOR DEBUG*
{span,180094370450826032544967824850795294459,5969980227405956772,undefined,
      14276444653144535440,<<"Sub operation...">>,'INTERNAL',
      -576460741349434100,-576460741349408901,
      [{lemons_key,<<"five">>}],
      [{event,-576460741349414157,<<"Sub span event!">>,[]}],
      [],undefined,1,false,undefined}
{span,180094370450826032544967824850795294459,14276444653144535440,undefined,
      undefined,:operation,'INTERNAL',-576460741353342627,
      -576460741349400034,
      [{another_key,<<"yes">>}],
      [{event,-576460741349446725,<<"Nice operation!">>,[{<<"bogons">>,100}]}],
      [],undefined,1,false,undefined}

下一步

通过更多的手动仪表化来丰富您的仪表化。

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

如果您想探索更复杂的示例,请查看OpenTelemetry Demo,其中包括基于 Erlang/Elixir 的特性标志服务

最后修改 December 13, 2023: improve glossary translation (46f8201b)