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

feat: prevents repeated event spamming burst #11

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/tower_slack/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule TowerSlack.Application do
use Application

def start(_type, _args) do
Supervisor.start_link(
[
TowerSlack.KeyCounter
],
strategy: :one_for_one,
name: TowerSlack.Supervisor
)
end
end
50 changes: 50 additions & 0 deletions lib/tower_slack/key_counter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
defmodule TowerSlack.KeyCounter do
use GenServer

require Logger

@empty_state %{}
@reset_window 60_000

def start_link(_initial_value) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end

def increment(key) do
GenServer.call(__MODULE__, {:increment, key})
end

# Callbacks

@impl true
def init(_) do
Process.send_after(__MODULE__, :reset, @reset_window)

{:ok, @empty_state}
end

@impl true
def handle_call({:increment, key}, _from, state) do
{_, new_state} =
Map.get_and_update(
state,
key,
fn current_value ->
{current_value, (current_value || 0) + 1}
end
)

{:reply, Map.get(new_state, key), new_state}
end

@impl true
def handle_info(:reset, state) do
Process.send_after(__MODULE__, :reset, @reset_window)

if !Enum.empty?(state) do
Logger.warning("Resetting non-empty TowerSlack.KeyCounter state=#{inspect(state)}")
end

{:noreply, @empty_state}
end
end
11 changes: 10 additions & 1 deletion lib/tower_slack/message.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule TowerSlack.Message do
@moduledoc false

def new(id, kind, reason, stacktrace \\ []) when is_list(stacktrace) do
def new(id, similarity_id, kind, reason, stacktrace \\ []) when is_list(stacktrace) do
%{
"blocks" => [
%{
Expand Down Expand Up @@ -29,6 +29,15 @@ defmodule TowerSlack.Message do
text: "id: #{id}"
}
]
},
%{
type: "rich_text_section",
elements: [
%{
type: "text",
text: "similarity_id: #{similarity_id}"
}
]
}
]
}
Expand Down
58 changes: 47 additions & 11 deletions lib/tower_slack/reporter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,80 @@ defmodule TowerSlack.Reporter do

@default_level :error

require Logger

@impl true
def report_event(%Tower.Event{level: level} = event) do
def report_event(%Tower.Event{similarity_id: similarity_id, level: level} = event) do
if Tower.equal_or_greater_level?(level, level()) do
do_report_event(event)
TowerSlack.KeyCounter.increment(similarity_id)
|> case do
1 ->
do_report_event(event)

amount ->
Logger.warning(
"Ignoring repeated event with similarity_id=#{similarity_id}. Seen #{amount} times."
)
end
end
end

defp do_report_event(%Tower.Event{
kind: :error,
id: id,
similarity_id: similarity_id,
reason: exception,
stacktrace: stacktrace
}) do
post_message(id, inspect(exception.__struct__), Exception.message(exception), stacktrace)
post_message(
id,
similarity_id,
inspect(exception.__struct__),
Exception.message(exception),
stacktrace
)
end

defp do_report_event(%Tower.Event{kind: :throw, id: id, reason: reason, stacktrace: stacktrace}) do
post_message(id, "Uncaught throw", reason, stacktrace)
defp do_report_event(%Tower.Event{
kind: :throw,
id: id,
similarity_id: similarity_id,
reason: reason,
stacktrace: stacktrace
}) do
post_message(id, similarity_id, "Uncaught throw", reason, stacktrace)
end

defp do_report_event(%Tower.Event{kind: :exit, id: id, reason: reason, stacktrace: stacktrace}) do
post_message(id, "Exit", reason, stacktrace)
defp do_report_event(%Tower.Event{
kind: :exit,
id: id,
similarity_id: similarity_id,
reason: reason,
stacktrace: stacktrace
}) do
post_message(id, similarity_id, "Exit", reason, stacktrace)
end

defp do_report_event(%Tower.Event{kind: :message, id: id, level: level, reason: message}) do
defp do_report_event(%Tower.Event{
kind: :message,
id: id,
similarity_id: similarity_id,
level: level,
reason: message
}) do
m =
if is_binary(message) do
message
else
inspect(message)
end

post_message(id, "[#{level}] #{m}", "")
post_message(id, similarity_id, "[#{level}] #{m}", "")
end

defp post_message(id, kind, reason, stacktrace \\ []) do
defp post_message(id, similarity_id, kind, reason, stacktrace \\ []) do
{:ok, _} =
TowerSlack.Message.new(id, kind, reason, stacktrace)
TowerSlack.Message.new(id, similarity_id, kind, reason, stacktrace)
|> TowerSlack.Client.deliver()

:ok
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ defmodule TowerSlack.MixProject do
# Run "mix help compile.app" to learn about applications.
def application do
[
mod: {TowerSlack.Application, []},
extra_applications: [:logger, :public_key, :inets]
]
end
Expand Down
36 changes: 35 additions & 1 deletion test/tower_slack_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,16 @@
"elements" => [
%{
"type" => "text",
"text" => "id: " <> _rest
"text" => "id: " <> _id_rest
}
]
},
%{
"type" => "rich_text_section",
"elements" => [
%{
"type" => "text",
"text" => "similarity_id: " <> _similarity_rest
}
]
}
Expand All @@ -73,13 +82,38 @@

capture_log(fn ->
in_unlinked_process(fn ->
1 / 0

Check warning on line 85 in test/tower_slack_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 25.3.2.13)

the call to //2 will fail with ArithmeticError

Check warning on line 85 in test/tower_slack_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 25.3.2.13)

the call to //2 will fail with ArithmeticError
end)
end)

assert_receive({^ref, :sent}, 500)
end

test "protects from repeated events", %{bypass: bypass} do
# ref message synchronization trick copied from
# https://github.com/PSPDFKit-labs/bypass/issues/112
parent = self()
ref = make_ref()

Bypass.expect_once(bypass, "POST", "/webhook", fn conn ->
send(parent, {ref, :sent})

conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.resp(200, Jason.encode!(%{"ok" => true}))
end)

capture_log(fn ->
for _ <- 1..5 do
in_unlinked_process(fn ->
1 / 0

Check warning on line 109 in test/tower_slack_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 25.3.2.13)

the call to //2 will fail with ArithmeticError

Check warning on line 109 in test/tower_slack_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 25.3.2.13)

the call to //2 will fail with ArithmeticError
end)
end
end)

assert_receive({^ref, :sent}, 500)
end

defp in_unlinked_process(fun) when is_function(fun, 0) do
{:ok, pid} = Task.Supervisor.start_link()

Expand Down