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

Adds config option exclude_errors #342

Closed
wants to merge 0 commits into from
Closed

Conversation

sezaru
Copy link
Contributor

@sezaru sezaru commented Mar 7, 2021

This PR adds support to exclude errors from being sent to the Honeybadger servers. This was discussed in issue #308.

My approach was to make this as simple as possible and not opinative, giving the full responsibility and power to the user to exclude errors in their desired way.

This is done by implementing the Honeybadger.ExcludeErrors behaviour function exclude_error?/1, this function receives the full Honeybadger.Notice and should return a boolean signalling the error exclusion or not.

By default, the library will not exclude any errors from being sent.

Note that tests are missing because I first want to know that the changes are ok to you guys.

Also note that this can be easily expanded in the future to ship with more exclusion implementations out-of-box, as an example, I will post bellow my full implementation for a rate-limit exclusion strategy where I limit the number of messages per error that can be sent in a time window:

exclude_errors.ex:

defmodule Log.Honeybadger.ExcludeErrors do
  alias Log.Honeybadger.ExcludeErrors.Server

  alias Honeybadger.ExcludeErrors

  @behaviour ExcludeErrors

  @spec child_spec(keyword) :: Supervisor.child_spec()
  defdelegate child_spec(opts), to: Server

  @impl ExcludeErrors
  def exclude_error?(notice), do: GenServer.call(Server, {:exclude_error?, notice}, :infinity)
end

exclude_errors/server.ex:

defmodule Log.Honeybadger.ExcludeErrors.Server do
  use GenServer

  alias Log.Honeybadger.ExcludeErrors.Impl

  def start_link(_), do: GenServer.start_link(__MODULE__, :noargs, name: __MODULE__)

  @impl GenServer
  def init(_), do: {:ok, Impl.initialize()}

  @impl GenServer
  def handle_call({:exclude_error?, notice}, _, state) do
    {reply, state} = Impl.exclude_error?(notice, state)

    {:reply, reply, state}
  end

  @impl GenServer
  def handle_info({:timeout, timer, :decrement_counters}, %{timer: timer} = state),
    do: {:noreply, Impl.decrement_counters(state)}

  def handle_info({:timeout, _, :decrement_counters}, state), do: {:noreply, state}
end

exclude_errors/impl.ex:

defmodule Log.Honeybadger.ExcludeErrors.Impl do
  alias Log.Honeybadger.ExcludeErrors.State

  alias Honeybadger.Notice

  @spec initialize :: State.t()
  def initialize, do: State.new!(self())

  @spec exclude_error?(Notice.t(), State.t()) :: {boolean, State.t()}
  def exclude_error?(notice, state) do
    %{error: %{fingerprint: fingerprint, class: class, backtrace: backtrace}} = notice

    if fingerprint != "", do: exclude?(fingerprint, state), else: exclude?({class, backtrace}, state)
  end

  @spec decrement_counters(State.t()) :: State.t()
  def decrement_counters(state) do
    state
    |> State.all()
    |> Enum.reduce(state, fn {key, _}, state ->
      {current_errors, state} = State.decrement(state, key)

      if current_errors == 0, do: State.delete(state, key), else: state
    end)
    |> State.start_timer(self())
  end

  defp exclude?(key, %{max_errors: max_errors} = state) do
    {current_errors, state} = State.increment(state, key)

    {current_errors == max_errors, state}
  end
end

exclude_errors/state.ex:

