Sonda

Sonda is a telemetry library for Elixir, providing configurable sinks for recording signals.

By default, Sonda is configured to record signals in memory and can be inspected through the provided utility functions.

{:ok, pid} = Sonda.start_link()
Sonda.record(pid, :a_signal, %{"some" -> "data"})
Sonda.recorded_once?(pid, &match?({:a_signal, _, _}, &1)) # => true
Sonda.record(pid, :another_signal, 123)
Sonda.record(pid, :signal_only)
Sonda.records(pid)
# [
#    {:a_signal, ~N[2020-04-22 23:07:05.905776], %{"some" -> "data"}},
#    {:another_signal, ~N[2020-04-22 23:07:06.905776], 123}},
#    {:signal_only, ~N[2020-04-22 23:07:07.905776], nil}
# ]

Installation

The package can be installed by adding sonda to your list of dependencies in mix.exs:

def deps do
  [
    {:sonda, "~> 0.1.0"}
  ]
end

Docs can be found at https://hexdocs.pm/sonda.

Basic Usage

The default configuration of Sonda provides all the utility functions for recording signals in memory and then inspecting the output:

Start

{:ok, pid} = Sonda.start_link()

Record signals

Sonda.record(pid, :a_signal, %{"some" -> "data"})
Sonda.record(pid, :another_signal, 123)
Sonda.record(pid, :signal_only)

Inspect

Was any message ever recorded with this shape?

Sonda.recorded?(pid, &match?({:a_signal, _, _}, &1)) # => true

Was any message ever recorded with this shape only one time?

Sonda.recorded_once?(pid, &match?({:a_signal, _, _}, &1)) # => true

List all the recorded messages starting from the oldest

Sonda.records(pid)
# [
#    {:a_signal, ~N[2020-04-22 23:07:05.905776], %{"some" -> "data"}},
#    {:another_signal, ~N[2020-04-22 23:07:06.905776], 123}},
#    {:signal_only, ~N[2020-04-22 23:07:07.905776], nil}
# ]

List all the recorded messages starting from the oldest and matching the provided function

Sonda.records(pid, fn {_, _, data} -> data != 123 end)
# [
#    {:a_signal, ~N[2020-04-22 23:07:05.905776], %{"some" -> "data"}},
#    {:signal_only, ~N[2020-04-22 23:07:07.905776], nil}
# ]

Gets the first recorded message matching the provided function, only if there is only one. An error is returned in all the other cases

Sonda.one_record(pid, fn {_, _, data} -> data == 123 end)
# {:ok, {:another_signal, ~N[2020-04-22 23:07:06.905776], 123}}}

Gets the first recorded message matching the provided function, only if there is only one. An error is returned in all the other cases

Sonda.one_record(pid, fn {_, _, data} -> data == 123 end)
# {:ok, {:another_signal, ~N[2020-04-22 23:07:06.905776], 123}}}

Is this signal allowed to be recorded?

Sonda.record_signal?(pid, :a_signal) # => true

See the Filtering Signals section for more information about why a signal might be recorded or not. This output of this function is false for any signal that's not accepted.

Filtering Signals

It's possible to ignore some signals from being recorded. It's sufficient to start Sonda with the following configuration:

{:ok, pid} = Sonda.start_link(signals: [:a_signal, :another_signal])

The output of the function Sonda.record_signal?/2 is true only for the signals :a_signal and :another_signal, like in the following example:

{:ok, pid} = Sonda.start_link(signals: [:a_signal, :another_signal])

Sonda.record_signal?(pid, :a_signal) # => true
Sonda.record_signal?(pid, :another_signal) # => true
Sonda.record_signal?(pid, :not_recorded) # => false

When the input of the functions record/2 and record/3 is a signal for which the output of Sonda.record_signal?/2 is false, no operation is performed. The following example shows that :not_recorded signal is ignored:

{:ok, pid} = Sonda.start_link(signals: [:a_signal, :another_signal])

Sonda.record(pid, :a_signal, %{"some" -> "data"})
Sonda.record(pid, :not_recorded)
Sonda.record(pid, :another_signal, 123)

Sonda.records(pid)
# [
#    {:a_signal, ~N[2020-04-22 23:07:05.905776], %{"some" -> "data"}},
#    {:another_signal, ~N[2020-04-22 23:07:06.905776], 123}}
# ]

Common Usage

ExUnit

One of the most common purpose of Sonda is to inspect the usage of closures or the behavior of GenServers. The following example shows the closure being invoked twice by inspecting the signals recorded through Sonda:

defmodule SomeTest do
  use ExUnit.Case

  test "Enum.map/2 is invoked once per list element" do
    sonda = start_supervised!(Sonda)
    elements = [1, 2]

    elements = Enum.map(elements, fn element ->
      Sonda.record(sonda, :map, element)
      element * 2
    end)
    records = Sonda.records(sonda)

    assert elements == [2, 4]
    assert length(records) == 2
  end
end

Advanced Usage

TODO:

  • Multiple sinks
  • Custom sinks
  • Custom clock

Thanks

This library is heavily influenced by telemetry, the Eventide library for ruby instrumentation.