When relying on OpenTelemetry for your Observability needs, it can be important to test that certain spans are created and attributes correctly set. For example, can you be sure that you attaching the right metadata to data that ultimately powers an SLO? This document covers an approach to that kind of validation.
Setup¶
Only the opentelemetry
and opentelemetry_api
libraries are required for
testing in Elixir/Erlang:
{{< tabpane langEqualsHeader=true >}}
{{< tab Erlang >}} {deps, [{opentelemetry_api, "~> 1.0"}, {opentelemetry, "~> 1.0"}]}. {{< /tab >}}
{{< tab Elixir >}} def deps do [ {:opentelemetry_api, "~> 1.0"}, {:opentelemetry, "~> 1.0"} ] end {{< /tab >}}
{{< /tabpane >}}
Set your exporter
to :none
and the span processor to
:otel_simple_processor
. This ensure that your tests don't actually export data
to a destination, and that spans can be analyzed after they are processed.
{{< tabpane langEqualsHeader=true >}}
{{< tab Erlang >}} %% config/sys.config.src {opentelemetry, [{traces_exporter, none}, {processors, [{otel_simple_processor, #{}}]}]} {{< /tab >}}
{{< tab Elixir >}}
config/test.exs¶
import Config
config :opentelemetry, traces_exporter: :none
config :opentelemetry, :processors, [ {:otel_simple_processor, %{}} ] {{< /tab >}}
{{< /tabpane >}}
A modified version of the hello
function from the
Getting Started guide will
serve as our test case:
{{< tabpane langEqualsHeader=true >}}
{{< tab Erlang >}} %% 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) -> ?set_attributes([{a_key, <<"a value">>}]), world {{< /tab >}}
{{< tab Elixir >}}
lib/otel_getting_started.ex¶
defmodule OtelGettingStarted do require OpenTelemetry.Tracer, as: Tracer
def hello do Tracer.with_span "operation" do Tracer.set_attributes([{:a_key, "a value"}]) :world end end end {{< /tab >}}
{{< /tabpane >}}
Testing¶
{{< tabpane langEqualsHeader=true >}}
{{< tab Erlang >}} -module(otel_getting_started_SUITE).
-compile(export_all).
-include_lib("stdlib/include/assert.hrl"). -include_lib("common_test/include/ct.hrl").
-include_lib("opentelemetry/include/otel_span.hrl").
-define(assertReceive(SpanName), receive {span, Span=#span{name=SpanName}} -> Span after 1000 -> ct:fail("Did not receive the span after 1s") end).
all() -> [greets_the_world].
init_per_suite(Config) -> application:load(opentelemetry), application:set_env(opentelemetry, processors, [{otel_simple_processor, #{}}]), {ok, _} = application:ensure_all_started(opentelemetry), Config.
end_per_suite(_Config) -> _ = application:stop(opentelemetry), _ = application:unload(opentelemetry), ok.
init_per_testcase(greets_the_world, Config) -> otel_simple_processor:set_exporter(otel_exporter_pid, self()), Config.
end_per_testcase(greets_the_world, _Config) -> otel_simple_processor:set_exporter(none), ok.
greets_the_world(_Config) -> otel_getting_started:hello(),
1 2 3 4 5 6 7 8 |
|
{{< /tab >}}
{{< tab Elixir >}} defmodule OtelGettingStartedTest do use ExUnit.Case
# Use Record module to extract fields of the Span record from the opentelemetry dependency.
require Record
@fields Record.extract(:span, from: "deps/opentelemetry/include/otel_span.hrl")
# Define macros for Span
.
Record.defrecordp(:span, @fields)
test "greets the world" do # Set exporter to :otel_exporter_pid, which sends spans # to the given process - in this case self() - in the format {:span, span} :otel_simple_processor.set_exporter(:otel_exporter_pid, self())
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
end end {{< /tab >}}
{{< /tabpane >}}