Current section
Files
Jump to
Current section
Files
lib/api_brasil/core/hooks.ex
defmodule ApiBrasil.Core.Hooks do
@moduledoc """
Ganchos de observabilidade — logging, métricas, tracing.
Podem ser um **mapa/keyword de funções** ou um **módulo** que implementa
este behaviour.
ApiBrasil.new(
hooks: %{
request: &IO.inspect(&1, label: "→"),
response: fn info -> Logger.info("← \#{info.status} em \#{info.duration}ms") end,
retry: fn info -> Logger.warning("retry em \#{info.delay}ms: \#{info.reason}") end
}
)
defmodule MinhaApp.ApiHooks do
@behaviour ApiBrasil.Core.Hooks
@impl true
def on_response(info), do: :telemetry.execute([:apibrasil], %{duration: info.duration}, info)
end
ApiBrasil.new(hooks: MinhaApp.ApiHooks)
"""
@typedoc """
Dados da requisição: `:method`, `:url` (com query string), `:headers`,
`:body` e `:attempt` (0 = primeira tentativa).
"""
@type request_info :: %{
method: ApiBrasil.Core.Transport.method(),
url: String.t(),
headers: map(),
body: term(),
attempt: non_neg_integer()
}
@typedoc """
Dados da resposta: `:method`, `:url`, `:status`, `:duration` (ms) e
`:attempt`.
"""
@type response_info :: %{
method: ApiBrasil.Core.Transport.method(),
url: String.t(),
status: pos_integer(),
duration: non_neg_integer(),
attempt: non_neg_integer()
}
@typedoc """
Dados do retry: `:method`, `:url`, `:attempt` (a próxima), `:delay` (ms) e
`:reason` (ex: `"HTTP 429"`).
"""
@type retry_info :: %{
method: ApiBrasil.Core.Transport.method(),
url: String.t(),
attempt: non_neg_integer(),
delay: non_neg_integer(),
reason: String.t()
}
@typedoc "Hooks aceitos pela configuração."
@type t :: module() | map() | keyword() | nil
@doc "Chamado antes de cada tentativa de requisição."
@callback on_request(request_info()) :: any()
@doc "Chamado quando a resposta chega, qualquer que seja o status."
@callback on_response(response_info()) :: any()
@doc "Chamado antes de aguardar o backoff de um retry."
@callback on_retry(retry_info()) :: any()
@optional_callbacks on_request: 1, on_response: 1, on_retry: 1
@events %{request: :on_request, response: :on_response, retry: :on_retry}
@doc """
Dispara um evento (`:request`, `:response` ou `:retry`) nos hooks
configurados. Falhas dentro de um hook nunca derrubam a requisição.
"""
@spec notify(t(), :request | :response | :retry, map()) :: :ok
def notify(nil, _event, _info), do: :ok
def notify(hooks, event, info) when is_map(hooks) and not is_struct(hooks) do
run(Map.get(hooks, event), info)
end
def notify(hooks, event, info) when is_list(hooks) do
run(Keyword.get(hooks, event), info)
end
def notify(module, event, info) when is_atom(module) do
callback = Map.fetch!(@events, event)
if Code.ensure_loaded?(module) and function_exported?(module, callback, 1) do
safely(fn -> apply(module, callback, [info]) end)
end
:ok
end
defp run(nil, _info), do: :ok
defp run(fun, info) when is_function(fun, 1), do: safely(fn -> fun.(info) end)
defp run(_other, _info), do: :ok
defp safely(fun) do
fun.()
:ok
rescue
_error -> :ok
catch
_kind, _reason -> :ok
end
end