Packages

appsignal

0.6.0
2.17.4 2.17.3 2.17.2 2.17.1 2.17.0 2.16.0 2.15.11 2.15.10 2.15.9 2.15.8 2.15.7 2.15.6 2.15.5 2.15.4 2.15.3 2.15.2 2.15.1 2.15.0 2.14.1 2.14.0 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.3 2.12.2 2.12.1 2.12.0 2.11.0 2.10.2 2.10.1 2.10.0 2.9.2 2.9.1 2.9.0 2.8.3 2.8.2 2.8.1 2.8.0 2.7.13 2.7.12 2.7.11 2.7.10 2.7.9 2.7.8 2.7.7 2.7.6 2.7.5 2.7.4 2.7.3 2.7.2 2.7.1 2.7.0 2.7.0-beta.1 2.6.1 2.6.0 2.5.3 2.5.2 2.5.1 2.5.0 2.4.3 2.4.2 2.4.1 2.4.0 2.3.1 2.3.0 2.2.19 2.2.18 2.2.17 2.2.16 2.2.15 2.2.14 2.2.13 2.2.12 retired 2.2.11 2.2.10 2.2.9 2.2.8 2.2.7 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 2.2.1 2.2.0 2.1.15 2.1.14 2.1.13 2.1.12 2.1.11 2.1.10 2.1.9 2.1.8 2.1.7 2.1.7-beta.1 2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.8 2.0.7 2.0.6 2.0.6-beta.1 2.0.5 retired 2.0.4 retired 2.0.3 retired 2.0.2 retired 2.0.1 retired 2.0.0 retired 2.0.0-beta.11 2.0.0-beta.10 2.0.0-beta.9 2.0.0-beta.8 2.0.0-beta.7 2.0.0-beta.6 2.0.0-beta.5 2.0.0-beta.4 2.0.0-beta.3 2.0.0-beta.2 2.0.0-beta.1 2.0.0-alpha.7 2.0.0-alpha.6 2.0.0-alpha.5 2.0.0-alpha.4 2.0.0-alpha.3 2.0.0-alpha.2 2.0.0-alpha.1 1.13.5 1.13.4 1.13.3 1.13.2 1.13.1 1.13.0 1.13.0-beta.2 1.13.0-beta.1 1.12.1 1.12.0 1.12.0-beta.3 1.12.0-beta.2 1.12.0-beta.1 1.11.8 1.11.7 1.11.6 1.11.5 1.11.4 1.11.3 1.11.2 1.11.1 1.11.0 1.11.0-beta.1 1.10.13 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.4 1.9.3 1.9.2 1.9.1 1.9.1-beta.1 1.9.0 1.9.0-beta.1 1.9.0-alpha.1 1.8.2 1.8.1 1.8.1-beta.2 1.8.1-beta.1 1.8.0 1.7.2 1.7.1 1.7.0 1.7.0-beta.2 1.7.0-beta.1 1.7.0-alpha.4 1.7.0-alpha.3 1.7.0-alpha.2 1.7.0-alpha.1 1.6.7 1.6.6 1.6.6-beta.2 1.6.6-beta.1 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.6.0-beta.1 1.6.0-alpha.1 1.5.1 1.5.0 1.5.0-beta.2 1.5.0-beta.1 1.4.11-beta.1 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.4.0-alpha.1 1.3.6 1.3.6-beta.1 1.3.5 1.3.4 1.3.4-beta.1 1.3.3 1.3.2 1.3.1 1.3.0 1.3.0-beta.3 1.3.0-beta.2 1.3.0-beta.1 1.2.3 1.2.2 1.2.1 1.2.0 1.1.1 1.1.0 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 1.0.0-rc.1 0.13.0 0.12.3 0.12.2 0.12.1 0.12.0 0.11.6 0.11.5 0.11.4 0.11.3 0.11.2 0.11.1 0.11.0 0.10.0 0.9.2 0.9.1 0.9.0 0.8.0 0.7.1 0.7.0 0.6.2 0.6.1 0.6.0 0.5.0 0.4.0 0.3.0 0.2.0 0.1.0 0.0.9 0.0.8 0.0.7 0.0.6 0.0.5 0.0.4 0.0.3 0.0.2 0.0.1

Collects error and performance data from your Elixir applications and sends it to AppSignal

Current section

Files

Jump to
appsignal lib appsignal error_handler.ex
Raw

lib/appsignal/error_handler.ex