defmodule Log.Honeybadger.ExcludeErrors.State do
  alias Common.Timer

  use TypedStruct

  @max_errors Application.compile_env!(:honeybadger, :max_errors) + 1
  @decrement_period Application.compile_env!(:honeybadger, :decrement_period)

  typedstruct enforce: true do
    field :max_errors, integer, default: @max_errors
    field :counter, reference
    field :timer, reference, enforce: false
  end

  @spec new!(pid) :: t
  def new!(from) do
    counter = :ets.new(Module.concat(__MODULE__, Counter), [:set, :protected])

    struct!(__MODULE__, counter: counter) |> start_timer(from)
  end

  @spec increment(State.t(), term) :: {integer, State.t()}
  def increment(%{counter: table} = state, key) do
    current_errors = :ets.update_counter(table, key, {2, 1, 5, 5}, {key, 0})

    {current_errors, state}
  end

  @spec decrement(State.t(), term) :: {integer, State.t()}
  def decrement(%{counter: table} = state, key) do
    current_errors = :ets.update_counter(table, key, {2, -1, 0, 0}, {key, 0})

    {current_errors, state}
  end

  @spec delete(State.t(), term) :: State.t()
  def delete(%{counter: table} = state, key) do
    :ets.delete(table, key)

    state
  end

  @spec all(State.t()) :: [{term, integer}]
  def all(%{counter: table}), do: :ets.tab2list(table)

  @spec start_timer(State.t(), pid) :: State.t()
  def start_timer(state, from),
    do: %{state | timer: Timer.start(from, :decrement_counters, @decrement_period)}
end

@sezaru
Copy link
Contributor Author

sezaru commented Mar 7, 2021

CI build has failed, but it seems to be an issue with the CI itself and not my changes as far as I can tell.

lib/honeybadger.ex Outdated Show resolved Hide resolved
@TraceyOnim
Copy link
Contributor

TraceyOnim commented Jul 27, 2021

@sezaru If I override the default configuration to exclude errors, none of the exception will be sent. How is this supposed to work?
Does a user have the power to selectively ignore an exception and not notify when they implement their own custom Honeybadger.ExcludeErrors behaviour?

@sezaru
Copy link
Contributor Author

sezaru commented Aug 17, 2021

Hi @TraceyOnim , I don't know if I understood your question correctly. do you mean that exceptions are handled differently inside Honeybadger? When I looked into the code I only found one way to send the errors.

Does a user have the power to selectively ignore an exception and not notify when they implement their own custom Honeybadger.ExcludeErrors behaviour?

Assuming that the exceptions are handled in the same part that I changed in the code, the answer would be yes, the user would receive the full Honeybadger.Notice struct, analyze it and decide if it wants to send it or not.

@TraceyOnim
Copy link
Contributor

Hi @TraceyOnim , I don't know if I understood your question correctly. do you mean that exceptions are handled differently inside Honeybadger? When I looked into the code I only found one way to send the errors.

Does a user have the power to selectively ignore an exception and not notify when they implement their own custom Honeybadger.ExcludeErrors behaviour?

Assuming that the exceptions are handled in the same part that I changed in the code, the answer would be yes, the user would receive the full Honeybadger.Notice struct, analyze it and decide if it wants to send it or not.

Yeah you got my question , it's now clear .Thank you for the clarification

@jdubya80
Copy link

jdubya80 commented Feb 1, 2022

Is this something that is planned to be introduced at some point? Is there another way to do "error exclusion"?

@joshuap
Copy link
Member

joshuap commented Feb 2, 2022

Is this something that is planned to be introduced at some point? Is there another way to do "error exclusion"?

@jdubya80 there isn't another way, and I'm all for including this.

@sezaru do you want to add some tests for this? I think it looks good otherwise. I might want to extend this in the future so that the default implementation will ignore a list of error names if you configure something like this: exclude_errors: [ArithmeticError, FunctionClauseError].

@TraceyOnim TraceyOnim closed this Aug 2, 2022
joshuap added a commit that referenced this pull request Oct 25, 2022
* Adds config option exclude_errors

* Changes if not with unless.

Co-authored-by: Tracey Onim <43263401+TraceyOnim@users.noreply.github.com>

* add test for exclude_error implementation

* support list of errors and  ExcludeError module to exclude_error option

* update configuration docs

* update changelog

* support error classes

* Update CHANGELOG.md

Co-authored-by: Joshua Wood <josh@joshuawood.net>

* update README

* Update README.md

Co-authored-by: Pangratios Cosma <pagcosma@gmail.com>

Co-authored-by: Eduardo Barreto Alexandre <sezdocs@gmail.com>
Co-authored-by: Tracey Onim <43263401+TraceyOnim@users.noreply.github.com>
Co-authored-by: TraceyOnim <traceyonim22@gmail.com>
Co-authored-by: Pangratios Cosma <pagcosma@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants