OpenTelemetry Tracing Testing

elixir opentelemetry testing tracing

Generally, I would advise to focus your testing on integration tests and to avoid any tests that rely on the internal behaviour of your code. Doing so makes internal changes simple while providing high confidence that the functionality of your modules is intact.

But at times it becomes necessary to debug a missing trace. Or maybe you have alerts that use certain traces as inputs, and want to ensure that the shape of a trace or span doesn’t change. This tutorial is intended for those occasions.

I’m assuming you have both :opentelemetry and :opentelemetry_api dependencies in your mix.exs:

 defp deps do
    [
      # other deps
      {:opentelemetry, "~> 1.0"},
      {:opentelemetry_api, "~> 1.0"}
    ]
  end

We’ll be using a modified version of the hello function from the OpenTelemetry Getting Started guide for Erlang/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

In order to test this function, we will set the exporter to :undefined and set a minimal delay on the batch processor:

# config/test.exs
import Config

config :opentelemetry,
    traces_exporter: :undefined

config :opentelemetry, :processors, [
  {:otel_batch_processor, %{scheduled_delay_ms: 1}}
]

Now we’re ready to test!

defmodule OtelGettingStartedTest do
  use ExUnit.Case, async: true
  doctest OtelGettingStarted

  require Record
  @fields Record.extract(:span, from: "deps/opentelemetry/include/otel_span.hrl")
  Record.defrecordp(:span, @fields)

  test "greets the world" do
    :otel_batch_processor.set_exporter(:otel_exporter_pid, self())
    OpenTelemetry.get_tracer(:test_tracer)

    OtelGettingStarted.hello()

    attributes = :otel_attributes.new([a_key: "a_value"], 128, :infinity)

    assert_receive {:span, span(
      name: "operation",
      attributes: ^attributes
      )}
  end
end

Let’s walk through this line by line:

require Record
@fields Record.extract(:span, from: "deps/opentelemetry/include/otel_span.hrl")
Record.defrecordp(:span, @fields)

We use Elixir’s Record module to extract the fields of the Span record from the erlang file in our deps folder. Then we define a set of private macros based on those fields.

We now have an Elixir representation of the spans that the erlang :opentelemetry library will emit.

:otel_batch_processor.set_exporter(:otel_exporter_pid, self())
OpenTelemetry.get_tracer(:test_tracer)

In the test itself, we set the exporter to :otel_exporter_pid, which will send a message containing any received spans to the supplied PID, in this case self() - the test itself.

Then we get a Tracer.

OtelGettingStarted.hello()

We call our function that will create a span with a single attribute.

attributes = :otel_attributes.new([a_key: "a_value"], 128, :infinity)

assert_receive {:span, span(
    name: "operation",
    attributes: ^attributes
    )}

The :otel_exporter_pid sends the span to the test process in the form {:span, span}, and we assert its reception.

Additionally, we use the :otel_attributes module to help us create our single attribute, and pin it to assert that the attribute and its values were set as expected. There’s a similar class for :otel_events to aid in testing for event inclusion.