Introduction to Telemetry in Elixir
Learn the basics of telemetry in Elixir

I write about Elixir, Elm, Software Development, and eBook writing.
Search for a command to run...
Learn the basics of telemetry in Elixir

I write about Elixir, Elm, Software Development, and eBook writing.
Great article!!!
As you say, we could send telemetry reports to an external service. In that case, it occurs to me that we could use a Task to do it.
For example:
def handle_event([:grocery, :store, :sale], measurements, metadata, _config) do
Task.start(fn ->
:timer.sleep(5_000)
Logger.info("[Sale telemetry: #{measurements.total}] total for #{metadata.product}")
end)
end
In this case, :timer.sleep(5_000) would be the time it takes to send the info to that service (a lot 😂😂😂). It's just an example.
But we wouldn't be blocking the function then.
My question is:
Do you recommend doing it like that? Is there a recommended way of doing it or it's fine like that?
Hi Manuel, that's certainly a way to do it asynchronously and without blocking our main process. It is a valid way to do it for sure. Most of the implementations that send data to external monitoring systems, like sentry or appsignal, do this using a GenServer that buffers the data and also doesn't block the main process. After a while, the genserver sends the accumulated data to the monitoring service. Of course it could just send each event every time it happens too. But as you correctly say, the principal thing to have in mind is to do it asynchronously.
Have a nice day!
BTW, today I publish the second part of this series!
Approaches to edit LiveView forms that use live_file_input

Collect Elixir metrics, aggregate them with StatsD and visualize them with Graphite

A couple of weeks ago I presented my talk: Elixir, Kubernetes, and minikube in the Elixir Meetup #5 organized by the fine people on curiosum.com. This talk is based on chapter 4 of my book "Deploying Elixir: Advanced Topics". If you haven't gotten t...

Deploying Elixir clusters to Production using Kubernetes

A couple of weeks ago I was invited to talk on the Elixir Wizards podcast by Smartlogic. We talk about Elixir in polyglot environments (that is, where you have a lot of languages used at the same time), my beginnings in the software field, my books, ...

