Skip to content

Commit 966b13c

Browse files
authored
Add overload protection to :logger handler (#727)
Closes #725.
1 parent c3df22f commit 966b13c

File tree

4 files changed

+137
-37
lines changed

4 files changed

+137
-37
lines changed

lib/sentry/logger_backend.ex

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
defmodule Sentry.LoggerBackend do
22
@moduledoc """
3-
Report Logger events like crashed processes to Sentry. To include in your
4-
application, start this backend in your application `start/2` callback:
3+
An Elixir `Logger` backend that reports logged messages and crashes to Sentry.
4+
5+
> #### `:logger` handler {: .warn}
6+
>
7+
> This module will eventually become **legacy**. Elixir `Logger` backends will
8+
> eventually be deprecated in favor of Erlang [`:logger`
9+
> handlers](https://erlang.org/doc/man/logger_chapter.html#handlers).
10+
>
11+
> Sentry already has a `:logger` handler, `Sentry.LoggerHandler`. In new projects
12+
> and wherever possible, use `Sentry.LoggerHandler` in favor of this backend.
13+
14+
To include in your application, start this backend in your application `start/2` callback:
515
616
# lib/my_app/application.ex
717
def start(_type, _args) do

lib/sentry/logger_handler.ex

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -66,26 +66,34 @@ defmodule Sentry.LoggerHandler do
6666
""",
6767
type_doc: "`t:keyword/0` or `nil`",
6868
default: nil
69+
],
70+
sync_threshold: [
71+
type: :non_neg_integer,
72+
default: 100,
73+
doc: """
74+
*since v10.6.0* - The number of queued events after which this handler switches
75+
to *sync mode*. Generally, this handler sends messages to Sentry **asynchronously**,
76+
equivalent to using `result: :none` in `Sentry.send_event/2`. However, if the number
77+
of queued events exceeds this threshold, the handler will switch to *sync mode*,
78+
where it starts using `result: :sync` to block until the event is sent. If you always
79+
want to use sync mode, set this option to `0`. This option effectively implements
80+
**overload protection**.
81+
"""
6982
]
7083
]
7184

7285
@options_schema NimbleOptions.new!(options_schema)
7386

