feat: email raw stacktrace on every Honeybadger exception
Add a Honeybadger NoticeFilter that sends an email with the full error class, message, stacktrace, and server info to graham@mcintire.me whenever an exception is reported. The notice passes through unchanged so Honeybadger still receives it normally.
This commit is contained in:
parent
aa1bac0d46
commit
debdd1a21f
3 changed files with 185 additions and 1 deletions
|
|
@ -25,7 +25,8 @@ config :honeybadger,
|
|||
environment_name: config_env(),
|
||||
insights_enabled: true,
|
||||
use_logger: true,
|
||||
filter: Towerops.HoneybadgerFilter
|
||||
filter: Towerops.HoneybadgerFilter,
|
||||
notice_filter: Towerops.HoneybadgerNoticeFilter
|
||||
|
||||
# Configure Elixir's Logger
|
||||
config :logger, :default_formatter,
|
||||
|
|
|
|||
80
lib/towerops/honeybadger_notice_filter.ex
Normal file
80
lib/towerops/honeybadger_notice_filter.ex
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
defmodule Towerops.HoneybadgerNoticeFilter do
|
||||
@moduledoc """
|
||||
Honeybadger notice filter that emails a raw stacktrace to the
|
||||
ops contact whenever an exception is reported.
|
||||
|
||||
Implements `Honeybadger.NoticeFilter` — receives the fully-built
|
||||
notice, sends an email, and returns the notice unchanged so it
|
||||
still reaches Honeybadger.
|
||||
"""
|
||||
|
||||
@behaviour Honeybadger.NoticeFilter
|
||||
|
||||
import Swoosh.Email
|
||||
|
||||
alias Towerops.Mailer
|
||||
|
||||
require Logger
|
||||
|
||||
@recipient "graham@mcintire.me"
|
||||
|
||||
@impl Honeybadger.NoticeFilter
|
||||
def filter(%Honeybadger.Notice{} = notice) do
|
||||
send_error_email(notice)
|
||||
notice
|
||||
end
|
||||
|
||||
defp send_error_email(%Honeybadger.Notice{error: error, server: server}) do
|
||||
from_address = Application.get_env(:towerops, :mailer_from, {"Towerops", "hi@towerops.net"})
|
||||
subject = "[#{server.environment_name}] #{error.class}: #{truncate(error.message, 120)}"
|
||||
|
||||
body = format_body(error, server)
|
||||
|
||||
email =
|
||||
new()
|
||||
|> to(@recipient)
|
||||
|> from(from_address)
|
||||
|> subject(subject)
|
||||
|> text_body(body)
|
||||
|
||||
case Mailer.deliver(email) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to send Honeybadger error email: #{inspect(reason)}")
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp format_body(error, server) do
|
||||
backtrace = format_backtrace(error.backtrace)
|
||||
|
||||
"""
|
||||
Exception: #{error.class}
|
||||
Message: #{error.message}
|
||||
|
||||
Environment: #{server.environment_name}
|
||||
Hostname: #{server.hostname}
|
||||
|
||||
Stacktrace:
|
||||
#{backtrace}
|
||||
"""
|
||||
end
|
||||
|
||||
defp format_backtrace(backtrace) when is_list(backtrace) do
|
||||
Enum.map_join(backtrace, "\n", fn entry ->
|
||||
" #{entry.file}:#{entry.number} in #{entry.method}"
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_backtrace(_), do: " (no stacktrace available)"
|
||||
|
||||
defp truncate(nil, _max), do: "(no message)"
|
||||
|
||||
defp truncate(message, max) when byte_size(message) > max do
|
||||
String.slice(message, 0, max) <> "..."
|
||||
end
|
||||
|
||||
defp truncate(message, _max), do: message
|
||||
end
|
||||
103
test/towerops/honeybadger_notice_filter_test.exs
Normal file
103
test/towerops/honeybadger_notice_filter_test.exs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
defmodule Towerops.HoneybadgerNoticeFilterTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
import Swoosh.TestAssertions
|
||||
|
||||
alias Towerops.HoneybadgerNoticeFilter
|
||||
|
||||
defp build_notice(attrs \\ %{}) do
|
||||
default_error = %{
|
||||
class: "RuntimeError",
|
||||
message: "something went wrong",
|
||||
backtrace: [
|
||||
%{file: "lib/towerops_web/live/dashboard_live.ex", number: 42, method: "mount/3"},
|
||||
%{file: "lib/phoenix_live_view/channel.ex", number: 100, method: "handle_info/2"}
|
||||
],
|
||||
tags: [],
|
||||
fingerprint: ""
|
||||
}
|
||||
|
||||
default_server = %{
|
||||
environment_name: :test,
|
||||
hostname: "test-host",
|
||||
project_root: "/app",
|
||||
revision: nil
|
||||
}
|
||||
|
||||
%Honeybadger.Notice{
|
||||
error: Map.merge(default_error, Map.get(attrs, :error, %{})),
|
||||
server: Map.merge(default_server, Map.get(attrs, :server, %{})),
|
||||
request: Map.get(attrs, :request, %{}),
|
||||
notifier: %{name: "honeybadger-elixir", url: "", version: "0.0.0"},
|
||||
breadcrumbs: %{},
|
||||
correlation_context: %{}
|
||||
}
|
||||
end
|
||||
|
||||
describe "filter/1" do
|
||||
test "returns the notice unchanged so Honeybadger still receives it" do
|
||||
notice = build_notice()
|
||||
|
||||
result = HoneybadgerNoticeFilter.filter(notice)
|
||||
|
||||
assert result == notice
|
||||
end
|
||||
|
||||
test "sends an email with the error class and message" do
|
||||
notice = build_notice(%{error: %{class: "ArgumentError", message: "bad argument"}})
|
||||
|
||||
HoneybadgerNoticeFilter.filter(notice)
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
assert email.to == [{"", "graham@mcintire.me"}]
|
||||
assert email.subject =~ "ArgumentError"
|
||||
assert email.subject =~ "bad argument"
|
||||
end)
|
||||
end
|
||||
|
||||
test "email body contains the full stacktrace" do
|
||||
notice =
|
||||
build_notice(%{
|
||||
error: %{
|
||||
backtrace: [
|
||||
%{file: "lib/my_app/worker.ex", number: 10, method: "perform/1"},
|
||||
%{file: "lib/oban/queue/executor.ex", number: 55, method: "safe_call/1"}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
HoneybadgerNoticeFilter.filter(notice)
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
body = email.text_body
|
||||
assert body =~ "lib/my_app/worker.ex:10 in perform/1"
|
||||
assert body =~ "lib/oban/queue/executor.ex:55 in safe_call/1"
|
||||
end)
|
||||
end
|
||||
|
||||
test "email body includes server environment info" do
|
||||
notice =
|
||||
build_notice(%{
|
||||
server: %{environment_name: :prod, hostname: "web-1.towerops.net"}
|
||||
})
|
||||
|
||||
HoneybadgerNoticeFilter.filter(notice)
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
body = email.text_body
|
||||
assert body =~ "prod"
|
||||
assert body =~ "web-1.towerops.net"
|
||||
end)
|
||||
end
|
||||
|
||||
test "email is sent from the configured mailer_from address" do
|
||||
notice = build_notice()
|
||||
|
||||
HoneybadgerNoticeFilter.filter(notice)
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
assert email.from == {"Towerops", "hi@towerops.net"}
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue