domvas

domvas

Testing and Telemetry events: how to test if they are sent?

Hello elixir forum,
I’m currently playing with Telemetry and Prometheus and at some point I wonder how to check that the events are sent.

In my code, I have:

def create_user(data) do
    with %Changeset{valid?: true} = changeset <- User.changeset(%User{}, data),
         ...
         {:ok, user} <- GraphRepo.insert(changeset) do
      :telemetry.execute([:user, :create], %{count: 1})
      {:ok, user}
    end

and

def business_logic_metrics do
    [
      counter("user.create.count")
    ]
  end

with Telemetry.Metrics aded in Supervisor and everything’s fine.
But for testing, I added in one of mys test support file:

Telemetry.Metrics.ConsoleReporter.start_link(metrics: business_logic_metrics())

I can now see the output when I launch my test:

[Telemetry.Metrics.ConsoleReporter] Got new event!
Event name: user.create
All measurements: %{count: 1}
All metadata: %{}

Metric measurement: :count (counter)
Tag values: %{}

But is there a way to catch this output and transform it in something I can assert on?
I’ve seen that a IO.device can be specified in ConsoleReporter options but I don’t really understand how I can use it.
The best would be end up with something like this: assert_metrics "user.create", %{count: 1}
My idea would be have a process to work as an IO.device storing every ConsoleReporter output and assert_metrics checking in that process state in the desired event in present.
Is it viable (and do-able)?

Thanks for the help.

Marked As Solved

domvas

domvas

I’ve came to a new solution that solve the problem of mixing telemetry events and metrics.
The previous solution use assert_metrics to check if a particular Telemetry events occured, which could cause some confusion.
Additionally, I wanted to also test for metrics produced by Telemetry.Metrics, the here is the final test support file:

defmodule MayApp.MetricsCase do
  @moduledoc """
  This module defines the setup for tests requiring metrics tests.

  Available tags:
    - `telemetry_events`: list the Telemetry events to listen
    - `metrics`: Specify the list of Telemetry.Metrics to used (format: [Module, :function, [args]])

  Available assertions:
    - `assert_telemetry_event(name, measurements, metadata \\ %{})`
    - `assert_metric(name, measurement, metadata \\ %{})`

  ## Example:

  @tag telemetry_events: [[:user, :subscription, :email_confirmation]],
       metrics: [MayApp.Metrics, :metrics, []]
  test "my test" do
    ...
    assert_telemetry_event([:user, :subscription, :email_confirmation], %{count: 1}, %{result: :error})
    assert_metric([:user, :subscription, :email_confirmation, :count], 1, %{success: false})
  end

  """
  use ExUnit.CaseTemplate

  using do
    quote do
      import MayApp.MetricsCase
    end
  end

  setup tags do
    if telemetry_events = tags[:telemetry_events] do
      metrics = get_metrics_from_tag(tags)

      self = self()

      groups = Enum.group_by(metrics, & &1.event_name)

      :telemetry.attach_many(
        tags[:test],
        telemetry_events,
        fn name, measurements, metadata, _config ->
          send(self, {:telemetry_event, name, measurements, metadata})

          # Send related metrics
          if Enum.count(metrics) > 0 do
            Enum.each(Map.get(groups, name, []), fn metric ->
              send(
                self,
                {:metric, metric.name, Map.get(measurements, metric.measurement),
                 extract_tags(metric, metadata)}
              )
            end)
          end
        end,
        nil
      )
    end

    :ok
  end

  defp extract_tags(metric, metadata) do
    tag_values = metric.tag_values.(metadata)
    Map.take(tag_values, metric.tags)
  end

  defp get_metrics_from_tag(%{metrics: [m, f, args]}) do
    apply(m, f, args)
  end

  defp get_metrics_from_tag(_) do
    []
  end

  @doc """
  Assert that given event has been sent.
  """
  defmacro assert_telemetry_event(name, measurements, metadata \\ %{}),
    do: do_assert_telemetry_event(name, measurements, metadata)

  defp do_assert_telemetry_event(name, measurements, %{}) do
    do_assert_telemetry_event(name, measurements, Macro.escape(%{}))
  end

  defp do_assert_telemetry_event(name, measurements, metadata) do
    do_assert_receive(:telemetry_event, name, measurements, metadata)
  end

  defmacro assert_metric(name, measurement, metadata \\ %{}),
    do: do_assert_metric(name, measurement, metadata)

  defp do_assert_metric(name, measurement, %{}) do
    do_assert_metric(name, measurement, Macro.escape(%{}))
  end

  defp do_assert_metric(name, measurement, metadata) do
    do_assert_receive(:metric, name, measurement, metadata)
  end

  defp do_assert_receive(msg_type, name, measurement, metadata) do
    quote do
      assert_receive {unquote(msg_type), unquote(name), unquote(measurement), unquote(metadata)}
    end
  end
end

Hope this will help other with the same need of testing both telemetry events and telemetry.metrics

Also Liked

binaryseed

binaryseed

You can pretty easily wire up another telemetry handler inside your test and it’ll run inline with the call to execute. From inside there you could do literally anything - send a message, store some state, etc.

https://hexdocs.pm/telemetry/telemetry.html#attach-4

domvas

domvas

Ii finally end up with this:

defmodule MyApp.MetricsCase do
  @moduledoc """
  This module defines the setup for tests requiring metrics tests.
  """
  use ExUnit.CaseTemplate

  using do
    quote do
      import Sharon.MetricsCase
    end
  end

  @doc """
  Add a tag for metrics testing.
  Takes a list of metrics as paramater.

  ## Usage

      @tag metrics: [[:my_app, :metrics1], [:my_app, :metrics2, :sub]]
  """
  setup tags do
    if metrics = tags[:metrics] do
      self = self()

      :telemetry.attach_many(
        tags[:test],
        metrics,
        fn name, measurements, metadata, _ ->
          send(self, {:telemetry_event, name, measurements, metadata})
        end,
        nil
      )
    end

    :ok
  end

  @doc """
  Assert that given event has been sent.
  """
  defmacro assert_metrics(name, measurements, metadata \\ %{}),
    do: do_assert_metrics(name, measurements, metadata)

  defp do_assert_metrics(name, measurements, %{}) do
    do_assert_metrics(name, measurements, Macro.escape(%{}))
  end

  defp do_assert_metrics(name, measurements, metadata) do
    quote do
      assert_receive {:telemetry_event, unquote(name), unquote(measurements), unquote(metadata)}
    end
  end
end

which can be used in both test support file and test file.

My example test is now:

@tag metrics: [[:user, :create], [:user, :registration]]
test "user creation ok" do
  Repo.insert!(user_data)
  
  assert_metrics([:user, :create], %{count: 1})
  assert_metrics([:user, :registration], %{count: 1}, %{stage: :creation})
end
akoutmos

akoutmos

Author of Build a Weather Station with Elixir and Nerves

It may be useful as a reference to checkout how telemetry events are tested in Broadway https://github.com/plataformatec/broadway/blob/master/test/broadway_test.exs#L593

Where Next?

Popular in Questions Top

jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement