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 logger.ex
Raw

lib/flames/logger.ex

defmodule Flames.Logger do
require Logger
use GenEvent
config_message = """
Please configure the repo Flames should use in your config.exs file.
config :flames,
repo: MyApp.Repo,
endpoint: MyApp.Endpoint \# (Optional, if using Phoenix)
"""
@repo Application.get_env(:flames, :repo) || raise(config_message)
@endpoint Application.get_env(:flames, :endpoint) || raise(config_message)
def init(_) do
{:ok, configure}
end
def handle_event({_level, gl, _event}, state) when node(gl) != node() do
{:ok, state}
end
def handle_event({level, _gl, event}, state) do
if proceed?(event) && meet_level?(level) do
Task.start(__MODULE__, :post_event, [level, event])
end
{:ok, state}
end
defp proceed?({Logger, _msg, _event_time, meta}) do
Keyword.get(meta, :flames, true)
end
defp meet_level?(lvl) do
Logger.compare_levels(lvl, :warn) in [:gt, :eq]
end
def post_event(level, data) do
level
|> error_changeset(data)
|> @repo.insert_or_update()
|> broadcast()
end
defp broadcast({:ok, error}) do
@endpoint.broadcast("errors", "error", error)
end
defp broadcast({:error, _}), do: nil
defp configure(options \\ []) do
options = Keyword.merge(options, [])
flames_config = Keyword.merge(Application.get_env(:logger, :flames, []), options)
Application.put_env(:logger, :flames, flames_config)
end
defp error_changeset(level, {Logger, msg, ts, md}) do
msg = IO.chardata_to_string(msg)
hash = hash(msg)
if e = @repo.get_by(Flames.Error, hash: hash) do
Flames.Error.changeset(e, %{count: e.count + 1})
else
Flames.Error.changeset(%Flames.Error{}, %{
message: msg,
level: to_string(level),
timestamp: ts,
alive: Process.alive?(md[:pid]),
module: md[:module] && to_string(md[:module]),
function: md[:function],
file: md[:file] |> file_string(),
line: md[:line],
hash: hash,
count: 1
})
end
end
@cwd File.cwd! |> String.replace("flames", "")
defp file_string(nil), do: nil
defp file_string(file) when is_binary(file) do
file
|> String.replace(@cwd, "")
|> String.split("/")
|> file_string()
end
defp file_string(["deps", lib | file]) do
["(#{lib})" | file]
end
defp file_string([lib | file]) do
["(#{lib})" | file]
end
@pid_regex ~r/#PID<\d+\.\d+\.\d+>/
@id_regex ~r/id: \d+/
@dates_regex ~r/#(Ecto\.)?DateTime<.*?>|#<DateTime(.*?)>/
@hash_ignore_regex ~r/#{@pid_regex.source}|#{@id_regex.source}|#{@dates_regex.source}/
def hash(list) when is_list(list), do: list |> hd |> hash
def hash(msg) when is_binary(msg) do
msg = msg |> String.replace(@hash_ignore_regex, "")
:crypto.hash(:sha256, msg) |> Base.encode16
end
end