Skip to content

Commit

Permalink
more
Browse files Browse the repository at this point in the history
  • Loading branch information
grzuy committed Sep 20, 2024
1 parent 602bcc5 commit a173d5d
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 48 deletions.
46 changes: 26 additions & 20 deletions lib/tower.ex
Original file line number Diff line number Diff line change
Expand Up @@ -250,26 +250,20 @@ defmodule Tower do
when reason == :normal or reason == :shutdown or
(is_tuple(reason) and tuple_size(reason) == 2 and elem(reason, 0) == :shutdown)

@doc """
Attaches the necessary handlers to automatically listen for application errors.
[Adds](https://www.erlang.org/doc/apps/kernel/logger.html#add_handler/3) a new
[`logger_handler`](https://www.erlang.org/doc/apps/kernel/logger_handler), which listens for all
uncaught exceptions, uncaught throws, abnormal process exits, among other log events of interest.
use Agent

Additionally adds other handlers specifically tailored for some packages that
do catch errors and have their own specific error handling and emit events instead
of letting errors get to the logger handler, like oban or bandit.
def start_link(opts) do
name = Keyword.get(opts, :name, __MODULE__)

Note that `Tower.attach/0` is not a precondition for `Tower` `handle_*` functions to work
properly and inform reporters. They are independent.
"""
@spec attach(atom()) :: :ok
def attach(opts \\ []) do
{:ok, config} = Tower.Config.from_opts(opts)
:ok = Tower.LoggerHandler.attach(Module.concat(name, :LoggerHandler))
:ok = Tower.LoggerHandler.attach(name)
:ok = Tower.BanditExceptionHandler.attach(Module.concat(name, :BanditExceptionHandler))
:ok = Tower.ObanExceptionHandler.attach(Module.concat(name, :ObanExceptionHandler))

Agent.start_link(fn -> opts end, name: name)
end

def reporters(name \\ __MODULE__) do
Agent.get(name, &Keyword.get(&1, :reporters, global_reporters()))
end

@doc """
Expand Down Expand Up @@ -333,7 +327,6 @@ defmodule Tower do
* Accepts same options as `handle_caught/4#options`.
"""
@spec handle_exception(Exception.t(), Exception.stacktrace()) :: :ok
@spec handle_exception(Exception.t(), Exception.stacktrace(), Keyword.t()) :: :ok
def handle_exception(exception, stacktrace, options \\ [])
when is_exception(exception) and is_list(stacktrace) do
Expand Down Expand Up @@ -433,8 +426,8 @@ defmodule Tower do
:logger.compare_levels(level1, level2) in [:gt, :eq]
end

defp report_event(%Event{} = event) do
reporters()
defp report_event(%Event{tower: tower} = event) do
reporters(tower)
|> Enum.each(fn reporter ->
report_event(reporter, event)
end)
Expand All @@ -445,6 +438,19 @@ defmodule Tower do
:ignore
end

defp report_event({reporter_module, pid}, event) do
async(fn ->
try do
reporter_module.report_event(pid, event)
rescue
exception ->
raise ReportEventError,
reporter: reporter_module,
original: {:error, exception, __STACKTRACE__}
end
end)
end

defp report_event(reporter, event) do
async(fn ->
try do
Expand All @@ -458,7 +464,7 @@ defmodule Tower do
end)
end

defp reporters do
defp global_reporters do
Application.fetch_env!(:tower, :reporters)
end

Expand Down
10 changes: 8 additions & 2 deletions lib/tower/ephemeral_reporter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ defmodule Tower.EphemeralReporter do

alias Tower.Event

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

def stop(pid) do
Expand All @@ -48,6 +48,12 @@ defmodule Tower.EphemeralReporter do
end
end

def report_event(pid, %Event{level: level} = event) do
if Tower.equal_or_greater_level?(level, @default_level) do
Agent.update(pid, fn events -> [event | events] end)
end
end

@doc """
Returns the list of all stored events.
"""
Expand Down
9 changes: 6 additions & 3 deletions lib/tower/event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ defmodule Tower.Event do
:stacktrace,
:log_event,
:plug_conn,
:metadata
:metadata,
:tower
]

@type error_kind :: :error | :exit | :throw
Expand All @@ -40,7 +41,8 @@ defmodule Tower.Event do
stacktrace: Exception.stacktrace() | nil,
log_event: :logger.log_event() | nil,
plug_conn: struct() | nil,
metadata: map()
metadata: map(),
tower: atom()
}

@similarity_source_attributes [:level, :kind, :reason, :stacktrace, :metadata]
Expand Down Expand Up @@ -133,7 +135,8 @@ defmodule Tower.Event do
datetime: event_datetime(log_event),
log_event: log_event,
plug_conn: plug_conn(options),
metadata: Keyword.get(options, :metadata, %{})
metadata: Keyword.get(options, :metadata, %{}),
tower: Keyword.get(options, :tower, Tower)
}
end

Expand Down
31 changes: 19 additions & 12 deletions lib/tower/logger_handler.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
defmodule Tower.LoggerHandler do
@moduledoc false

@default_handler_id __MODULE__
@own_logs_domain [:tower, :logger_handler]

require Logger

@spec attach() :: :ok | {:error, term()}
def attach(handler_id \\ @default_handler_id) do
def attach(name \\ Tower) do
:logger.add_handler(
handler_id,
handler_id(name),
__MODULE__,
%{
level: :all,
Expand All @@ -18,14 +17,15 @@ defmodule Tower.LoggerHandler do
&:logger_filters.domain/2,
{:stop, :sub, [:elixir | @own_logs_domain]}
}
]
],
config: [name: name]
}
)
end

@spec detach() :: :ok | {:error, term()}
def detach(handler_id \\ @default_handler_id) do
:logger.remove_handler(handler_id)
def detach(name \\ Tower) do
:logger.remove_handler(handler_id(name))
end

