Packages
honeybadger
0.20.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.1
0.22.0
0.21.0
0.20.0
0.19.0
0.18.1
0.18.0
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.8.0
0.7.0
0.7.0-beta
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.0
0.3.1
0.3.0
0.2.0
0.1.2
0.1.1
0.1.0
Elixir client, Plug and error_logger for integrating with the Honeybadger.io exception tracker"
Current section
Files
Jump to
Current section
Files
lib/honeybadger/client.ex
defmodule Honeybadger.Client do
@moduledoc false
use GenServer
require Logger
@headers [
{"Accept", "application/json"},
{"Content-Type", "application/json"},
{"User-Agent", "Honeybadger Elixir"}
]
@max_connections 20
@notices_endpoint "/v1/notices"
# State
@type t :: %__MODULE__{
api_key: binary(),
enabled: boolean(),
headers: [{binary(), term()}],
proxy: binary(),
proxy_auth: {binary(), binary()},
url: binary(),
hackney_opts: keyword()
}
defstruct [
:api_key,
:enabled,
:headers,
:proxy,
:proxy_auth,
:url,
:hackney_opts
]
# API
@doc false
@spec start_link(Keyword.t()) :: GenServer.on_start()
def start_link(opts) do
GenServer.start_link(__MODULE__, new(opts), name: __MODULE__)
end
@doc false
@spec new(Keyword.t()) :: t()
def new(opts) do
%__MODULE__{
api_key: get_env(opts, :api_key),
enabled: enabled?(opts),
headers: build_headers(opts),
proxy: get_env(opts, :proxy),
proxy_auth: get_env(opts, :proxy_auth),
url: get_env(opts, :origin) <> @notices_endpoint,
hackney_opts: get_env(opts, :hackney_opts)
}
end
@doc false
@spec send_notice(map()) :: :ok
def send_notice(notice) when is_map(notice) do
if pid = Process.whereis(__MODULE__) do
GenServer.cast(pid, {:notice, notice})
else
Logger.warn(fn ->
"[Honeybadger] Unable to notify, the :honeybadger client isn't running"
end)
end
end
@doc """
Check whether reporting is enabled for the current environment.
## Example
iex> Honeybadger.Client.enabled?(environment_name: :dev, exclude_envs: [:test, :dev])
false
iex> Honeybadger.Client.enabled?(environment_name: "dev", exclude_envs: [:test, :dev])
false
iex> Honeybadger.Client.enabled?(environment_name: :dev, exclude_envs: [:test])
true
iex> Honeybadger.Client.enabled?(environment_name: "unexpected", exclude_envs: [:test])
true
"""
@spec enabled?(Keyword.t()) :: boolean
def enabled?(opts) do
env_name = get_env(opts, :environment_name)
excluded = get_env(opts, :exclude_envs)
maybe_to_atom(env_name) not in excluded
end
# Callbacks
@impl GenServer
def init(%__MODULE__{} = state) do
warn_if_incomplete_env(state)
warn_in_dev_mode(state)
:ok = :hackney_pool.start_pool(__MODULE__, max_connections: @max_connections)
{:ok, state}
end
@impl GenServer
def terminate(_reason, _state) do
:ok = :hackney_pool.stop_pool(__MODULE__)
end
@impl GenServer
def handle_cast({:notice, _notice}, %{enabled: false} = state) do
{:noreply, state}
end
def handle_cast({:notice, _notice}, %{api_key: nil} = state) do
{:noreply, state}
end
def handle_cast({:notice, notice}, %{enabled: true, url: url, headers: headers} = state) do
case Honeybadger.JSON.encode(notice) do
{:ok, payload} ->
opts =
state
|> Map.take([:proxy, :proxy_auth])
|> Enum.into(Keyword.new())
|> Keyword.put(:pool, __MODULE__)
hackney_opts =
state
|> Map.get(:hackney_opts)
|> Keyword.merge(opts)
post_notice(url, headers, payload, hackney_opts)
{:error, %Jason.EncodeError{message: message}} ->
Logger.warn(fn -> "[Honeybadger] Notice encoding failed: #{message}" end)
{:error, %Protocol.UndefinedError{description: message}} ->
Logger.warn(fn -> "[Honeybadger] Notice encoding failed: #{message}" end)
end
{:noreply, state}
end
@impl GenServer
def handle_info(message, state) do
Logger.info(fn -> "[Honeybadger] unexpected message: #{inspect(message)}" end)
{:noreply, state}
end
# API Integration
defp build_headers(opts) do
[{"X-API-Key", get_env(opts, :api_key)}] ++ @headers
end
defp post_notice(url, headers, payload, hackney_opts) do
case :hackney.post(url, headers, payload, hackney_opts) do
{:ok, code, _headers, ref} when code in 200..399 ->
body = body_from_ref(ref)
Logger.debug(fn -> "[Honeybadger] API success: #{inspect(body)}" end)
{:ok, code, _headers, ref} when code == 429 ->
body = body_from_ref(ref)
Logger.warn(fn -> "[Honeybadger] API failure: #{inspect(body)}" end)
{:ok, code, _headers, ref} when code in 400..599 ->
body = body_from_ref(ref)
Logger.warn(fn -> "[Honeybadger] API failure: #{inspect(body)}" end)
{:error, reason} ->
Logger.warn(fn -> "[Honeybadger] connection error: #{inspect(reason)}" end)
end
end
defp body_from_ref(ref) do
ref
|> :hackney.body()
|> elem(1)
end
# Incomplete Env
defp get_env(opts, key) do
Keyword.get(opts, key, Honeybadger.get_env(key))
end
defp maybe_to_atom(value) when is_binary(value), do: String.to_atom(value)
defp maybe_to_atom(value), do: value
@mandatory_keys ~w(api_key environment_name)a
defp warn_if_incomplete_env(%{enabled: true}) do
for key <- @mandatory_keys do
unless Honeybadger.get_env(key) do
Logger.error(fn ->
"[Honeybadger] Mandatory config key :#{key} not set"
end)
end
end
end
defp warn_if_incomplete_env(_state), do: :ok
defp warn_in_dev_mode(%{enabled: false}) do
Logger.info(fn ->
"[Honeybadger] Development mode is enabled. " <>
"Data will not be reported until you deploy your app."
end)
end
defp warn_in_dev_mode(_state), do: :ok
end