Packages

Live error monitoring to watch your Phoenix app going up in flames in real time! Open source version of error aggregation services. Hooks into Elixir's Logger to provide accurate error reporting all throughout your application.

Current section

Files

Jump to
flames lib flames error worker.ex
Raw

lib/flames/error/worker.ex

defmodule Flames.Error.Worker do
use GenServer
require Logger
def start_link() do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
def init(_) do
{:ok, nil}
end
def handle_cast({level, event}, _) do
handle_event(level, event)
{:noreply, nil}
end
def post_event(level, event) do
GenServer.cast(__MODULE__, {level, event})
end
def handle_event(level, data) do
repo = Application.get_env(:flames, :repo)
try do
level
|> error_changeset(data)
|> repo.insert_or_update!()
|> broadcast()
rescue
error ->
Logger.error(Exception.format(:error, error), flames: false)
{:error, error}
catch
error ->
Logger.error(error, flames: false)
{:error, error}
end
end
defp broadcast(error) do
endpoint = Application.get_env(:flames, :endpoint)
endpoint && endpoint.broadcast("errors", "error", error)
error
end
@message_regex ~r/^\s*\((?<app>.*?)\) (?<file>.*?):(?<line>\d+): (?<fun>.*)$/m
defp error_changeset(level, {Logger, msg, {date, {hour, min, sec, _ms}}, md}) do
repo = Application.get_env(:flames, :repo)
message = normalize_message(msg)
hash = hash(message.full)
timestamp =
NaiveDateTime.from_erl!({date, {hour, min, sec}}) |> DateTime.from_naive!("Etc/UTC")
if e = Flames.Error.find_reported(hash) |> repo.one() do
e
|> Flames.Error.recur_changeset(%{
count: e.count + 1,
incidents: [
%{message: message.full, timestamp: timestamp}
| Enum.map(e.incidents, &Map.from_struct/1)
]
})
else
{file, fun, line} = analyze(message.full)
Flames.Error.changeset(%Flames.Error{}, %{
message: message.full,
level: to_string(level),
timestamp: timestamp,
alive: Process.alive?(md[:pid]),
module: (md[:module] && to_string(md[:module])) || message.module,
function: md[:function] || fun,
file: md[:file] |> file_string() || file,
line: md[:line] || line,
hash: hash,
count: 1
})
end
end
defp analyze(message) do
case Regex.named_captures(@message_regex, message) do
%{"file" => file, "line" => line, "fun" => fun} -> {file, fun, line}
nil -> {nil, nil, nil}
end
end
defp normalize_message(full_message = [message, stack, fun | args])
when is_binary(stack) or is_list(stack) do
%{
message: IO.chardata_to_string(message),
stack: IO.chardata_to_string(stack),
module: fun |> IO.chardata_to_string() |> String.trim() |> String.replace("Function: ", ""),
args: args |> IO.chardata_to_string() |> String.trim() |> String.replace("Args: ", ""),
full: IO.chardata_to_string(full_message)
}
end
defp normalize_message(message) do
%{
message: IO.chardata_to_string(message),
stack: nil,
module: nil,
args: nil,
full: IO.chardata_to_string(message)
}
end
defp file_string(nil), do: nil
defp file_string(file) when is_binary(file) do
file
|> String.replace(File.cwd!() |> String.replace("flames", ""), "")
|> String.split("/", trim: true)
|> file_string()
|> Enum.join("/")
end
# Heroku
defp file_string(["tmp", build = "build_" <> _some_number | path]),
do: file_string([build | path])
defp file_string(["deps", lib | file]) do
["(#{lib}) " | file]
end
defp file_string([lib | file]) when is_binary(lib) do
["(#{lib}) " | file]
end
defp file_string(list), do: list
@args_regex ~r/Args: \[.*?\]/
@struct_regex ~r/%.*?{.*?}/
@tuple_regex ~r/{.*?}/
@function_regex ~r/#Function<\d+\.\d+\/\d+ in .*?\/\d{1}>/
@pid_regex ~r/#PID<\d+\.\d+\.\d+>/
@ref_regex ~r/#Reference<\d+\.\d+\.\d+\.\d+>/
@port_regex ~r/#Port<\d+\.\d+>/
@id_regex ~r/id: "?\d+"?/
@string_regex ~r/".*?"|'.*?'/
@number_regex ~r/\d+/
@dates_regex ~r/#(Ecto\.)?DateTime<.*?>|#<DateTime(.*?)>/
@data_equals_regex ~r/Data == .*?\*\*/
@state_equals_regex ~r/State == .*?\*\*/
@hash_ignore_regex ~r/#{@state_equals_regex.source}|#{@data_equals_regex.source}|#{
@pid_regex.source
}|#{@ref_regex.source}|#{@port_regex.source}|#{@struct_regex.source}|#{
@tuple_regex.source
}|#{@id_regex.source}|#{@function_regex.source}|#{@args_regex.source}|#{
@string_regex.source
}|#{@number_regex.source}|#{@dates_regex.source}/
def hash_ignore_regex(), do: @hash_ignore_regex
def strip_variable_data(msg), do: msg |> String.replace(hash_ignore_regex(), "")
def hash(list) when is_list(list), do: list |> hd |> hash
def hash(msg) when is_binary(msg) do
msg = strip_variable_data(msg)
:crypto.hash(:sha256, msg) |> Base.encode16()
end
end