Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: basic error handling/capturing #3

Merged
merged 11 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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