1defmodule Logger.Config do
2  @moduledoc false
3
4  @behaviour :gen_event
5  @name __MODULE__
6  @update_counter_message {__MODULE__, :update_counter}
7
8  def configure(options) do
9    :gen_event.call(Logger, @name, {:configure, options})
10  end
11
12  def add_translator(translator) do
13    :gen_event.call(Logger, @name, {:add_translator, translator})
14  end
15
16  def remove_translator(translator) do
17    :gen_event.call(Logger, @name, {:remove_translator, translator})
18  end
19
20  ## Callbacks
21
22  def init(counter) do
23    state = load_state(counter)
24    state = update_counter(state, false)
25    schedule_update_counter(state)
26    {:ok, state}
27  end
28
29  defp load_state(counter) do
30    {counter, :log, Application.fetch_env!(:logger, :discard_threshold),
31     Application.fetch_env!(:logger, :discard_threshold_periodic_check)}
32  end
33
34  def handle_event(_event, state) do
35    {:ok, update_counter(state, false)}
36  end
37
38  def handle_call({:configure, options}, {counter, _, _, _}) do
39    Enum.each(options, fn
40      {:level, level} ->
41        :logger.set_primary_config(:level, Logger.Handler.elixir_level_to_erlang_level(level))
42
43      {key, value} ->
44        Application.put_env(:logger, key, value)
45    end)
46
47    {:ok, :ok, load_state(counter)}
48  end
49
50  def handle_call({:add_translator, translator}, state) do
51    update_translators(fn t -> [translator | List.delete(t, translator)] end)
52    {:ok, :ok, state}
53  end
54
55  def handle_call({:remove_translator, translator}, state) do
56    update_translators(&List.delete(&1, translator))
57    {:ok, :ok, state}
58  end
59
60  def handle_info(@update_counter_message, state) do
61    state = update_counter(state, true)
62    schedule_update_counter(state)
63    {:ok, state}
64  end
65
66  def handle_info(_, state) do
67    {:ok, state}
68  end
69
70  def terminate(_reason, _state) do
71    :ok
72  end
73
74  def code_change(_old, state, _extra) do
75    {:ok, state}
76  end
77
78  @counter_pos 1
79
80  defp update_counter({counter, log, discard_threshold, discard_period}, periodic_check?) do
81    # If length is more than the total, it means the counter is behind,
82    # due to non-log messages, so we need to increase the counter.
83    #
84    # If length is less than the total, then we either have a spike or
85    # the counter drifted due to failures.
86    #
87    # Because we always bump the counter and then we send the message,
88    # there is a chance clients have bumped the counter but they did not
89    # deliver the message yet. Those bumps will be lost. At the same time,
90    # we are careful to read the counter first here, so if the counter is
91    # bumped after we read from it, those bumps won't be lost.
92    total = :counters.get(counter, @counter_pos)
93    {:message_queue_len, length} = Process.info(self(), :message_queue_len)
94    :counters.add(counter, @counter_pos, length - total)
95
96    # In case we are logging but we reached the threshold, we log that we
97    # started discarding messages. This can only be reverted by the periodic
98    # discard check.
99    cond do
100      total >= discard_threshold ->
101        if log == :log or periodic_check? do
102          warn("Attempted to log #{total} messages, which is above :discard_threshold")
103        end
104
105        {counter, :discard, discard_threshold, discard_period}
106
107      log == :discard and periodic_check? ->
108        warn("Attempted to log #{total} messages, which is below :discard_threshold")
109        {counter, :log, discard_threshold, discard_period}
110
111      true ->
112        {counter, log, discard_threshold, discard_period}
113    end
114  end
115
116  defp warn(message) do
117    utc_log = Application.fetch_env!(:logger, :utc_log)
118    event = {Logger, message, Logger.Utils.timestamp(utc_log), pid: self()}
119    :gen_event.notify(self(), {:warn, Process.group_leader(), event})
120  end
121
122  defp schedule_update_counter({_, _, _, discard_period}) do
123    Process.send_after(self(), @update_counter_message, discard_period)
124  end
125
126  ## Data helpers
127
128  defp update_translators(fun) do
129    {:ok, %{config: data}} = :logger.get_handler_config(Logger)
130    translators = fun.(data.translators)
131    Application.put_env(:logger, :translators, translators)
132    :ok = :logger.update_handler_config(Logger, :config, translators: translators)
133  end
134end
135