Getting Started
Welcome to the OpenTelemetry for Erlang/Elixir getting started guide! This guide will walk you through the basic steps in installing, configuring, and exporting data from OpenTelemetry.
Phoenix
This part of the guide will show you how to get started with OpenTelemetry in the Phoenix Web Framework.
Prerequisites
Ensure that you have Erlang, Elixir, PostgreSQL (or the database of your choice), and Phoenix installed locally. The Phoenix installation guide will help you get set up with everything you need.
Example Application
The following example uses a basic Phoenix web application. For reference, a complete example of the code you will build can be found here: opentelemetry-erlang-contrib/examples/dice_game. You can git clone that project or just follow along in your browser.
Additional examples can be found here.
Dependencies
We’ll need a few other dependencies that Phoenix doesn’t come with.
opentelemetry_api
: contains the interfaces you’ll use to instrument your code. Things likeTracer.with_span
andTracer.set_attribute
are defined here.opentelemetry
: contains the SDK that implements the interfaces defined in the API. Without it, all the functions in the API are no-ops.opentelemetry_exporter
: allows you to send your telemetry data to an OpenTelemetry Collector and/or to self-hosted or commercial services.opentelemetry_phoenix
: creates OpenTelemetry spans from the Elixir:telemetry
events created by Phoenix.opentelemetry_cowboy
: creates OpenTelemetry spans from the Elixir:telemetry
events created by the Cowboy web server (which is used by Phoenix).
# 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
The last two also need to be setup when your application starts:
# application.ex
@impl true
def start(_type, _args) do
:opentelemetry_cowboy.setup()
OpentelemetryPhoenix.setup(adapter: :cowboy2)
end
If you’re using ecto, you’ll also want to add
OpentelemetryEcto.setup([:dice_game, :repo])
.
We also need to configure the opentelemetry
application as temporary by adding
a releases
section to your project configuration. This will ensure that if it
terminates, even abnormally, the dice_game
application will be terminated.
# 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
Now we can use the new mix setup
command to install the dependencies, build
the assets, and create and migrate the database.
Try It Out
We can ensure everything is working by setting the stdout exporter as
OpenTelemetry’s traces_exporter
and then starting the app with
mix phx.server
.
# config/dev.exs
config :opentelemetry, traces_exporter: {:otel_exporter_stdout, []}
If everything went well, you should be able to visit
localhost:4000
in your browser and see quite a few
lines that look like this in your terminal.
(Don’t worry if the format looks a little unfamiliar. Spans are recorded in the
Erlang record
data structure. You can find more information about records
here, and
this
file describes the span
record structure, and explains what the different
fields are.)
*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}}
These are the raw Erlang records that will get serialized and sent when you configure the exporter for your preferred service.
Rolling The Dice
Now we’ll check out the API endpoint that will let us roll the dice and return a random number between 1 and 6.
Before we call our API, let’s add our first bit of manual instrumentation. In
our DiceController
we call a private dice_roll
method that generates our
random number. This seems like a pretty important operation, so in order to
capture it in our trace we’ll need to wrap it in a span.
defp dice_roll do
Tracer.with_span("dice_roll") do
to_string(Enum.random(1..6))
end
end
It would also be nice to know what number it generated, so we can extract it as a local variable and add it as an attribute on the span.
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
Now if you point your browser/curl/etc. to
localhost:4000/api/rolldice
you should
get a random number in response, and 3 spans in your console.
View the full spans
*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">>
This is the first span in the request, aka the root span. That undefined
next
to the span name tells you that it doesn’t have a parent span. The two very
large negative numbers are the start and end time of the span, in the native
time unit. If you’re curious, you can calculate the duration in milliseconds
like so
System.convert_time_unit(-576460729491912750 - -576460729549928500, :native, :millisecond)
.
The phoenix.plug
and phoenix.action
will tell you the controller and
function that handled the request. You’ll notice however, that the
instrumentation_scope is opentelemetry_cowboy
. When we told
opentelemetry_phoenix’s setup function that we want to use the :cowboy2
adapter, that let it know not to create and additional span, but to instead
append attributes to the existing cowboy span. This ensures we have more
accurate latency data in our traces.
<<"HTTP GET">>
This is the request for the favicon, which you can see in the
'http.target' => <<"/favicon.ico">>
attribute. I believe it has a generic
name because it does not have an http.route
.
<<"dice_roll">>
This is the custom span we added to our private method. You’ll notice it only
has the one attribute that we set, roll => 2
. You should also note that it is
part of the same trace as our <<"/api/rolldice">>
span,
224439009126930788594246993907621543552
and has a parent span ID of
5581431573601075988
which is the span ID of the <<"/api/rolldice">>
span.
That means that this span is a child of that one, and will be shown below it
when rendered in your tracing tool of choice.
Next Steps
Enrich your automatically generated instrumentation with manual instrumentation of your own codebase. This allows you to customize the observability data your application emits.
You’ll also want to configure an appropriate exporter to export your telemetry data to one or more telemetry backends.
Creating a New Mix/Rebar Project
To get started with this guide, create a new project with rebar3
or mix
:
rebar3 new release otel_getting_started
mix new --sup otel_getting_started
Then, in the project you just created, add both opentelemetry_api
and
opentelemetry
as dependencies. We add both because this is a project we will
run as a Release and export spans from.
{deps, [{opentelemetry_api, "~> 1.2"},
{opentelemetry, "~> 1.3"}]}.
def deps do
[
{:opentelemetry_api, "~> 1.2"},
{:opentelemetry, "~> 1.3"}
]
end
In the case of Erlang, the API Application will also need to be added to
src/otel_getting_started.app.src
and a relx
section to rebar.config
. In an
Elixir project, a releases
section needs to be added to mix.exs
:
%% 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]
]
]
The SDK opentelemetry
should be added as early as possible in the Release boot
process to ensure it is available before any telemetry is produced. Here it is
also set to temporary
under the assumption that we prefer to have a running
Release not producing telemetry over crashing the entire Release.
In addition to the API and SDK, an exporter for getting data out is needed. The SDK comes with an exporter for debugging purposes that prints to stdout and there are separate packages for exporting over the OpenTelemetry Protocol (OTLP) and the Zipkin protocol.
Initialization and Configuration
Configuration is done through the
Application environment
or
OS Environment Variables.
The SDK (opentelemetry
Application) uses the configuration to initialize a
Tracer Provider, its
Span Processors and
the Exporter.
Using the Console Exporter
Exporters are packages that allow telemetry data to be emitted somewhere - either to the console (which is what we’re doing here), or to a remote system or collector for further analysis and/or enrichment. OpenTelemetry supports a variety of exporters through its ecosystem, including popular open source tools like Jaeger and Zipkin.
To configure OpenTelemetry to use a particular exporter, in this case
otel_exporter_stdout
, the Application environment for opentelemetry
must set
the exporter
for the span processor otel_batch_processor
, a type of span
processor that batches up multiple spans over a period of time:
%% 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, []}
Working with Spans
Now that the dependencies and configuration are set up, we can create a module
with a function hello/0
that starts some 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() ->
%% start an active span and run a local function
?with_span(operation, #{}, fun nice_operation/1).
nice_operation(_SpanCtx) ->
?add_event(<<"Nice operation!">>, [{<<"bogons">>, 100}]),
?set_attributes([{another_key, <<"yes">>}]),
%% start an active span and run an anonymous function
?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
In this example, we’re using macros that use the process dictionary for context propagation and for getting the tracer.
Inside our function, we’re creating a new span named operation
with the
with_span
macro. The macro sets the new span as active
in the current
context – stored in the process dictionary, since we aren’t passing a context
as a variable.
Spans can have attributes and events, which are metadata and log statements that
help you interpret traces after-the-fact. The first span has an event
Nice operation!
, with attributes on the event, as well as an attribute set on
the span itself.
Finally, in this code snippet, we can see an example of creating a child span of
the currently-active span. When the with_span
macro starts a new span, it uses
the active span of the current context as the parent. So when you run this
program, you’ll see that the Sub operation...
span has been created as a child
of the operation
span.
To test out this project and see the spans created, you can run with
rebar3 shell
or iex -S mix
, each will pick up the corresponding
configuration for the release, resulting in the tracer and exporter to started.
$ 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}
Next Steps
Enrich your instrumentation with more manual instrumentation.
You’ll also want to configure an appropriate exporter to export your telemetry data to one or more telemetry backends.