defmodule Appsignal.ErrorHandler do
@moduledoc """
Error handler which sends all crash reports to the Appsignal backend.
When you add `:appsignal` to your application's dependencies, this
error logger will automatically be installed. All processes that are
supervised, like GenServers, Tasks, Agents, Supervisored will be
monitored for crashes. In the case of a crash, the Appsignal error
handler collects error information and sends it to the backend.
"""
use GenEvent
require Logger
alias Appsignal.{Transaction,TransactionRegistry}
@doc """
Retrieve the last Appsignal.Transaction.t that the error logger picked up
"""
@spec get_last_transaction :: Transaction.t | nil
def get_last_transaction do
GenEvent.call(:error_logger, Appsignal.ErrorHandler, :get_last_transaction)
end
def init(_) do
# state of the error handler holds the last matched transaction
{:ok, nil}
end
def handle_event(event, state) do
state =
case match_event(event) do
{origin, reason, message, stack, metadata} ->
case supervisor_event?(reason) do
false ->
transaction = lookup_or_create_transaction(origin)
if transaction != nil do
submit_transaction(transaction, reason, message, stack, metadata)
end
true ->
# ignore this event; we have already handled it.
state
end
:nomatch ->
state
end
{:ok, state}
end
defp lookup_or_create_transaction(origin) do
case TransactionRegistry.lookup(origin, true) do
:removed ->
# transaction existed but has already been submitted, on the timer to be removed
nil
nil ->
# could not find a linked transaction; start new transaction
Transaction.start(generate_id(), :background_job)
t ->
t
end
end
defp submit_transaction(transaction, reason, message, stack, metadata) do
Transaction.set_error(transaction, reason, message, stack)
if metadata[:conn] != nil do
Transaction.set_request_metadata(transaction, metadata[:conn])
end
Transaction.finish(transaction)
Transaction.complete(transaction)
Logger.debug("Submitting #{inspect transaction}: #{message}")
transaction
end
# Generate a transaction id for crashed processed that have no
# associated transaction. Prefixed with '_' to indicate a new transaction was createdk.
defp generate_id do
"_" <> Transaction.generate_id()
end
# inspect the 'reason' argument to see if it is a supervisor
# event. if so, we skip submitting it, because the original event
# has already been processed.
defp supervisor_event?({:exit, {_reason, [entry|_]}}) do
case entry do
{_,_,_} -> true
{_,_,_,_} -> true
_ -> false
end
end
defp supervisor_event?(_) do
false
end
def handle_call(:get_last_transaction, state) do
{:ok, state, state}
end
@doc false
@spec match_event(term) :: {pid, term, String.t, list, Map.t} | :nomatch
def match_event({:error, _gleader, {_pid, format, data}}) do
match_error_format(format, data)
end
def match_event({:error_report, _gleader, {origin, :crash_report, report}}) do
match_error_report(origin, report)
end
def match_event(_event) do
:nomatch
end
defp match_error_report(origin, [[{:initial_call, _},
{:pid, pid},
{:registered_name, name},
{:error_info, {kind, exception, stack}} | _], _linked]) do
reason = {kind, exception}
case supervisor_event?(reason) do
false ->
msg = "Process #{crash_name(pid, name)} terminating"
{origin, "#{inspect reason}", msg, format_stack(stack), %{}}
true ->
:nomatch
end
end
# Match on the various format strings that OTP gives us to extract the
# error information like stack traces et cetera.
# TODO: Add crashes caused by gen_event handlers
defp match_error_format('Error in process ' ++ _, [pid, {reason, stack}]) do
msg = "Process #{inspect pid} raised an exception"
{reason, msg} = extract_reason_and_message(reason, msg)
{pid, reason, msg, format_stack(stack), %{}}
end
defp match_error_format('** Generic server ' ++ _, [pid, _last, _state, reason]) do
{reason, stack} = maybe_extract_stack(reason)
{reason, msg} = extract_reason_and_message(reason, "GenServer #{inspect pid} terminating")
{pid, reason, msg, format_stack(stack), %{}}
end
defp match_error_format('** Task ' ++ _, [pid, starter, function, args, reason]) do
{reason, stack} = maybe_extract_stack(reason)
msg = "Task #{inspect pid} started from #{inspect starter} terminating. Function: #{inspect function}, args: #{inspect args}"
{reason, msg} = extract_reason_and_message(reason, msg)
{pid, reason, msg, format_stack(stack), %{}}
end
# FIXME add test coverage for this one
defp match_error_format('Ranch listener ' ++ _, [_, _, pid, {{reason, stack}, initial}]) do
metadata = case extract_conn(initial) do
nil -> %{}
c -> %{conn: c}
end
msg = "HTTP request #{inspect pid} crashed"
{reason, msg} = extract_reason_and_message(reason, msg)
{pid, reason, msg, format_stack(stack), metadata}
end
# Format the stack trace as an array of strings
@doc false
def format_stack(stacktrace) do
for entry <- stacktrace do
Exception.format_stacktrace_entry(entry)
end
end
# Extract stack trace from GenServer crash report reason
defp maybe_extract_stack({maybe_exception, [_ | _ ] = maybe_stacktrace} = reason) do
try do
{maybe_exception, maybe_stacktrace}
catch
:error, _ ->
{reason, []}
end
end
defp maybe_extract_stack(reason) do
{reason, []}
end
defp crash_name(pid, []), do: inspect(pid)
defp crash_name(pid, name), do: "#{inspect(name)} (#{inspect(pid)})"
defp extract_conn({_, :call, [%Plug.Conn{} = conn, _params]}), do: conn
defp extract_conn(_), do: nil
@doc """
Extract a consise reason from the given error reason, stripping it from long stack traces and the like.
Also returns a 'message' which is supposed to contain extra error information.
"""
@spec extract_reason_and_message(any(), binary()) :: {any(), binary()}
def extract_reason_and_message(reason, message) when is_binary(reason) do
{reason, message}
end
def extract_reason_and_message(reason, message) when is_atom(reason) do
try do
{Exception.message(reason.exception([])), message}
rescue
UndefinedFunctionError -> {"#{inspect reason}", message}
end
end
def extract_reason_and_message(%Protocol.UndefinedError{value: {:error, {error = %{}, _stack}}}, message) do
extract_reason_and_message(error, message)
end
def extract_reason_and_message(%Phoenix.Template.UndefinedError{assigns: %{conn: %{assigns: %{kind: :error, reason: reason}}}}, message) do
extract_reason_and_message(reason, message)
end
def extract_reason_and_message(r = %{}, message) do
msg = Exception.message(r)
{"#{inspect r.__struct__}", message <> ": " <> msg}
end
def extract_reason_and_message(any, message) do
# inspect any term; truncate it
{"#{inspect any}", message}
end
end