Current section
Files
Jump to
Current section
Files
lib/rollbax/client.ex
defmodule Rollbax.Client do
@moduledoc false
# This GenServer keeps a pre-built bare-bones version of an exception (a
# "draft") to be reported to Rollbar, which is then filled with the data
# related to each specific exception when such exception is being
# reported. This GenServer is also responsible for actually sending data to
# the Rollbar API and receiving responses from said API.
use GenServer
require Logger
alias Rollbax.Item
@api_url "https://api.rollbar.com/api/1/item/"
@headers [{"content-type", "application/json"}]
## GenServer state
defstruct [:draft, :url, :enabled, hackney_responses: %{}]
## Public API
def start_link(token, environment, enabled, custom, url \\ @api_url) do
state = new(token, environment, url, enabled, custom)
GenServer.start_link(__MODULE__, state, [name: __MODULE__])
end
def emit(level, timestamp, body, custom, occurrence_data) do
if pid = Process.whereis(__MODULE__) do
event = {Atom.to_string(level), timestamp, body, custom, occurrence_data}
GenServer.cast(pid, {:emit, event})
else
Logger.warn("(Rollbax) Trying to report an exception but the :rollbax application has not been started", rollbax: false)
end
end
## GenServer callbacks
def init(state) do
Logger.metadata(rollbax: false)
:ok = :hackney_pool.start_pool(__MODULE__, [max_connections: 20])
{:ok, state}
end
def terminate(_reason, _state) do
:ok = :hackney_pool.stop_pool(__MODULE__)
end
def handle_cast({:emit, _event}, %{enabled: false} = state) do
{:noreply, state}
end
def handle_cast({:emit, event}, %{enabled: :log} = state) do
Logger.info [
"(Rollbax) registered report.\n", event_to_chardata(event),
]
{:noreply, state}
end
def handle_cast({:emit, event}, %{enabled: true} = state) do
case compose_json(state.draft, event) do
{:ok, payload} ->
opts = [:async, pool: __MODULE__]
case :hackney.post(state.url, @headers, payload, opts) do
{:ok, _ref} -> :ok
{:error, reason} ->
Logger.error("(Rollbax) connection error: #{inspect(reason)}")
end
{:error, exception} ->
Logger.error [
"(Rollbax) failed to encode report below ",
"for reason: ", Exception.message(exception),
?\n, event_to_chardata(event),
]
end
{:noreply, state}
end
def handle_info({:hackney_response, ref, response}, state) do
new_state = handle_hackney_response(ref, response, state)
{:noreply, new_state}
end
def handle_info(message, state) do
Logger.info("(Rollbax) unexpected message: #{inspect(message)}")
{:noreply, state}
end
## Helper functions
defp new(token, environment, url, enabled, custom) do
draft = Item.draft(token, environment, custom)
%__MODULE__{draft: draft, url: url, enabled: enabled}
end
defp compose_json(draft, event) do
item = Item.compose(draft, event)
# We use `try/1` here instead of `Poison.decode/1`
# since under some circumstances it
# still unsafe and will raise an exception.
try do
{:ok, Poison.encode!(item, iodata: true)}
rescue
exception -> {:error, exception}
end
end
defp event_to_chardata({level, timestamp, body, custom, occurrence_data}) do
[
inspect(body),
"\nLevel: ", level,
"\nTimestamp: ", Integer.to_string(timestamp),
"\nCustom data: ", inspect(custom),
"\nOccurrence data: ", inspect(occurrence_data),
]
end
defp handle_hackney_response(ref, :done, %{hackney_responses: responses} = state) do
body = responses |> Map.fetch!(ref) |> IO.iodata_to_binary()
case Poison.decode(body) do
{:ok, %{"err" => 1, "message" => message}} when is_binary(message) ->
Logger.error("(Rollbax) API returned an error: #{inspect message}")
{:ok, response} ->
Logger.debug("(Rollbax) API response: #{inspect response}")
{:error, _} ->
Logger.error("(Rollbax) API returned malformed JSON: #{inspect body}")
end
%{state | hackney_responses: Map.delete(responses, ref)}
end
defp handle_hackney_response(ref, {:status, code, description}, %{hackney_responses: responses} = state) do
if code != 200 do
Logger.error("(Rollbax) unexpected API status: #{code}/#{description}")
end
%{state | hackney_responses: Map.put(responses, ref, [])}
end
defp handle_hackney_response(_ref, {:headers, headers}, state) do
Logger.debug("(Rollbax) API headers: #{inspect(headers)}")
state
end
defp handle_hackney_response(ref, body_chunk, %{hackney_responses: responses} = state)
when is_binary(body_chunk) do
%{state | hackney_responses: Map.update!(responses, ref, &[&1 | body_chunk])}
end
defp handle_hackney_response(ref, {:error, reason}, %{hackney_responses: responses} = state) do
Logger.error("(Rollbax) connection error: #{inspect(reason)}")
%{state | hackney_responses: Map.delete(responses, ref)}
end
end