Skip to content

Commit a6928f7

Browse files
josevalimJosé Valim
authored and
José Valim
committed
Add discard_threshold to Logger (#7193)
1 parent 3d08d24 commit a6928f7

File tree

8 files changed

+228
-102
lines changed

8 files changed

+228
-102
lines changed

lib/logger/lib/logger.ex

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ defmodule Logger do
130130
in the queue is reduced to `sync_threshold * 0.75` messages.
131131
Defaults to 20 messages.
132132
133+
* `:discard_threshold` - if the `Logger` manager has more than
134+
`:discard_threshold` messages in its queue, `Logger` will change
135+
to *discard mode* and messages will be discarded directly in the
136+
clients. `Logger` will return to *sync mode* once the number of
137+
messages in the queue is reduced to `discard_threshold * 0.75`
138+
messages. Defaults to 500 messages.
139+
133140
* `:translator_inspect_opts` - when translating OTP reports and
134141
errors, the last message and state must be inspected in the
135142
error reports. This configuration allow developers to change
@@ -159,21 +166,25 @@ defmodule Logger do
159166
in Erlang syntax until the Logger application kicks in and
160167
uninstalls SASL's logger in favor of its own. Defaults to `false`.
161168
162-
* `:discard_threshold_for_error_logger` - a value that, when
163-
reached, triggers the error logger to discard messages. This
164-
value must be a positive number that represents the maximum
165-
number of messages accepted per second. Once above this
166-
threshold, the [`:error_logger`](http://erlang.org/doc/man/error_logger.html)
167-
enters discard mode for the remainder of that second. Defaults to 500 messages.
169+
* `:discard_threshold_for_error_logger` - if `:error_logger` has more than
170+
`discard_threshold` messages in its inbox, messages will be dropped
171+
until the message queue goes down to `discard_threshold * 0.75`
172+
entries. The threshold will be checked once again after 10% of thereshold
173+
messages are processed, to avoid messages from being constantly dropped.
174+
For exmaple, if the thereshold is 500 (the default) and the inbox has
175+
600 messages, 250 messages will dropped, bringing the inbox down to
176+
350 (0.75 * threshold) entries and 50 (0.1 * theshold) messages will
177+
be processed before the threshold is checked once again.
168178
169-
For example, to configure `Logger` to redirect all [`:error_logger`](http://erlang.org/doc/man/error_logger.html)
170-
messages using a `config/config.exs` file:
179+
For example, to configure `Logger` to redirect all
180+
[`:error_logger`](http://erlang.org/doc/man/error_logger.html) messages
181+
using a `config/config.exs` file:
171182
172183
config :logger,
173184
handle_otp_reports: true,
174185
handle_sasl_reports: true
175186
176-
Furthermore, `Logger` allows messages sent by OTP's [`:error_logger`](http://erlang.org/doc/man/error_logger.html)
187+
Furthermore, `Logger` allows messages sent by OTP's `:error_logger`
177188
to be translated into an Elixir format via translators. Translators
178189
can be dynamically added at any time with the `add_translator/1`
179190
and `remove_translator/1` APIs. Check `Logger.Translator` for more
@@ -600,7 +611,7 @@ defmodule Logger do
600611
%{mode: mode, truncate: truncate, level: min_level, utc_log: utc_log?} =
601612
Logger.Config.__data__()
602613

603-
if compare_levels(level, min_level) != :lt do
614+
if compare_levels(level, min_level) != :lt and mode != :discard do
604615
metadata = [pid: self()] ++ Keyword.merge(pdict, metadata)
605616
{message, metadata} = normalize_message(chardata_or_fun, metadata)
606617
truncated = truncate(message, truncate)

lib/logger/lib/logger/config.ex

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,28 @@ defmodule Logger.Config do
105105
{:ok, state}
106106

107107
new_mode ->
108+
if new_mode == :discard do
109+
message =
110+
"Logger has #{message_queue_length()} messages in its queue, " <>
111+
"which is above :discard_threshold. Messages will be discarded " <>
112+
"until the message queue goes back to 75% of the threshold size"
113+
114+
log(:warn, message, state)
115+
end
116+
117+
if mode == :discard do
118+
log(:warn, "Logger has stopped discarding messages", state)
119+
end
120+
108121
{:ok, persist(%{state | mode: new_mode})}
109122
end
110123
end
111124

125+
defp log(level, message, state) do
126+
event = {Logger, message, Logger.Utils.timestamp(state.utc_log), pid: self()}
127+
:gen_event.notify(self(), {level, Process.group_leader(), event})
128+
end
129+
112130
def handle_call(:backends, state) do
113131
{:ok, Application.get_env(:logger, :backends), state}
114132
end
@@ -162,18 +180,27 @@ defmodule Logger.Config do
162180
## Helpers
163181

164182
defp compute_mode(state) do
165-
{:message_queue_len, len} = Process.info(self(), :message_queue_len)
166-
167-
cond do
168-
len > state.sync_threshold and state.mode == :async ->
169-
:sync
183+
%{
184+
mode: mode,
185+
async_threshold: async_threshold,
186+
sync_threshold: sync_threshold,
187+
keep_threshold: keep_threshold,
188+
discard_threshold: discard_threshold
189+
} = state
170190

171-
len < state.async_threshold and state.mode == :sync ->
172-
:async
191+
Logger.Utils.compute_mode(
192+
mode,
193+
message_queue_length(),
194+
async_threshold,
195+
sync_threshold,
196+
keep_threshold,
197+
discard_threshold
198+
)
199+
end
173200

174-
true ->
175-
state.mode
176-
end
201+
defp message_queue_length() do
202+
{:message_queue_len, messages} = Process.info(self(), :message_queue_len)
203+
messages
177204
end
178205

179206
defp update_backends(fun) do
@@ -196,22 +223,24 @@ defmodule Logger.Config do
196223
sync_threshold = Application.get_env(:logger, :sync_threshold)
197224
async_threshold = trunc(sync_threshold * 0.75)
198225

226+
discard_threshold = Application.get_env(:logger, :discard_threshold)
227+
keep_threshold = trunc(discard_threshold * 0.75)
228+
199229
state = %{
200230
level: level,
201231
mode: mode,
202232
truncate: truncate,
203233
utc_log: utc_log,
204-
sync_threshold: sync_threshold,
205234
async_threshold: async_threshold,
235+
sync_threshold: sync_threshold,
236+
keep_threshold: keep_threshold,
237+
discard_threshold: discard_threshold,
206238
translators: translators
207239
}
208240

209241
case compute_mode(state) do
210-
^mode ->
211-
persist(state)
212-
213-
new_mode ->
214-
persist(%{state | mode: new_mode})
242+
^mode -> persist(state)
243+
new_mode -> persist(%{state | mode: new_mode})
215244
end
216245
end
217246

lib/logger/lib/logger/error_handler.ex

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
defmodule Logger.ErrorHandler do
22
@moduledoc false
3-
43
@behaviour :gen_event
54

6-
require Logger
7-
85
def init({otp?, sasl?, threshold}) do
96
# We store the Logger PID in the state because when we are shutting
107
# down the Logger application, the Logger process may be terminated
@@ -14,11 +11,10 @@ defmodule Logger.ErrorHandler do
1411
state = %{
1512
otp: otp?,
1613
sasl: sasl?,
17-
threshold: threshold,
14+
discard_threshold: threshold,
15+
keep_threshold: trunc(threshold * 0.75),
1816
logger: Process.whereis(Logger),
19-
last_length: 0,
20-
last_time: :os.timestamp(),
21-
dropped: 0
17+
skip: 0
2218
}
2319

2420
{:ok, state}
@@ -31,7 +27,7 @@ defmodule Logger.ErrorHandler do
3127
end
3228

3329
def handle_event(event, state) do
34-
state = check_threshold(state)
30+
state = check_threshold_unless_skipping(state)
3531
log_event(event, state)
3632
{:ok, state}
3733
end
@@ -84,10 +80,15 @@ defmodule Logger.ErrorHandler do
8480
defp log_event(_, _state), do: :ok
8581

8682
defp log_event(level, kind, gl, pid, {type, _} = data, state) do
87-
%{level: min_level, truncate: truncate, utc_log: utc_log?, translators: translators} =
88-
Logger.Config.__data__()
89-
90-
with log when log != :lt <- Logger.compare_levels(level, min_level),
83+
%{
84+
mode: mode,
85+
level: min_level,
86+
truncate: truncate,
87+
utc_log: utc_log?,
88+
translators: translators
89+
} = Logger.Config.__data__()
90+
91+
with true <- Logger.compare_levels(level, min_level) != :lt and mode != :discard,
9192
{:ok, message} <- translate(translators, min_level, level, kind, data, truncate) do
9293
message = Logger.Utils.truncate(message, truncate)
9394

@@ -106,31 +107,35 @@ defmodule Logger.ErrorHandler do
106107
defp ensure_pid(pid) when is_pid(pid), do: pid
107108
defp ensure_pid(_), do: self()
108109

109-
defp check_threshold(state) do
110-
%{last_time: last_time, last_length: last_length, dropped: dropped, threshold: threshold} =
111-
state
110+
defp check_threshold_unless_skipping(%{skip: 0} = state) do
111+
check_threshold(state)
112+
end
112113

113-
{m, s, _} = current_time = :os.timestamp()
114-
current_length = message_queue_length()
114+
defp check_threshold_unless_skipping(%{skip: skip} = state) do
115+
%{state | skip: skip - 1}
116+
end
115117

116-
cond do
117-
match?({^m, ^s, _}, last_time) and current_length - last_length > threshold ->
118-
count = drop_messages(current_time, 0)
119-
%{state | dropped: dropped + count, last_length: message_queue_length()}
118+
def check_threshold(state) do
119+
%{discard_threshold: discard_threshold, keep_threshold: keep_threshold} = state
120+
current_length = message_queue_length()
120121

121-
match?({^m, ^s, _}, last_time) ->
122-
state
122+
if current_length >= discard_threshold do
123+
to_drop = current_length - keep_threshold
124+
drop_messages(to_drop)
123125

124-
true ->
125-
if dropped > 0 do
126-
message =
127-
"Logger dropped #{dropped} OTP/SASL messages as it " <>
128-
"exceeded the amount of #{threshold} messages/second"
126+
message =
127+
"Logger dropped #{to_drop} OTP/SASL messages as it had #{current_length} messages in " <>
128+
"its inbox, exceeding the amount of :discard_threshold #{discard_threshold} messages. " <>
129+
"The number of messages was reduced to #{keep_threshold} (75% of the threshold)"
129130

130-
Logger.warn(message)
131-
end
131+
%{utc_log: utc_log?} = Logger.Config.__data__()
132+
event = {Logger, message, Logger.Utils.timestamp(utc_log?), pid: self()}
133+
:gen_event.notify(state.logger, {:warn, Process.group_leader(), event})
132134

133-
%{state | dropped: 0, last_time: current_time, last_length: current_length}
135+
# We won't check the threshold for the next 10% of the threshold messages
136+
%{state | skip: trunc(discard_threshold * 0.1)}
137+
else
138+
state
134139
end
135140
end
136141

@@ -139,17 +144,15 @@ defmodule Logger.ErrorHandler do
139144
len
140145
end
141146

142-
defp drop_messages({m, s, _} = last_time, count) do
143-
case :os.timestamp() do
144-
{^m, ^s, _} ->
145-
receive do
146-
{:notify, _event} -> drop_messages(last_time, count + 1)
147-
after
148-
0 -> count
149-
end
147+
defp drop_messages(0) do
148+
:ok
149+
end
150150

151-
_ ->
152-
count
151+
defp drop_messages(count) do
152+
receive do
153+
{:notify, _event} -> drop_messages(count - 1)
154+
after
155+
0 -> :ok
153156
end
154157
end
155158

lib/logger/lib/logger/utils.ex

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,37 @@
11
defmodule Logger.Utils do
22
@moduledoc false
33

4+
@doc """
5+
Computes the logging mode.
6+
7+
The result may be either :sync, :async or :discard.
8+
"""
9+
@spec compute_mode(
10+
mode,
11+
non_neg_integer(),
12+
non_neg_integer(),
13+
non_neg_integer(),
14+
non_neg_integer(),
15+
non_neg_integer()
16+
) :: mode
17+
when mode: :sync | :async | :discard
18+
def compute_mode(
19+
mode,
20+
messages,
21+
async_threshold,
22+
sync_threshold,
23+
keep_threshold,
24+
discard_threshold
25+
) do
26+
case mode do
27+
_ when messages >= discard_threshold -> :discard
28+
:discard when messages > keep_threshold -> :discard
29+
_ when messages >= sync_threshold -> :sync
30+
:sync when messages > async_threshold -> :sync
31+
_ -> :async
32+
end
33+
end
34+
435
@doc """
536
Truncates a `chardata` into `n` bytes.
637

lib/logger/mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ defmodule Logger.MixProject do
2020
backends: [:console],
2121
translators: [{Logger.Translator, :translate}],
2222
sync_threshold: 20,
23+
discard_threshold: 500,
2324
handle_otp_reports: true,
2425
handle_sasl_reports: false,
26+
discard_threshold_for_error_logger: 500,
2527
compile_time_purge_level: :debug,
2628
compile_time_application: nil,
27-
discard_threshold_for_error_logger: 500,
2829
translator_inspect_opts: [],
2930
console: []
3031
]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
defmodule Logger.ConfigTest do
2+
use Logger.Case
3+
require Logger
4+
5+
test "log/2 relies on sync_threshold" do
6+
Logger.remove_backend(:console)
7+
Logger.configure(sync_threshold: 0)
8+
for _ <- 1..1000, do: Logger.log(:info, "some message")
9+
after
10+
Logger.configure(sync_threshold: 20)
11+
Logger.add_backend(:console)
12+
end
13+
14+
test "log/2 relies on discard_threshold" do
15+
Logger.remove_backend(:console)
16+
Logger.configure(discard_threshold: 0)
17+
for _ <- 1..1000, do: Logger.log(:info, "some message")
18+
after
19+
Logger.configure(discard_threshold: 10000)
20+
Logger.add_backend(:console)
21+
end
22+
23+
test "restarts Logger.Config on Logger exits" do
24+
Process.whereis(Logger) |> Process.exit(:kill)
25+
wait_for_logger()
26+
wait_for_handler(Logger, Logger.Config)
27+
wait_for_handler(:error_logger, Logger.ErrorHandler)
28+
end
29+
30+
test "Logger.Config updates config on config_change/3" do
31+
:ok = Logger.configure(level: :debug)
32+
33+
try do
34+
Application.put_env(:logger, :level, :error)
35+
assert Logger.App.config_change([level: :error], [], []) === :ok
36+
assert Logger.level() === :error
37+
after
38+
Logger.configure(level: :debug)
39+
end
40+
end
41+
end

0 commit comments

Comments
 (0)