7487
@moduledoc """
75-
`:logger` handler to report logged events to Sentry.
76-
77-
This module is similar to `Sentry.LoggerBackend`, but it implements a
78-
[`:logger` handler](https://erlang.org/doc/man/logger_chapter.html#handlers) rather
79-
than an Elixir's `Logger` backend. It provides additional functionality compared to
80-
the `Logger` backend, such as rate-limiting of reported messages, better fingerprinting,
81-
and better handling of crashes.
88+
A highly-configurable [`:logger` handler](https://erlang.org/doc/man/logger_chapter.html#handlers)
89+
that reports logged messages and crashes to Sentry.
8290
8391
*This module is available since v9.0.0 of this library*.
8492
8593
> #### When to Use the Handler vs the Backend? {: .info}
8694
>
87-
> There is **no functional difference in behavior** between `Sentry.LoggerHandler` and
88-
> `Sentry.LoggerBackend` when it comes to reporting to Sentry. The main functional
95+
> Sentry's Elixir SDK also ships with `Sentry.LoggerBackend`, an Elixir `Logger`
96+
> backend. The backend has similar functionality to this handler. The main functional
8997
> difference is that `Sentry.LoggerBackend` runs in its own process, while
9098
> `Sentry.LoggerHandler` runs in the process that logs. The latter is generally
9199
> preferable.
@@ -94,11 +102,15 @@ defmodule Sentry.LoggerHandler do
94102
> feature in Erlang/OTP, and `Sentry.LoggerBackend` was created before `:logger`
95103
> handlers were introduced.
96104
>
97-
> In general, try to use `Sentry.LoggerHandler` if possible. In future Elixir releases,
105+
> In general, use `Sentry.LoggerHandler` whenever possible. In future Elixir releases,
98106
> `Logger` backends may become deprecated and hence `Sentry.LoggerBackend` may be
99107
> eventually removed.
100108
101-
## Crash Reports
109+
## Features
110+
111+
This logger handler provides the features listed here.
112+
113+
### Crash Reports
102114
103115
The reason you'll want to add this handler to your application is so that you can
104116
report **crashes** in your system to Sentry. Sometimes, you're able to catch exceptions
@@ -111,6 +123,24 @@ defmodule Sentry.LoggerHandler do
111123
crash reports in Sentry. That's where this handler comes in. This handler hooks
112124
into `:logger` and reports nicely-formatted crash reports to Sentry.
113125
126+
### Overload Protection
127+
128+
This handler has built-in *overload protection* via the `:sync_threshold`
129+
configuration option. Under normal circumstances, this handler sends events to
130+
Sentry asynchronously, without blocking the logging process. However, if the
131+
number of queued up events exceeds the `:sync_threshold`, then this handler
132+
starts *blocking* the logging process until the event is sent.
133+
134+
*Overload protection is available since v10.6.0*.
135+
136+
### Rate Limiting
137+
138+
You can configure this handler to rate-limit the number of messages it sends to
139+
Sentry. This can help avoid "spamming" Sentry. See the `:rate_limiting` configuration
140+
option.
141+
142+
*Rate limiting is available since v10.5.0*.
143+
114144
## Usage
115145
116146
To add this handler to your system, see [the documentation for handlers in
@@ -183,9 +213,17 @@ defmodule Sentry.LoggerHandler do
183213

184214
alias Sentry.LoggerUtils
185215
alias Sentry.LoggerHandler.RateLimiter
216+
alias Sentry.Transport.SenderPool
186217

187218
# The config for this logger handler.
188-
defstruct [:level, :excluded_domains, :metadata, :capture_log_messages, :rate_limiting]
219+
defstruct [
220+
:level,
221+
:excluded_domains,
222+
:metadata,
223+
:capture_log_messages,
224+
:rate_limiting,
225+
:sync_threshold
226+
]
189227

190228
## Logger handler callbacks
191229

@@ -301,30 +339,29 @@ defmodule Sentry.LoggerHandler do
301339
end
302340

303341
# "report" here is of type logger:report/0, which is a map or keyword list.
304-
defp log_unfiltered(%{msg: {:report, report}}, sentry_opts, %__MODULE__{} = _config) do
342+
defp log_unfiltered(%{msg: {:report, report}}, sentry_opts, %__MODULE__{} = config) do
305343
case Map.new(report) do
306344
%{report: %{reason: {exception, stacktrace}}}
307345
when is_exception(exception) and is_list(stacktrace) ->
308346
sentry_opts = Keyword.merge(sentry_opts, stacktrace: stacktrace, handled: false)
309-
Sentry.capture_exception(exception, sentry_opts)
347+
capture(:exception, exception, sentry_opts, config)
310348

311349
%{report: %{reason: {reason, stacktrace}}} when is_list(stacktrace) ->
312350
sentry_opts = Keyword.put(sentry_opts, :stacktrace, stacktrace)
313-
Sentry.capture_message("** (stop) " <> Exception.format_exit(reason), sentry_opts)
351+
capture(:message, "** (stop) " <> Exception.format_exit(reason), sentry_opts, config)
314352

315353
%{report: report_info} ->
316-
Sentry.capture_message(inspect(report_info), sentry_opts)
354+
capture(:message, inspect(report_info), sentry_opts, config)
317355

318356
%{reason: {reason, stacktrace}} when is_list(stacktrace) ->
319357
sentry_opts = Keyword.put(sentry_opts, :stacktrace, stacktrace)
320-
Sentry.capture_message("** (stop) " <> Exception.format_exit(reason), sentry_opts)
358+
capture(:message, "** (stop) " <> Exception.format_exit(reason), sentry_opts, config)
321359

322360
%{reason: reason} ->
323361
sentry_opts =
324362
Keyword.update!(sentry_opts, :extra, &Map.put(&1, :crash_reason, inspect(reason)))
325363

326-
msg = "** (stop) #{Exception.format_exit(reason)}"
327-
Sentry.capture_message(msg, sentry_opts)
364+
capture(:message, "** (stop) #{Exception.format_exit(reason)}", sentry_opts, config)
328365

329366
_other ->
330367
:ok
@@ -355,14 +392,19 @@ defmodule Sentry.LoggerHandler do
355392
{exception, stacktrace},
356393
_chardata_message,
357394
sentry_opts,
358-
%__MODULE__{}
395+
%__MODULE__{} = config
359396
)
360397
when is_exception(exception) and is_list(stacktrace) do
361398
sentry_opts = Keyword.merge(sentry_opts, stacktrace: stacktrace, handled: false)
362-
Sentry.capture_exception(exception, sentry_opts)
399+
capture(:exception, exception, sentry_opts, config)
363400
end
364401

365-
defp log_from_crash_reason({reason, stacktrace}, chardata_message, sentry_opts, %__MODULE__{})
402+
defp log_from_crash_reason(
403+
{reason, stacktrace},
404+
chardata_message,
405+
sentry_opts,
406+
%__MODULE__{} = config
407+
)
366408
when is_list(stacktrace) do
367409
sentry_opts =
368410
sentry_opts
@@ -380,18 +422,23 @@ defmodule Sentry.LoggerHandler do
380422
inspect(call)
381423
])
382424

383-
Sentry.capture_message(Exception.format_exit(reason), sentry_opts)
425+
capture(:message, Exception.format_exit(reason), sentry_opts, config)
384426

385427
_other ->
386-
try_to_parse_message_or_just_report_it(chardata_message, sentry_opts)
428+
try_to_parse_message_or_just_report_it(chardata_message, sentry_opts, config)
387429
end
388430
end
389431

390-
defp log_from_crash_reason(_other_reason, chardata_message, sentry_opts, %__MODULE__{
391-
capture_log_messages: true
392-
}) do
432+
defp log_from_crash_reason(
433+
_other_reason,
434+
chardata_message,
435+
sentry_opts,
436+
%__MODULE__{
437+
capture_log_messages: true
438+
} = config
439+
) do
393440
string_message = :unicode.characters_to_binary(chardata_message)
394-
Sentry.capture_message(string_message, sentry_opts)
441+
capture(:message, string_message, sentry_opts, config)
395442
end
396443

397444
defp log_from_crash_reason(_other_reason, _string_message, _sentry_opts, _config) do
@@ -439,7 +486,8 @@ defmodule Sentry.LoggerHandler do
439486
"\nState: ",
440487
inspected_state | _
441488
],
442-
sentry_opts
489+
sentry_opts,
490+
config
443491
) do
444492
string_reason = chardata_reason |> :unicode.characters_to_binary() |> String.trim()
445493

@@ -452,7 +500,7 @@ defmodule Sentry.LoggerHandler do
452500
genserver_state: inspected_state
453501
})
454502

455-
Sentry.capture_message("GenServer %s terminating: #{string_reason}", sentry_opts)
503+
capture(:message, "GenServer %s terminating: #{string_reason}", sentry_opts, config)
456504
end
457505

458506
defp try_to_parse_message_or_just_report_it(
@@ -468,7 +516,8 @@ defmodule Sentry.LoggerHandler do
468516
"\nState: ",
469517
inspected_state | _
470518
],
471-
sentry_opts
519+
sentry_opts,
520+
config
472521
) do
473522
string_reason = chardata_reason |> :unicode.characters_to_binary() |> String.trim()
474523

@@ -480,15 +529,30 @@ defmodule Sentry.LoggerHandler do
480529
genserver_state: inspected_state
481530
})
482531

483-
Sentry.capture_message("GenServer %s terminating: #{string_reason}", sentry_opts)
532+
capture(:message, "GenServer %s terminating: #{string_reason}", sentry_opts, config)
484533
end
485534

486-
defp try_to_parse_message_or_just_report_it(chardata_message, sentry_opts) do
535+
defp try_to_parse_message_or_just_report_it(chardata_message, sentry_opts, config) do
487536
string_message = :unicode.characters_to_binary(chardata_message)
488-
Sentry.capture_message(string_message, sentry_opts)
537+
capture(:message, string_message, sentry_opts, config)
489538
end
490539

491540
defp add_extra_to_sentry_opts(sentry_opts, new_extra) do
492541
Keyword.update(sentry_opts, :extra, %{}, &Map.merge(new_extra, &1))
493542
end
543+
544+
for function <- [:exception, :message] do
545+
sentry_fun = :"capture_#{function}"
546+
547+
defp capture(unquote(function), exception_or_message, sentry_opts, %__MODULE__{} = config) do
548+
sentry_opts =
549+
if SenderPool.get_queued_events_counter() >= config.sync_threshold do
550+
Keyword.put(sentry_opts, :result, :sync)
551+
else
552+
sentry_opts
553+
end
554+
555+
Sentry.unquote(sentry_fun)(exception_or_message, sentry_opts)
556+
end
557+
end
494558
end

lib/sentry/transport/sender.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ defmodule Sentry.Transport.Sender do
2020
@spec send_async(module(), Event.t()) :: :ok
2121
def send_async(client, %Event{} = event) when is_atom(client) do
2222
random_index = Enum.random(1..Transport.SenderPool.pool_size())
23-
23+
Transport.SenderPool.increase_queued_events_counter()
2424
GenServer.cast({:via, Registry, {@registry, random_index}}, {:send, client, event})
2525
end
2626

@@ -42,6 +42,9 @@ defmodule Sentry.Transport.Sender do
4242
|> Transport.post_envelope(client)
4343
|> maybe_log_send_result([event])
4444

45+
# We sent an event, so we can decrease the number of queued events.
46+
Transport.SenderPool.decrease_queued_events_counter()
47+
4548
{:noreply, state}
4649
end
4750

lib/sentry/transport/sender_pool.ex

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@ defmodule Sentry.Transport.SenderPool do
33

44
use Supervisor
55

6+
@queued_events_key {__MODULE__, :queued_events}
7+
68
@spec start_link(keyword()) :: Supervisor.on_start()
79
def start_link([] = _opts) do
810
Supervisor.start_link(__MODULE__, [])
911
end
1012

1113
@impl true
1214
def init([]) do
15+
queued_events_counter = :counters.new(1, [])
16+
:persistent_term.put(@queued_events_key, queued_events_counter)
17+
1318
children =
1419
for index <- 1..pool_size() do
1520
Supervisor.child_spec({Sentry.Transport.Sender, index: index},
@@ -30,4 +35,22 @@ defmodule Sentry.Transport.SenderPool do
3035
value
3136
end
3237
end
38+
39+
@spec increase_queued_events_counter() :: :ok
40+
def increase_queued_events_counter do
41+
counter = :persistent_term.get(@queued_events_key)
42+
:counters.add(counter, 1, 1)
43+
end
44+
45+
@spec decrease_queued_events_counter() :: :ok
46+
def decrease_queued_events_counter do
47+
counter = :persistent_term.get(@queued_events_key)
48+
:counters.sub(counter, 1, 1)
49+
end
50+
51+
@spec get_queued_events_counter() :: non_neg_integer()
52+
def get_queued_events_counter do
53+
counter = :persistent_term.get(@queued_events_key)
54+
:counters.get(counter, 1)
55+
end
3356
end

0 commit comments

Comments
 (0)