A biblioteca Tesla, usada para trabalhar com requisições HTTP em Elixir, possui um middleware de telemetria que podemos usar para obter algumas informações sobre as requisições que estão sendo executadas.
No caso de uso que irei demonstrar estarei coletando algumas informações da requisição para posteriormente salvar em um banco de dados como se fosse um log.
Para começar vamos criar um projeto Phoenix que irei carinhosamente chamar de Frog.
mix phx.new frog
Primeiro adicionamos o tesla em nossas dependências, no arquivo mix.exs
defp deps do
[
...,
{:tesla, "~> 1.4"}
]
end
Em seguida precisamos criar um cliente HTTP qualquer, para podermos ter algo com o que trabalhar. Então vamos consultar uma API de CEP chamada: Viacep
A base do nosso cliente fica assim:
defmodule Frog.Client do
use Tesla
@url "http://viacep.com.br/ws/01001000/json/"
plug Tesla.Middleware.JSON
def get, do: get!(@url).body
end
Então, para começar, vamos adicionar o middleware de telemetria junto aos middlewares que estamos usando em nosso cliente.
defmodule Frog.Client do
use Tesla
@url "http://viacep.com.br/ws/01001000/json/"
plug Tesla.Middleware.JSON
plug Tesla.Middleware.Telemetry
def get, do: get!(@url).body
end
A partir daí o middleware já estará trabalhando.
O que ele faz é emitir eventos. Quando a requisição for feita, ele emitirá um evento de start
e quando terminar, um evento de stop
.
Cada evento é composto de três partes, vamos ver a composição do evento de stop:
- Evento, formado por uma lista de átomos.
[:tesla, :request, :stop]
- Medição, contem a duração da requisição em nanosegundos.
%{duration: native_time}
- Metadados, todas as informações que vem na resposta do tesla.
%{env: Tesla.Env.t()}
Agora só precisamos capturar esses eventos quando forem emitidos. Para isso iremos utilizar um recurso pro próprio Erlang, o módulo :telemetry.
Vamos criar um módulo para poder trabalhar isso, e criaremos uma função para iniciar o nosso handler
.
defmodule Frog.HandleTelemetry do
def setup do
:telemetry.attach(
"my-tesla-telemetry", # Nome do evento, pode ser qualquer nome.
[:tesla, :request, :stop], # Evento que queremos capturar
&__MODULE__.handle_event/4, # Função que irá tratar os dados.
nil # Trata-se de configurações, mas podemos ignorar.
)
end
end
O :telemetry.attach/4
fica escutando os eventos lançados e captura os que definirmos. Essa função recebe 4 argumentos:
- Um nome para o nosso evento
- O evento que queremos capturar
- Uma função para tratarmos os dados
- E configurações adicionais, que segundo a documentação, podemos ignorar com segurança.
Agora vamos criar a nossa função para tratar os dados que estão vindo.
defmodule Frog.HandleTelemetry do
def setup do
:telemetry.attach(
"my-tesla-telemetry",
[:tesla, :request, :stop],
&__MODULE__.handle_event/4,
nil
)
end
def handle_event(_event, %{duration: duration}, %{env: metadata}, nil) do
%{
duration: duration,
status: metadata.status,
method: metadata.method,
url: metadata.url
}
end
end
A nossa função handle_event deve esperar receber 4 argumentos:
- O evento, que podemos ignorar nesse caso.
- A duração da requisição
- O dados da requisição
- As configurações que passamos em
:telemetry.attach/4
, e que podemos ignorar.
Observação sobre o retorno da função:
Nesse caso eu contruí um mapa com as informações relevantes para mim, e a ideia aqui é passar isso para outro módulo que fará a inserção desses dados num banco. Você pode usar o IO.inspect/1
para ir analisando tudo que está recebendo, e o que será importante para montar o retorno da função conforme a sua necessidade.
Um ponto importante aqui é que se a lógica desse handle_event quebrar o :telemetry.attach/4
para de funcionar e você perderá os dados. Para evitar, isso podemos usar o rescue
para tratar possíveis exceções. Junto a isso estarei usando o módulo Logger
para ter uma visualização do erro. Vou aproveitar e adicionar também uma função privada para capturar a data nos headers da requisição.
defmodule Frog.HandleTelemetry do
require Logger
def setup do
:telemetry.attach(
"my-tesla-telemetry",
[:tesla, :request, :stop],
&__MODULE__.handle_event/4,
nil
)
end
def handle_event(_event, %{duration: duration}, %{env: metadata}, nil) do
%{
date: get_date(metadata.headers),
duration: duration,
status: metadata.status,
method: metadata.method,
url: metadata.url
}
rescue
error -> Logger.error("[#{__MODULE__}] Error: #{inspect(error)}")
end
defp get_date([{"date", date} | _]), do: date
defp get_date([_ | tail]), do: get_date(tail)
defp get_date(_), do: nil
end
Para finalizar, precisamos chamar a nossa função setup dentro do start da aplicação.
defmodule Frog.Application do
...
def start(_type, _args) do
children = [
...
]
Frog.HandleTelemetry.setup()
...
end
...
end
Com isso, a partir do momento que a sua aplicação iniciar, o handler estará esperando os eventos e os capturando quando ocorrerem. Agora você pode usar essas informações da forma que achar mais apropriado.
Obs:
- Os nomes das funções "setup" e "handle_event" são opcionais, mas verá que na própria documentação também é usado o nome "handle_event", então estou seguindo essa convenção.
- Outra maneira de fazer isso seria capturando essas informações diretamente no módulo do cliente. Mas isso criaria um acoplamento desnecessário. E estaria indo contra o princípio da responsabilidade única, pois o seu cliente estaria fazendo muitas coisas em um só lugar.
- A função
:telemetry.attach_many/4
recebe uma lista de eventos e irá capturar todos os eventos definidos da lista. Assim você poderá capturar os eventos de start e stop caso queira. - Muitas outras bibliotecas Elixir tem suporte a telemetria, como Ecto, Oban, Absinthe e o próprio Phoenix.
Links para as Documentações:
Top comments (0)