入门
欢迎来到 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_span和Tracer.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.plug 和 phoenix.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_span和Tracer.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.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 和实验性 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.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/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}}
下一步
通过手动插装来丰富您的插装。
您还需要配置一个合适的导出器,将您的遥测数据导出到一个或多个遥测后端。