Using Elixir Telemetry and Tesla
Tesla provides a Telemetry middleware which is pretty straightforward to configure for your clients. However, I find that the middleware on its own is not sufficient to get more detailed metrics.
My goal here is to be able to extract the domain and the path in the metrics. This post will show you how to do that.
API Client
As an example, we’ll create an API client to hex.pm.
defmodule MyApp.HexPm do
use Tesla, only: [:get], docs: false
# required middleware for basic requests to hex.pm
plug Tesla.Middleware.BaseUrl, "https://hex.pm/"
plug Tesla.Middleware.Headers, [{"user-agent", "MyApp/0.0.1"}]
plug Tesla.Middleware.JSON, decode_content_types: ["application/vnd.hex+json"]
# required middleware for telemetry
plug Tesla.Middleware.KeepRequest
plug Tesla.Middleware.Telemetry, metadata: %{api: "hex.pm"}
plug Tesla.Middleware.PathParams
def get_package(name) do
get("/api/packages/:name", opts: [path_params: [name: name]])
end
end
The middleware that will enable Telemetry events are:
KeepRequest
- Store request URL, body and headers into
:opts
. The options in our case would look like this:
[
req_url: "https://hex.pm/api/packages/:name",
req_headers: [{"user-agent", "MyApp/0.0.1"}],
req_body: nil,
path_params: [name: "phoenix"]
]
PathParams
- Use templated URLs with separate params. In our example, it enables
/api/packages/:name
to receive the:name
dynamically when sending the request but allows to group requests by the template. -
Telemetry
: - Emits events using the
:telemetry
library to expose instrumentation. This middleware automatically adds theenv
to the event, but extending it with themetadata
option is also possible. In our case, I added%{api: "hex.pm"}
, which will help group requests by their “api”. It’s helpful if you have multiple Tesla clients with distinct APIs.
Telemetry Configuration
Another necessary step is to add the proper configuration to your app’s Telemetry configuration:
defmodule MyApp.Telemetry do
use Supervisor
import Telemetry.Metrics
# ...
@impl true
def init(_arg) do
# starting ConsoleReporter to see metrics in IEX console
children = [{Telemetry.Metrics.ConsoleReporter, metrics: metrics()}]
Supervisor.init(children, strategy: :one_for_one)
end
def metrics do
[
# your other metrics...
summary("tesla.request.stop.duration",
unit: {:native, :millisecond},
tags: [:api, :method, :req_url],
tag_values: &tesla_tag_values/1
)
]
end
# extract the tags from the env + middleware metadata
defp tesla_tag_values(meta) do
%{api: meta.api, method: meta.env.method, req_url: meta.env.opts[:req_url]})
end
end
This configuration enables a few things:
tags
- Specify which tags to extract from the
metadata
. We’re specifyingreq_url
here, but it’s unavailable at the event’s root. We need to extract it usingtag_values
. tag_values
- A function that processes the event
metadata
. Because ourreq_url
is deep inside themeta.env.opts
, we need to extract it here. The map returned will be available to thetags
function.
Running the example in the iex
console:
iex> MyApp.HexPm.get_package("phoenix")
[Telemetry.Metrics.ConsoleReporter] Got new event!
Event name: tesla.request.stop
All measurements: %{duration: 188736000}
All metadata: %{api: "hex.pm", env: ...}
Metric measurement: #Function<.../1 in Telemetry.Metrics.maybe_convert_measurement/2> (summary)
With value: 188.736 millisecond
Tag values: %{api: "hex.pm", method: :get, req_url: "https://hex.pm/api/packages/:name"}
An important note: reporters are responsible for extracting tags and tag_values, and each reporter may implement it differently. Check the reporter implementation to ensure it applies the proper tags transformation!