Elixir: Usando Tesla.Middleware.Telemetry
Published: 2022-11-02 13:56:42

A biblioteca [Tesla](https://hexdocs.pm/tesla/Tesla.html), usada para trabalhar com requisições HTTP em Elixir, possui um [middleware](https://dev.to/cyytrus/entendendo-plugs-em-elixir-469c#:~:text=1%20%2D%20Middlewares%3A%20Um%20breve%20resumo) 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.
```elixir
mix phx.new frog
```

Primeiro adicionamos o tesla em nossas dependências, no arquivo `mix.exs`
```elixir
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](https://viacep.com.br/)

A base do nosso cliente fica assim:
```elixir
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.

```elixir
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.
```elixir
[:tesla, :request, :stop]
``` 
- Medição, contem a duração da requisição em nanosegundos.
```elixir
%{duration: native_time}
```
- Metadados, todas as informações que vem na resposta do tesla.
```elixir
%{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](https://hexdocs.pm/telemetry/readme.html).

Vamos criar um módulo para poder trabalhar isso, e criaremos uma função para iniciar o nosso `handler`.
```elixir
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.

```elixir
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.

```elixir
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. 

```elixir
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:**
- [Tesla](https://hexdocs.pm/tesla/Tesla.html)
- [Tesla.Middleware.Telemetry](https://hexdocs.pm/tesla/Tesla.Middleware.Telemetry.html)
- [:telemetry](https://hexdocs.pm/telemetry/readme.html)
- [Outras bibliotecas que usam telemetria](https://hexdocs.pm/phoenix/telemetry.html#libraries-using-telemetry)

# Obrigado por ler até aqui. 😁