Skip to content

Commit ae2648d

Browse files
authored
Fix infinite logging loop (#657)
Closes #656.
1 parent f165050 commit ae2648d

File tree

7 files changed

+82
-15
lines changed

7 files changed

+82
-15
lines changed

lib/sentry.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ defmodule Sentry do
171171
> with `:source_code_exclude_patterns`.
172172
"""
173173

174-
alias Sentry.{Config, Event}
174+
alias Sentry.{Config, Event, LoggerUtils}
175175

176176
require Logger
177177

@@ -324,7 +324,7 @@ defmodule Sentry do
324324

325325
cond do
326326
is_nil(event.message) and event.exception == [] ->
327-
Logger.log(Config.log_level(), "Sentry: unable to parse exception")
327+
LoggerUtils.log("Cannot report event without message or exception: #{inspect(event)}")
328328
:ignored
329329

330330
!Config.dsn() ->

lib/sentry/client.ex

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule Sentry.Client do
55
# and sampling.
66
# See https://develop.sentry.dev/sdk/unified-api/#client.
77

8-
alias Sentry.{Config, Dedupe, Envelope, Event, Interfaces, Transport}
8+
alias Sentry.{Config, Dedupe, Envelope, Event, Interfaces, LoggerUtils, Transport}
99

1010
require Logger
1111

@@ -72,7 +72,10 @@ defmodule Sentry.Client do
7272
:ok
7373

7474
:existing ->
75-
log("Event dropped due to being a duplicate of a previously-captured event.")
75+
LoggerUtils.log(
76+
"Event dropped due to being a duplicate of a previously-captured event."
77+
)
78+
7679
:excluded
7780
end
7881
else
@@ -241,10 +244,6 @@ defmodule Sentry.Client do
241244
nil
242245
end
243246

244-
if message, do: log(fn -> ["Failed to send Sentry event. ", message] end)
245-
end
246-
247-
defp log(message) do
248-
Logger.log(Config.log_level(), message, domain: [:sentry])
247+
if message, do: LoggerUtils.log(fn -> ["Failed to send Sentry event. ", message] end)
249248
end
250249
end

lib/sentry/logger_utils.ex

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
defmodule Sentry.LoggerUtils do
22
@moduledoc false
33

4-
# Utilities that are shared between Sentry.LoggerHandler and Sentry.LoggerBackend.
4+
# Utilities that are shared between Sentry.LoggerHandler and Sentry.LoggerBackend, plus
5+
# functions that wrap Elixir's Logger and that make sure we use the right options for Logger
6+
# calls. For example, if we (the Sentry SDK) log without using `domain: [:sentry]`, then we can
7+
# run into loops in which Sentry reports messages that the SDK itself logs.
8+
9+
alias Sentry.Config
10+
11+
require Logger
512

613
@spec build_sentry_options(Logger.level(), keyword() | nil, map(), [atom()] | :all) ::
714
keyword()
@@ -19,9 +26,10 @@ defmodule Sentry.LoggerUtils do
1926
|> Map.to_list()
2027
end
2128

29+
# Always excludes the :sentry domain, to avoid infinite logging loops.
2230
@spec excluded_domain?([atom()], [atom()]) :: boolean()
2331
def excluded_domain?(logged_domains, excluded_domains) do
24-
Enum.any?(logged_domains, &(&1 in excluded_domains))
32+
Enum.any?(logged_domains, &(&1 == :sentry or &1 in excluded_domains))
2533
end
2634

2735
defp elixir_logger_level_to_sentry_level(level) do
@@ -76,4 +84,10 @@ defmodule Sentry.LoggerUtils do
7684
defp attempt_to_convert_iodata(other) do
7785
other
7886
end
87+
88+
@spec log(iodata() | (-> iodata()), keyword()) :: :ok
89+
def log(message_or_fun, meta \\ []) when is_list(meta) do
90+
meta = Keyword.merge(meta, domain: [:sentry])
91+
Logger.log(Config.log_level(), message_or_fun, meta)
92+
end
7993
end

lib/sentry/transport/sender.ex

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule Sentry.Transport.Sender do
33

44
use GenServer
55

6-
alias Sentry.{Config, Envelope, Event, Transport}
6+
alias Sentry.{Envelope, Event, LoggerUtils, Transport}
77

88
require Logger
99

@@ -72,8 +72,7 @@ defmodule Sentry.Transport.Sender do
7272
end
7373

7474
if message do
75-
level = Config.log_level()
76-
Logger.log(level, fn -> ["Failed to send Sentry event. ", message] end, domain: [:sentry])
75+
LoggerUtils.log(fn -> ["Failed to send Sentry event. ", message] end)
7776
end
7877
end
7978
end

test/logger_backend_test.exs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,15 @@ defmodule Sentry.LoggerBackendTest do
306306
assert event.message == "Error"
307307
end
308308

309+
test "doesn't log events with :sentry as a domain" do
310+
Logger.configure_backend(Sentry.LoggerBackend, capture_log_messages: true)
311+
ref = register_before_send()
312+
313+
Logger.error("Error", domain: [:sentry])
314+
315+
refute_receive {^ref, _event}
316+
end
317+
309318
test "sets event level to Logger message level" do
310319
Logger.configure_backend(Sentry.LoggerBackend,
311320
level: :warning,

test/sentry/logger_handler_test.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ defmodule Sentry.LoggerHandlerTest do
143143
assert_receive {^ref, event}
144144
assert event.message == "no domain"
145145
end
146+
147+
@tag handler_config: %{capture_log_messages: true}
148+
test "ignores log messages that are logged by Sentry itself", %{sender_ref: ref} do
149+
Logger.error("Sentry had an error", domain: [:sentry])
150+
refute_receive {^ref, _event}
151+
end
146152
end
147153

148154
describe "with a crashing GenServer" do

test/sentry_test.exs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,47 @@ defmodule SentryTest do
7676
assert Sentry.send_event(Sentry.Event.create_event([])) == :ignored
7777
end)
7878

79-
assert log =~ "Sentry: unable to parse exception"
79+
assert log =~ "Cannot report event without message or exception: %Sentry.Event{"
80+
end
81+
82+
test "doesn't incur into infinite logging loops because we prevent that", %{bypass: bypass} do
83+
put_test_config(dedup_events: true)
84+
message_to_report = "Hello #{System.unique_integer([:positive])}"
85+
86+
Bypass.expect(bypass, fn conn ->
87+
Plug.Conn.send_resp(conn, 200, ~s<{"id": "340"}>)
88+
end)
89+
90+
:ok =
91+
:logger.add_handler(:sentry_handler, Sentry.LoggerHandler, %{
92+
config: %{capture_log_messages: true, level: :debug}
93+
})
94+
95+
on_exit(fn ->
96+
_ = :logger.remove_handler(:sentry_handler)
97+
end)
98+
99+
# First one is reported correctly as it has no duplicates
100+
assert {:ok, "340"} = Sentry.capture_message(message_to_report)
101+
102+
log =
103+
capture_log(fn ->
104+
# Then, we log the same message, which triggers the SDK to log that the message wasn't sent
105+
# because it's a duplicate.
106+
assert :excluded = Sentry.capture_message(message_to_report)
107+
108+
# Then we log the same message again, which again triggers the SDK to log that the message
109+
# wasn't sent. But this time, *that* log (the one about the duplicate event) is also a
110+
# duplicate. So, we can test that it doesn't result in an infinite logging loop.
111+
assert :excluded = Sentry.capture_message(message_to_report)
112+
end)
113+
114+
logged_count =
115+
~r/Event dropped due to being a duplicate/
116+
|> Regex.scan(log)
117+
|> length()
118+
119+
assert logged_count == 2
80120
end
81121

82122
test "does not send events if :dsn is not configured or nil" do

0 commit comments

Comments
 (0)