Skip to content

Commit

Permalink
Merge branch 'main' into unrecognized
Browse files Browse the repository at this point in the history
  • Loading branch information
grzuy committed Jul 31, 2024
2 parents c36df52 + a5d2946 commit c79fa62
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 86 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ jobs:
_build
deps
key: ${{ runner.os }}-${{ matrix.elixir }}-${{ matrix.erlang }}-${{ hashFiles('mix.lock') }}
- uses: actions/cache@v4
with:
# Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones
key: ${{ runner.os }}-${{ matrix.elixir }}-${{ matrix.erlang }}-plt
path: priv/plts
if: ${{ matrix.lint }}
- uses: erlef/setup-beam@v1
with:
otp-version: ${{ matrix.erlang }}
Expand All @@ -40,4 +46,6 @@ jobs:
- run: mix deps.unlock --check-unused
if: ${{ matrix.lint }}
- run: mix compile --warnings-as-errors
- run: mix dialyzer --format github
if: ${{ matrix.lint }}
- run: mix test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ tower-*.tar

# Temporary files, for example, from tests.
/tmp/

/priv/plts
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Tower

[![ci](https://github.com/mimiquate/tower/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mimiquate/tower/actions?query=branch%3Amain)
[![Hex.pm](https://img.shields.io/hexpm/v/tower.svg)](https://hex.pm/packages/tower)
[![Docs](https://img.shields.io/badge/docs-gray.svg)](https://hexdocs.pm/tower)

Solid error handling and reporting

## Installation
Expand Down
32 changes: 18 additions & 14 deletions lib/tower.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule Tower do
Documentation for `Tower`.
"""

alias Tower.Event

@default_reporters [Tower.EphemeralReporter]

def attach do
Expand All @@ -13,28 +15,30 @@ defmodule Tower do
:ok = Tower.LoggerHandler.detach()
end

def handle_exception(exception, stacktrace, meta \\ %{})
def handle_exception(exception, stacktrace, options \\ [])
when is_exception(exception) and is_list(stacktrace) do
each_reporter(fn reporter ->
reporter.report_exception(exception, stacktrace, meta)
end)
Event.from_exception(exception, stacktrace, options)
|> report_event()
end

def handle_throw(reason, stacktrace, metadata \\ %{}) do
each_reporter(fn reporter ->
reporter.report_throw(reason, stacktrace, metadata)
end)
def handle_throw(reason, stacktrace, options \\ []) do
Event.from_throw(reason, stacktrace, options)
|> report_event()
end

def handle_exit(reason, stacktrace, metadata \\ %{}) do
each_reporter(fn reporter ->
reporter.report_exit(reason, stacktrace, metadata)
end)
def handle_exit(reason, stacktrace, options \\ []) do
Event.from_exit(reason, stacktrace, options)
|> report_event()
end

def handle_message(level, message, options \\ []) do
Event.from_message(level, message, options)
|> report_event()
end

def handle_message(level, message, metadata \\ %{}) do
defp report_event(%Event{} = event) do
each_reporter(fn reporter ->
reporter.report_message(level, message, metadata)
reporter.report_event(event)
end)
end

Expand Down
26 changes: 12 additions & 14 deletions lib/tower/ephemeral_reporter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,28 @@ defmodule Tower.EphemeralReporter do

use Agent

alias Tower.Event

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

@impl true
def report_exception(exception, stacktrace, metadata \\ %{})
when is_exception(exception) and is_list(stacktrace) do
add_error(exception.__struct__, Exception.message(exception), stacktrace, metadata)
def report_event(%Event{time: time, kind: :error, reason: exception, stacktrace: stacktrace}) do
add_error(time, exception.__struct__, Exception.message(exception), stacktrace)
end

@impl true
def report_throw(reason, stacktrace, metadata \\ %{}) do
add_error(:throw, reason, stacktrace, metadata)
def report_event(%Event{time: time, kind: :exit, reason: reason, stacktrace: stacktrace}) do
add_error(time, :exit, reason, stacktrace)
end

@impl true
def report_exit(reason, stacktrace, metadata \\ %{}) do
add_error(:exit, reason, stacktrace, metadata)
def report_event(%Event{time: time, kind: :throw, reason: reason, stacktrace: stacktrace}) do
add_error(time, :throw, reason, stacktrace)
end

@impl true
def report_message(level, message, metadata \\ %{}) do
def report_event(%Event{time: time, kind: :message, level: level, reason: message}) do
add(%{
time: Map.get(metadata, :time, :logger.timestamp()),
time: time,
level: level,
kind: nil,
reason: message,
Expand All @@ -38,9 +36,9 @@ defmodule Tower.EphemeralReporter do
Agent.get(__MODULE__, & &1)
end

defp add_error(kind, reason, stacktrace, metadata) do
defp add_error(time, kind, reason, stacktrace) do
add(%{
time: Map.get(metadata, :time, :logger.timestamp()),
time: time,
level: :error,
kind: kind,
reason: reason,
Expand Down
77 changes: 77 additions & 0 deletions lib/tower/event.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
defmodule Tower.Event do
defstruct [:time, :level, :kind, :reason, :stacktrace, :metadata]

@type metadata :: %{log_event: :logger.log_event()}

@type t :: %__MODULE__{
time: :logger.timestamp(),
level: :logger.level(),
kind: :error | :exit | :throw | :message,
reason: Exception.t() | term(),
stacktrace: Exception.stacktrace(),
metadata: metadata()
}

def from_exception(exception, stacktrace, options \\ []) do
log_event = Keyword.get(options, :log_event)

%__MODULE__{
time: log_event[:meta][:time] || now(),
level: :error,
kind: :error,
reason: exception,
stacktrace: stacktrace,
metadata: %{
log_event: log_event
}
}
end

def from_exit(reason, stacktrace, options \\ []) do
log_event = Keyword.get(options, :log_event)

%__MODULE__{
time: log_event[:meta][:time] || now(),
level: :error,
kind: :exit,
reason: reason,
stacktrace: stacktrace,
metadata: %{
log_event: log_event
}
}
end

def from_throw(reason, stacktrace, options \\ []) do
log_event = Keyword.get(options, :log_event)

%__MODULE__{
time: log_event[:meta][:time] || now(),
level: :error,
kind: :throw,
reason: reason,
stacktrace: stacktrace,
metadata: %{
log_event: log_event
}
}
end

def from_message(level, message, options \\ []) do
log_event = Keyword.get(options, :log_event)

%__MODULE__{
time: log_event[:meta][:time] || now(),
level: level,
kind: :message,
reason: message,
metadata: %{
log_event: log_event
}
}
end

defp now do
:logger.timestamp()
end
end
68 changes: 38 additions & 30 deletions lib/tower/logger_handler.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule Tower.LoggerHandler do
@default_level :notice
@default_log_level :critical
@handler_id Tower

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

def detach do
Expand All @@ -21,76 +21,74 @@ defmodule Tower.LoggerHandler do
end

# elixir 1.15+
def log(%{level: :error, meta: %{crash_reason: {exception, stacktrace}} = meta}, _config)
def log(%{level: :error, meta: %{crash_reason: {exception, stacktrace}}} = log_event, _config)
when is_exception(exception) and is_list(stacktrace) do
Tower.handle_exception(exception, stacktrace, meta)
Tower.handle_exception(exception, stacktrace, log_event: log_event)
end

# elixir 1.15+
def log(
%{level: :error, meta: %{crash_reason: {{:nocatch, reason}, stacktrace}} = meta},
%{level: :error, meta: %{crash_reason: {{:nocatch, reason}, stacktrace}}} = log_event,
_config
)
when is_list(stacktrace) do
Tower.handle_throw(reason, stacktrace, meta)
Tower.handle_throw(reason, stacktrace, log_event: log_event)
end

# elixir 1.15+
def log(%{level: :error, meta: %{crash_reason: {exit_reason, stacktrace}} = meta}, _config)
def log(%{level: :error, meta: %{crash_reason: {exit_reason, stacktrace}}} = log_event, _config)
when is_list(stacktrace) do
Tower.handle_exit(exit_reason, stacktrace, meta)
Tower.handle_exit(exit_reason, stacktrace, log_event: log_event)
end

# elixir 1.14
def log(
%{
level: :error,
msg: {:report, %{report: %{reason: {exception, stacktrace}}}},
meta: meta
},
%{level: :error, msg: {:report, %{report: %{reason: {exception, stacktrace}}}}} =
log_event,
_config
)
when is_exception(exception) and is_list(stacktrace) do
Tower.handle_exception(exception, stacktrace, meta)
Tower.handle_exception(exception, stacktrace, log_event: log_event)
end

# elixir 1.14
def log(
%{
level: :error,
msg: {:report, %{report: %{reason: {{:nocatch, reason}, stacktrace}}}},
meta: meta
},
%{level: :error, msg: {:report, %{report: %{reason: {{:nocatch, reason}, stacktrace}}}}} =
log_event,
_config
)
when is_list(stacktrace) do
Tower.handle_throw(reason, stacktrace, meta)
Tower.handle_throw(reason, stacktrace, log_event: log_event)
end

# elixir 1.14
def log(
%{
level: :error,
msg: {:report, %{report: %{reason: {reason, stacktrace}}}},
meta: meta
},
%{level: :error, msg: {:report, %{report: %{reason: {reason, stacktrace}}}}} = log_event,
_config
)
when is_list(stacktrace) do
case Exception.normalize(:error, reason) do
%ErlangError{} ->
Tower.handle_exit(reason, stacktrace, meta)
Tower.handle_exit(reason, stacktrace, log_event: log_event)

e when is_exception(e) ->
Tower.handle_exception(e, stacktrace, meta)
Tower.handle_exception(e, stacktrace, log_event: log_event)

_ ->
Tower.handle_exit(reason, stacktrace, meta)
Tower.handle_exit(reason, stacktrace, log_event: log_event)
end
end

def log(%{level: level, msg: {:string, reason}, meta: meta}, _config) do
Tower.handle_message(level, reason, meta)
def log(%{level: level, msg: {:string, reason_chardata}} = log_event, _config) do
if should_handle?(level) do
Tower.handle_message(level, IO.chardata_to_string(reason_chardata), log_event: log_event)
end
end

def log(%{level: level, msg: {:report, report}} = log_event, _config) do
if should_handle?(level) do
Tower.handle_message(level, report, log_event: log_event)
end
end

def log(log_event, _config) do
Expand All @@ -100,4 +98,14 @@ defmodule Tower.LoggerHandler do
IO.puts(warning_message)
Tower.handle_message(:warning, warning_message)
end

defp should_handle?(level) do
:logger.compare_levels(level, log_level()) in [:gt, :eq]
end

defp log_level do
# This config env can be to any of the 8 levels in https://www.erlang.org/doc/apps/kernel/logger#t:level/0,
# or special values :all and :none.
Application.get_env(:tower, :log_level, @default_log_level)
end
end
12 changes: 2 additions & 10 deletions lib/tower/reporter.ex
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
defmodule Tower.Reporter do
@doc """
Reports an exception.
Reports events.
"""
@callback report_exception(exception :: Exception.t(), stacktrace :: list()) :: :ok
@callback report_exception(exception :: Exception.t(), stacktrace :: list(), metadata :: map()) ::
:ok
@callback report_throw(reason :: term(), stacktrace :: list()) :: :ok
@callback report_throw(reason :: term(), stacktrace :: list(), metadata :: map()) :: :ok
@callback report_exit(reason :: term(), stacktrace :: list()) :: :ok
@callback report_exit(reason :: term(), stacktrace :: list(), metadata :: map()) :: :ok
@callback report_message(level :: atom(), message :: term()) :: :ok
@callback report_message(level :: atom(), message :: term(), metadata :: map()) :: :ok
@callback report_event(event :: Tower.Event.t()) :: :ok
end
10 changes: 9 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ defmodule Tower.MixProject do
start_permanent: Mix.env() == :prod,
deps: deps(),
package: package(),
dialyzer: [
plt_local_path: "priv/plts"
],

# Docs
name: "Tower",
Expand All @@ -33,7 +36,12 @@ defmodule Tower.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ex_doc, "~> 0.34.0", only: :dev, runtime: false}
# Dev
{:ex_doc, "~> 0.34.0", only: :dev, runtime: false},
{:dialyxir, "~> 1.4", only: :dev, runtime: false},

# Test
{:assert_eventually, "~> 1.0", only: :test}
]
end

Expand Down
Loading

0 comments on commit c79fa62

Please sign in to comment.