diff --git a/config/config.exs b/config/config.exs index a84764e2..01a3e138 100644 --- a/config/config.exs +++ b/config/config.exs @@ -12,7 +12,8 @@ config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase config :error_tracker, repo: Towerops.Repo, - otp_app: :towerops + otp_app: :towerops, + ignorer: Towerops.ErrorTrackerIgnorer # Configure esbuild (the version is required) config :esbuild, diff --git a/lib/towerops/error_tracker_ignorer.ex b/lib/towerops/error_tracker_ignorer.ex new file mode 100644 index 00000000..ffe260f5 --- /dev/null +++ b/lib/towerops/error_tracker_ignorer.ex @@ -0,0 +1,24 @@ +defmodule Towerops.ErrorTrackerIgnorer do + @moduledoc """ + Drops benign errors from ErrorTracker that occur during K8s pod shutdown. + + Matches the same patterns as `Towerops.HoneybadgerFilter` and + `Towerops.LoggerFilters.drop_shutdown_errors/2`. + """ + + @behaviour ErrorTracker.Ignorer + + @impl true + def ignore?(%ErrorTracker.Error{reason: reason}, _context) do + shutdown_error?(reason) + end + + defp shutdown_error?(reason) when is_binary(reason) do + String.contains?(reason, "port_died") or + String.contains?(reason, "write_failed") or + String.contains?(reason, "epipe") or + String.contains?(reason, "broken pipe") + end + + defp shutdown_error?(_), do: false +end diff --git a/test/towerops/error_tracker_ignorer_test.exs b/test/towerops/error_tracker_ignorer_test.exs new file mode 100644 index 00000000..9b8186bd --- /dev/null +++ b/test/towerops/error_tracker_ignorer_test.exs @@ -0,0 +1,45 @@ +defmodule Towerops.ErrorTrackerIgnorerTest do + use ExUnit.Case, async: true + + alias Towerops.ErrorTrackerIgnorer + + defp build_error(reason) do + %ErrorTracker.Error{ + kind: "ErlangError", + reason: reason, + source_line: "-", + source_function: "-", + status: :unresolved, + fingerprint: "abc", + last_occurrence_at: DateTime.utc_now(), + muted: false + } + end + + describe "ignore?/2" do + test "ignores port_died errors" do + error = build_error("Erlang error: {:port_died, :normal}") + assert ErrorTrackerIgnorer.ignore?(error, %{}) + end + + test "ignores write_failed errors" do + error = build_error("{:write_failed, :standard_io, :enoent}") + assert ErrorTrackerIgnorer.ignore?(error, %{}) + end + + test "ignores epipe errors" do + error = build_error("{:error, {:write_failed, %{type: :standard_io}, {:badarg, :epipe}}}") + assert ErrorTrackerIgnorer.ignore?(error, %{}) + end + + test "ignores broken pipe errors" do + error = build_error("broken pipe (epipe)") + assert ErrorTrackerIgnorer.ignore?(error, %{}) + end + + test "does not ignore regular errors" do + error = build_error("something went wrong") + refute ErrorTrackerIgnorer.ignore?(error, %{}) + end + end +end