# :logger callbacks
Expand All @@ -38,42 +38,45 @@ defmodule Tower.LoggerHandler do
:ok
end

def log(log_event, _config) do
handle_log_event(log_event)
def log(log_event, %{config: [name: name]}) do
handle_log_event(name, log_event)
end

defp handle_log_event(
name,
%{level: :error, meta: %{crash_reason: {exception, stacktrace}}} = log_event
)
when is_exception(exception) and is_list(stacktrace) do
Tower.handle_exception(exception, stacktrace, log_event: log_event)
Tower.handle_exception(exception, stacktrace, log_event: log_event, tower: name)
end

defp handle_log_event(
_name,
%{level: :error, meta: %{crash_reason: {{:nocatch, reason}, stacktrace}}} = log_event
)
when is_list(stacktrace) do
Tower.handle_throw(reason, stacktrace, log_event: log_event)
end

defp handle_log_event(
_name,
%{level: :error, meta: %{crash_reason: {exit_reason, stacktrace}}} = log_event
)
when is_list(stacktrace) do
Tower.handle_exit(exit_reason, stacktrace, log_event: log_event)
end

defp handle_log_event(%{level: :error, meta: %{crash_reason: exit_reason}} = log_event) do
defp handle_log_event(_name, %{level: :error, meta: %{crash_reason: exit_reason}} = log_event) do
Tower.handle_exit(exit_reason, [], log_event: log_event)
end

defp handle_log_event(%{level: level, msg: {:string, reason_chardata}} = log_event) do
defp handle_log_event(_name, %{level: level, msg: {:string, reason_chardata}} = log_event) do
if should_handle?(level) do
Tower.handle_message(level, IO.chardata_to_string(reason_chardata), log_event: log_event)
end
end

defp handle_log_event(%{level: level, msg: {:report, report}} = log_event) do
defp handle_log_event(_name, %{level: level, msg: {:report, report}} = log_event) do
if should_handle?(level) do
Tower.handle_message(level, report, log_event: log_event)
end
Expand Down Expand Up @@ -106,4 +109,8 @@ defmodule Tower.LoggerHandler do
defp safe_log(level, message) do

Check warning on line 109 in lib/tower/logger_handler.ex

View workflow job for this annotation

GitHub Actions / main (1.15, 25.3.2.12)

function safe_log/2 is unused

Check warning on line 109 in lib/tower/logger_handler.ex

View workflow job for this annotation

GitHub Actions / main (1.15, 24.3.4.17)

function safe_log/2 is unused
Logger.log(level, message, domain: @own_logs_domain)
end

defp handler_id(name \\ Tower) do

Check warning on line 113 in lib/tower/logger_handler.ex

View workflow job for this annotation

GitHub Actions / main (1.15, 25.3.2.12)

default values for the optional arguments in handler_id/1 are never used

Check warning on line 113 in lib/tower/logger_handler.ex

View workflow job for this annotation

GitHub Actions / main (1.15, 24.3.4.17)

default values for the optional arguments in handler_id/1 are never used
Module.concat(name, :LoggerHandler)
end
end
22 changes: 11 additions & 11 deletions test/tower_multiple_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ defmodule TowerMultipleTest do
# import ExUnit.CaptureLog, only: [capture_log: 1]

test "multiple towers" do
{:ok, ephemeral_reporter_1} = Tower.EphemeralReporter.start_link([])
{:ok, ephemeral_reporter_2} = Tower.EphemeralReporter.start_link([])
{:ok, ephemeral_reporter_1} = Tower.EphemeralReporter.start_link(name: Tower1.EphemeralReporter)
{:ok, ephemeral_reporter_2} = Tower.EphemeralReporter.start_link(name: Tower2.EphemeralReporter)

assert [] = Tower.EphemeralReporter.events(ephemeral_reporter_1)
assert [] = Tower.EphemeralReporter.events(ephemeral_reporter_2)
Expand All @@ -18,22 +18,22 @@ defmodule TowerMultipleTest do
assert [] = Tower.EphemeralReporter.events(ephemeral_reporter_1)
assert [] = Tower.EphemeralReporter.events(ephemeral_reporter_2)

Tower.attach(Tower1, reporters: [{Tower.EphemeralReporter, ephemeral_reporter_1}])
Tower.attach(Tower2, reporters: [{Tower.EphemeralReporter, ephemeral_reporter_2}])
{:ok, tower_1} = Tower.start_link(name: Tower1, reporters: [{Tower.EphemeralReporter, ephemeral_reporter_1}])
{:ok, tower_2} = Tower.start_link(name: Tower2, reporters: [{Tower.EphemeralReporter, ephemeral_reporter_2}])

spawn(fn -> 1 / 0 end)
Process.sleep(200)

assert [_event] = Tower.EphemeralReporter.events(ephemeral_reporter_1)
assert [_event] = Tower.EphemeralReporter.events(ephemeral_reporter_2)

:ok = Tower.detach(Tower1)

spawn(fn -> 1 / 0 end)
Process.sleep(200)

assert [_event] = Tower.EphemeralReporter.events(ephemeral_reporter_1)
assert [_event1, _event2] = Tower.EphemeralReporter.events(ephemeral_reporter_2)
# :ok = Tower.detach(Tower1)
#
# spawn(fn -> 1 / 0 end)
# Process.sleep(200)
#
# assert [_event] = Tower.EphemeralReporter.events(ephemeral_reporter_1)
# assert [_event1, _event2] = Tower.EphemeralReporter.events(ephemeral_reporter_2)
# :ok = Tower.EphemeralReporter.stop(ephemeral_reporter_1)
end
end

0 comments on commit a173d5d

Please sign in to comment.