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.
Installation
OpenTelemetry packages for Erlang/Elixir are available on hex.pm. There are two packages you might want to install, depending on what you are trying to accomplish.
opentelemetry_api
If you are developing a library or OTP Application that someone else would include
into their deployed code and you want to provide OpenTelemetry instrumentation
for them, you’ll want to add the dependency opentelemetry_api
. This package contains
only the API of OpenTelemetry. It will not start any processes and all API calls
(such as starting a span) will be a no-op that creates no data, unless the
opentelemetry
SDK package is also installed.
opentelemetry
If you are developing an Application that will actually be deployed and export
OpenTelemetry data, whether from instrumented dependencies or your code itself,
you’ll want to add the opentelemetry
package. This is the implementation of the
API and will start a Supervision tree, handling the necessary components for recording
and exporting OpenTelemetry signals. This is the package where you would configure
the destination(s) for your OpenTelemetry data, whether it be to an OpenTelemetry
Collector instance, or directly to a vendor’s data ingestion API.
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.0"},
{opentelemetry, "~> 1.0"}]}.
def deps do
[
{:opentelemetry_api, "~> 1.0"},
{:opentelemetry, "~> 1.0"}
]
end
In the case of Erlang, the Applications will also need to be added to
src/otel_getting_started.app.src
. In an Elixir project, a releases
section needs to be added to mix.exs
:
...
{applications, [kernel,
stdlib,
opentelemetry_api,
opentelemetry]},
...
releases: [
otel_getting_started: [
version: "0.0.1",
applications: [otel_getting_started: :permanent]
]
]
Initialization and Configuration
Configuration is done through the Application
environment
or OS Environment Variables. The
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}
Exporting to the OpenTelemetry Collector
The Collector provides a vendor agnostic way to receive, process and export telemetry data. The package opentelemetry_exporter provides support for both exporting over both HTTP (the default) and gRPC to the collector, which can then export Spans to a self-hosted service like Zipkin or Jaeger, as well as commercial services. For a full list of available exporters, see the registry.
For testing purposes the opentelemetry-erlang
repo has a Collector
configuration,
config/otel-collector-config.yaml
that can be used as a starting point. This configuration is used in
docker-compose.yml
to start the Collector with receivers for both HTTP and gRPC that then export to
Zipkin also run by docker-compose.
To export to the running Collector the opentelemetry_exporter
package must be
added to the project’s dependencies:
{deps, [{opentelemetry_api, "~> 1.0"},
{opentelemetry, "~> 1.0"},
{opentelemetry_exporter, "~> 1.0"}]}.
def deps do
[
{:opentelemetry_api, "~> 1.0"},
{:opentelemetry, "~> 1.0"},
{:opentelemetry_exporter, "~> 1.0"}
]
end
It should then be added to the configuration of the Release, it should be before the SDK Application to ensure the exporter’s dependencies are started before the SDK attempts to initialize and use the exporter.
Example of Release configuration in rebar.config
and for mix’s Release
task:
%% rebar.config
{relx, [{release, {my_instrumented_release, "0.1.0"},
[opentelemetry_exporter,
{opentelemetry, temporary},
my_instrumented_app]},
...]}.
# mix.exs
def project do
[
releases: [
my_instrumented_release: [
applications: [opentelemetry_exporter: :permanent, opentelemetry: :temporary]
],
...
]
]
end
Finally, the runtime configuration of the opentelemetry
and
opentelemetry_exporter
Applications are set to export to the Collector. The
configurations below show the defaults that are used if none are set, which are
the HTTP protocol with endpoint of localhost
on port 4318
. If using grpc
for the otlp_protocol
the endpoint should be changed to http://localhost:4317
.
%% config/sys.config.src
[
{opentelemetry,
[{span_processor, batch},
{traces_exporter, otlp}]},
{opentelemetry_exporter,
[{otlp_protocol, http_protobuf},
{otlp_endpoint, "http://localhost:4318"}]}]}
].
# config/runtime.exs
config :opentelemetry,
span_processor: :batch,
traces_exporter: :otlp
config :opentelemetry_exporter,
otlp_protocol: :http_protobuf,
otlp_endpoint: "http://localhost:4318"