Skip to content

Commit

Permalink
feat: basic error handling/capturing (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
grzuy committed Jun 6, 2024
1 parent e088854 commit 84b071f
Show file tree
Hide file tree
Showing 4 changed files with 282 additions and 10 deletions.
32 changes: 24 additions & 8 deletions lib/tower.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,32 @@ defmodule Tower do
Documentation for `Tower`.
"""

@doc """
Hello world.
@default_reporters [Tower.EphemeralReporter]

## Examples
def attach do
:ok = Tower.LoggerHandler.attach()
end

iex> Tower.hello()
:world
def detach do
:ok = Tower.LoggerHandler.detach()
end

"""
def hello do
:world
def report_exception(exception, stacktrace, meta \\ %{})
when is_exception(exception) and is_list(stacktrace) do
reporters()
|> Enum.each(fn reporter ->
reporter.report_exception(exception, stacktrace, meta)
end)
end

def report(type, reason, stacktrace, meta \\ %{}) when is_atom(type) and is_list(stacktrace) do
reporters()
|> Enum.each(fn reporter ->
reporter.report(type, reason, stacktrace, meta)
end)
end

def reporters do
Application.get_env(:tower, :reporters, @default_reporters)
end
end
46 changes: 46 additions & 0 deletions lib/tower/ephemeral_reporter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule Tower.EphemeralReporter do
use Agent

def start_link(_opts) do
Agent.start_link(fn -> [] end, name: __MODULE__)
end

def report_exception(exception, stacktrace, meta \\ %{})
when is_exception(exception) and is_list(stacktrace) do
Agent.update(
__MODULE__,
fn errors ->
[
%{
time: Map.get(meta, :time, :logger.timestamp()),
type: exception.__struct__,
reason: Exception.message(exception),
stacktrace: stacktrace
}
| errors
]
end
)
end

def report(type, reason, stacktrace, meta \\ %{}) when is_atom(type) and is_list(stacktrace) do
Agent.update(
__MODULE__,
fn errors ->
[
%{
time: Map.get(meta, :time, :logger.timestamp()),
type: type,
reason: reason,
stacktrace: stacktrace
}
| errors
]
end
)
end

def errors do
Agent.get(__MODULE__, & &1)
end
end
56 changes: 56 additions & 0 deletions lib/tower/logger_handler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule Tower.LoggerHandler do
@default_level :error
@handler_id :tower

def attach do
:logger.add_handler(@handler_id, __MODULE__, %{level: @default_level})
end

def detach do
:logger.remove_handler(@handler_id)
end

# :logger callbacks

def adding_handler(config) do
IO.puts("[Tower.LoggerHandler] ADDING config=#{inspect(config)}")

{:ok, config}
end

def removing_handler(config) do
IO.puts("[Tower.LoggerHandler] REMOVING config=#{inspect(config)}")

:ok
end

def log(%{level: :error, meta: %{crash_reason: {exception, stacktrace}} = meta}, _config)
when is_exception(exception) and is_list(stacktrace) do
IO.puts("[Tower.LoggerHandler] EXCEPTION #{inspect(exception)}")

Tower.report_exception(exception, stacktrace, meta)
end

def log(
%{level: :error, meta: %{crash_reason: {{:nocatch, reason}, stacktrace}} = meta},
_config
)
when is_list(stacktrace) do
IO.puts("[Tower.LoggerHandler] NOCATCH #{inspect(reason)}")

Tower.report(:nocatch, reason, stacktrace, meta)
end

def log(%{level: :error, meta: %{crash_reason: {exit_reason, stacktrace}} = meta}, _config)
when is_list(stacktrace) do
IO.puts("[Tower.LoggerHandler] EXIT #{inspect(exit_reason)}")

Tower.report(:exit, exit_reason, stacktrace, meta)
end

def log(log_event, _config) do
IO.puts(
"[Tower.LoggerHandler] UNHANDLED LOG EVENT log_event=#{inspect(log_event, pretty: true)}"
)
end
end
158 changes: 156 additions & 2 deletions test/tower_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,161 @@ defmodule TowerTest do
use ExUnit.Case
doctest Tower

test "greets the world" do
assert Tower.hello() == :world
setup do
Tower.attach()

on_exit(fn ->
Tower.detach()
end)
end

test "starts with 0 exceptions" do
Tower.EphemeralReporter.start_link([])

assert [] = Tower.EphemeralReporter.errors()
end

test "reports arithmetic error" do
Tower.EphemeralReporter.start_link([])

in_unlinked_process(fn ->
1 / 0
end)

assert(
[
%{
time: _,
type: ArithmeticError,
reason: "bad argument in arithmetic expression",
stacktrace: stacktrace
}
] = Tower.EphemeralReporter.errors()
)

assert is_list(stacktrace)
end

test "reports a raise" do
Tower.EphemeralReporter.start_link([])

in_unlinked_process(fn ->
raise "error inside process"
end)

assert(
[
%{
time: _,
type: RuntimeError,
reason: "error inside process",
stacktrace: stacktrace
}
] = Tower.EphemeralReporter.errors()
)

assert is_list(stacktrace)
end

test "reports a thrown string" do
Tower.EphemeralReporter.start_link([])

in_unlinked_process(fn ->
throw("error")
end)

assert(
[
%{
time: _,
type: :nocatch,
reason: "error",
stacktrace: stacktrace
}
] = Tower.EphemeralReporter.errors()
)

assert is_list(stacktrace)
end

test "reports a thrown non-string" do
Tower.EphemeralReporter.start_link([])

in_unlinked_process(fn ->
throw(something: "here")
end)

assert(
[
%{
time: _,
type: :nocatch,
reason: [something: "here"],
stacktrace: stacktrace
}
] = Tower.EphemeralReporter.errors()
)

assert is_list(stacktrace)
end

test "doesn't report an normal exit" do
Tower.EphemeralReporter.start_link([])

in_unlinked_process(fn ->
exit(:normal)
end)

assert [] = Tower.EphemeralReporter.errors()
end

test "reports an abnormal exit" do
Tower.EphemeralReporter.start_link([])

in_unlinked_process(fn ->
exit(:abnormal)
end)

assert(
[
%{
time: _,
type: :exit,
reason: :abnormal,
stacktrace: stacktrace
}
] = Tower.EphemeralReporter.errors()
)

assert is_list(stacktrace)
end

test "reports a kill exit" do
Tower.EphemeralReporter.start_link([])

in_unlinked_process(fn ->
exit(:kill)
end)

assert(
[
%{
time: _,
type: :exit,
reason: :kill,
stacktrace: stacktrace
}
] = Tower.EphemeralReporter.errors()
)

assert is_list(stacktrace)
end

defp in_unlinked_process(fun) when is_function(fun, 0) do
{:ok, pid} = Task.Supervisor.start_link()

pid
|> Task.Supervisor.async_nolink(fun)
|> Task.yield()
end
end

0 comments on commit 84b071f

Please sign in to comment.