Elixir: Usando Tesla.Middleware.Telemetry

jvmartyns

João Vitor Martins Araújo

Posted on November 2, 2022

Elixir: Usando Tesla.Middleware.Telemetry

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
Enter fullscreen mode Exit fullscreen mode

Primeiro adicionamos o tesla em nossas dependências, no arquivo mix.exs

defp deps do
    [
     ...,
      {:tesla, "~> 1.4"}
    ]
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode
  • Medição, contem a duração da requisição em nanosegundos.
%{duration: native_time}
Enter fullscreen mode Exit fullscreen mode
  • Metadados, todas as informações que vem na resposta do tesla.
%{env: Tesla.Env.t()}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

Obrigado por ler até aqui. 😁

💖 💪 🙅 🚩
jvmartyns
João Vitor Martins Araújo

Posted on November 2, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related