Maybe you, like me, have found Telemetry while reading Elixir but have had no time to learn more details about it. I recently put some time aside to learn it and decided to write about it.
In this article I am going to show you what Telemetry is, why is needed, and how to use it in your own Elixir project.
First of all, what is telemetry?
Wikipedia explains it very clearly:
Telemetry is the [...] collection of measurements [...] and their automatic transmission [...] for monitoring
We can apply that to software systems, too, by collecting measurements while a software system is running and keeping track of that data.
Why would we do this, you may ask? Glad you ask. There are several reasons, but the main one is to discover what is happening inside our systems and then optimize them based on our findings.
What problem does telemetry solve for us? It gives us visibility into things that are important to us to measure and monitor.
Maybe we want to keep track of CPU utilization to see why our AWS bill is so high, or we need to see how many times a query is executed and how long it took to finish in our PostgreSQL database.
Measuring things and monitoring is not enough. If we don't use the collected data to make decisions and change or optimize the system measured then we might as well not measure anything at all.
By measuring our systems we get several benefits:
In summary, software telemetry allows us to measure and act upon the collected data of a running system for our own benefit.
There are several ways to take measures from a running Elixir app.
For example, the following fragment would take the duration of a function execution:
def some_function do
start = System.monotonic_time()
# do something that you want to measure
stop = System.monotonic_time()
record_measure("operation took: ", stop - start)
# ...
end
In the strict sense, that is telemetry. But is not the best approach. It has several drawbacks:
Given that telemetry is something that many people want, a library was created to address this need. It is called telemetry.
Advantages:
How does it work? It is very simple:
Let's try a simple example to see how easy is to start collecting telemetry data.
I am going to create a very simple Elixir for a fictional grocery store. We want to measure each sale.
First, create a simple Elixir project:
mix new telemetry_intro
cd telemetry_intro
Add telemetry to the list of dependencies in mix.exs:
defp deps() do
[
{:telemetry, "~> 1.0"}
]
end
Download the dependencies:
mix deps.get
Now our business logic module. Create a file named lib/grocery/store.ex and put this inside:
defmodule TelemetryIntro.Grocery.Store do
def sale(product, quantity, amount) do
total = quantity * amount
"Sold #{product}: #{quantity} units at #{amount} each. Total #{total}"
end
end
As you see it is very simple. It just receives some parameters, does something, and then returns a value. Doesn't have any measure code yet.
Let's just try it with iex. In the shell, write:
iex -S mix
iex(1)> TelemetryIntro.Grocery.Store.sale("apple", 4, 0.5)
"Sold apple: 4 units at 0.5 each. Total 2.0"
Nothing unexpected here. Let's add some measurements. Say we want to keep track of the total and also to track what product was sold.
Change the sale function to be like this:
defmodule TelemetryIntro.Grocery.Store do
def sale(product, quantity, amount) do
total = quantity * amount
:telemetry.execute(
[:grocery, :store, :sale],
%{total: total},
%{product: product}
)
"Sold #{product}: #{quantity} units at #{amount} each. Total #{total}"
end
end
Good. Now we are doing something. We are using the execute function of the telemetry module and we are passing three things:
The metadata could be anything we want: our users' IP address, some tracking header from a web request, the user id, etc.
The measurement is what we are interested in. This is what we'll collect and analyze and then use to decide things. It can be anything, but it must be a measurement of something.
Finally is the event name. You can put anything here. Most people put a hierarchy of words to easily identify the events. What is important to keep in mind is that this event name will be used later to match which function will handle this event.
Let's see how the counterpart, the event handler, looks.
Create a file lib/metrics/instrumenter.ex
defmodule TelemetryIntro.Metrics.Instrumenter do
require Logger
def handle_event([:grocery, :store, :sale], measurements, metadata, _config) do
Logger.info("[Sale telemetry: #{measurements.total}] total for #{metadata.product}")
end
end
It is very simple. It has a function (the name doesn't matter but the convention is to name it handle_event) that has as the first parameter the keyword list we are going to match against. Then the measurements and metadata. The last parameter doesn't matter right now. Just ignore it.
This function is going to be called every time the execute function is called and the event name matches. That means that this function must avoid blocking execution for much time as it will be evaluated synchronously.
Finally, we do a lame thing and just pass the received info to Logger.info. Ideally, we'll send that telemetry data to some repository for further processing and analysis but that is outside of the scope of this article (I'll cover it in a future one, tho).
What you need to notice here is that we decoupled the emitting of the telemetry data with the handling of the data collected. Our app can use :telemetry.execute everywhere with the same standard interface and, as long as we have a corresponding function to handle that event, we'll be collecting that data.
Even more important, if we don't have an event handler for that event, our emitting code will still work. It won't even notice there is nothing collecting data.
We are only missing one thing. We need to specify which function will handle a given event. We use the :telemetry.attach function to do that.
Create a function called setup in the Instrumenter module:
defmodule TelemetryIntro.Metrics.Instrumenter do
require Logger
def setup do
events = [
[:grocery, :store, :sale]
]
:telemetry.attach_many("telemetry-intro-instrumenter", events, &__MODULE__.handle_event/4, nil)
end
def handle_event([:grocery, :store, :sale], measurements, metadata, _config) do
Logger.info("[Sale telemetry: #{measurements.total}] total for #{metadata.product}")
end
end
As you can see, it is passing to :telemetry.attach_many a list of event names to monitor and the function that will handle all those event names.
By using pattern matching, the same handle_event function can handle all the necessary events in your app.
Now we need to call this setup function when the app starts.
Let's change the way our elixir app starts by modifying the mix.exs file and changing the application function inside it so that it is like this:
def application do
[
extra_applications: [:logger],
mod: {TelemetryIntro, []}
]
end
We added the mod: to specify a module that will be called when the application starts to execute some custom code. Our custom code will be the telemetry configuration.
Now replace the contents of the lib/telemetry_intro.ex module with this:
defmodule TelemetryIntro do
use Application
def start(_type, _args) do
TelemetryIntro.Metrics.Instrumenter.setup()
children = []
Supervisor.start_link(children, strategy: :one_for_one)
end
end
This module implements the start callback of the Application behavior. This function's responsibility is to spawn and link a supervisor and return {:ok, pid} or {:ok, pid, state}. Here we just start a supervisor with no children. That will suffice for this example.
Let's try it. Start again the application with iex:
iex -S mix
iex(1)> TelemetryIntro.Grocery.Store.sale("apple", 4, 0.2)
00:18:01.867 [info] [Sale telemetry: 0.8] total for apple
"Sold apple: 4 units at 0.2 each. Total 0.8"
Great, it works. The first output line is the result of handle_event call. The second one is the return value of the sale function.
Let's see what happens if we don't configure a handler for the event.
Comment the TelemetryIntro.Metrics.Instrumenter.setup() in telemetry_intro.ex
defmodule TelemetryIntro do
use Application
def start(_type, _args) do
# TelemetryIntro.Metrics.Instrumenter.setup()
children = []
Supervisor.start_link(children, strategy: :one_for_one)
end
end
And try again with iex:
iex -S mix
iex(1)> TelemetryIntro.Grocery.Store.sale("apple", 4, 0.2)
"Sold apple: 4 units at 0.2 each. Total 0.8"
As you can see, the code still works, even if there is no handler configured for that event. Our app still calls the :telemetry.execute function but nothing breaks when nothing is configured to handle that event. How cool is that?
We learned how to emit events and handle them. You can send this info to a third-party error monitoring service or ingest it to a big data store. From here, the sky is the limit.
In this article we learned:
As we have seen, telemetry is a powerful tool to inspect the behavior of a running system.
As long as we use the collected data for making decisions to improve our system or its behavior, data collection through telemetry is an investment that every system must seriously consider making.
If you want to learn more about telemetry, you can read the full documentation here
I'm Miguel Cobá. I write about Elixir, Elm, Software Development, and eBook writing.