defmodule LoggerBackends do
@moduledoc """
`:gen_event`-based logger handlers with overload protection.
This module provides backends for Elixir's Logger with
built-in overload protection. This was the default
mechanism for hooking into Elixir's Logger until Elixir v1.15.
Elixir backends run in a single separate process which comes with
overload protection. All backends run in this same process as a
unified front for handling log events.
The available backends by default are:
* `LoggerBackends.Console` - logs messages to the console
(see its documentation for more information)
Developers may also implement their own backends, an option that
is explored in more detail later.
Backends can be added and removed via `add/2` and `remove/2` functions.
This is often done in your `c:Application.start/2` callback:
@impl true
def start(_type, _args) do
LoggerBackends.add(MyCustomBackend)
# ...
end
The backend can be configured in your config files:
config :logger, MyCustomBackend,
some_config: ...
## Application configuration
Application configuration goes under the `:logger` application for
backwards compatibility. The following keys must be set before
the `:logger` application (and this application) are started.
* `:discard_threshold_periodic_check` - a periodic check that
checks and reports if logger is discarding messages. It logs a warning
message whenever the system is (or continues) in discard mode and
it logs a warning message whenever if the system was discarding messages
but stopped doing so after the previous check. By default it runs
every `30_000` milliseconds.
* `:start_options` - passes start options to LoggerBackends's main process, such
as `:spawn_opt` and `:hibernate_after`. All options in `t:GenServer.option/0`
are accepted, except `:name`.
## Runtime configuration
The following keys can be set at runtime via the `configure/1` function.
In your config files, they also go under the `:logger` application
for backwards compatibility.
* `:utc_log` - when `true`, uses UTC in logs. By default it uses
local time (i.e., it defaults to `false`).
* `:truncate` - the maximum message size to be logged (in bytes).
Defaults to 8192 bytes. Note this configuration is approximate.
Truncated messages will have `" (truncated)"` at the end.
The atom `:infinity` can be passed to disable this behavior.
* `:sync_threshold` - if the `Logger` manager has more than
`:sync_threshold` messages in its queue, `Logger` will change
to *sync mode*, to apply backpressure to the clients.
`Logger` will return to *async mode* once the number of messages
in the queue is reduced to one below the `sync_threshold`.
Defaults to 20 messages. `:sync_threshold` can be set to `0` to
force *sync mode*.
* `:discard_threshold` - if the `Logger` manager has more than
`:discard_threshold` messages in its queue, `Logger` will change
to *discard mode* and messages will be discarded directly in the
clients. `Logger` will return to *sync mode* once the number of
messages in the queue is reduced to one below the `discard_threshold`.
Defaults to 500 messages.
## Custom backends
Any developer can create their own backend. Since `Logger` is an
event manager powered by `:gen_event`, writing a new backend
is a matter of creating an event handler, as described in the
[`:gen_event`](`:gen_event`) documentation.
From now on, we will be using the term "event handler" to refer
to your custom backend, as we head into implementation details.
The event manager and all added event handlers are automatically
supervised by `Logger`. If a backend fails to start by returning
`{:error, :ignore}` from its `init/1` callback, then it's not added
to the backends but nothing fails. If a backend fails to start by
returning `{:error, reason}` from its `init/1` callback, the system
will fail to start.
Once initialized, the handler should be designed to handle the
following events:
* `{level, group_leader, {Logger, message, timestamp, metadata}}` where:
* `level` is one of `:debug`, `:info`, `:warn`, or `:error`, as previously
described (for compatibility with pre 1.10 backends the `:notice` will
be translated to `:info` and all messages above `:error` will be translated
to `:error`)
* `group_leader` is the group leader of the process which logged the message
* `{Logger, message, timestamp, metadata}` is a tuple containing information
about the logged message:
* the first element is always the atom `Logger`
* `message` is the actual message (as chardata)
* `timestamp` is the timestamp for when the message was logged, as a
`{{year, month, day}, {hour, minute, second, millisecond}}` tuple
* `metadata` is a keyword list of metadata used when logging the message
* `:flush`
It is recommended that handlers ignore messages where the group
leader is in a different node than the one where the handler is
installed. For example:
def handle_event({_level, gl, {Logger, _, _, _}}, state)
when node(gl) != node() do
{:ok, state}
end
In the case of the event `:flush` handlers should flush any pending
data. This event is triggered by `Logger.flush/0`.
Furthermore, backends can be configured via the `configure_backend/2`
function which requires event handlers to handle calls of the
following format:
{:configure, options}
where `options` is a keyword list. The result of the call is the result
returned by `configure_backend/2`. The recommended return value for
successful configuration is `:ok`. For example:
def handle_call({:configure, options}, state) do
new_state = reconfigure_state(state, options)
{:ok, :ok, new_state}
end
It is recommended that backends support at least the following configuration
options:
* `:level` - the logging level for that backend
* `:format` - the logging format for that backend
* `:metadata` - the metadata to include in that backend
Check the `LoggerBackends.Console` implementation in Elixir's codebase
for examples on how to handle the recommendations in this section and
how to process the existing options.
"""
@typedoc """
A logger handler.
"""
@typedoc since: "1.0.0"
@type backend :: :gen_event.handler()
@doc """
Applies runtime configuration to all backends.
See the module doc for more information.
"""
@backend_options [:sync_threshold, :discard_threshold, :truncate, :utc_log]
@spec configure(keyword) :: :ok
def configure(options) do
LoggerBackends.Config.configure(Keyword.take(options, @backend_options))
:ok = :logger.update_handler_config(LoggerBackends, :config, :refresh)
end
@doc """
Configures a given backend.
"""
@spec configure(backend, keyword) :: term
def configure(backend, options) when is_list(options) do
:gen_event.call(LoggerBackends, backend, {:configure, options})
end
@doc """
Adds a new backend.
Adding a backend calls the `init/1` function in that backend
with the name of the backend as its argument. For example,
calling
LoggerBackends.add(MyBackend)
will call `MyBackend.init(MyBackend)` to initialize the new
backend. If the backend's `init/1` callback returns `{:ok, _}`,
then this function returns `{:ok, pid}`. If the handler returns
`{:error, :ignore}` from `init/1`, this function still returns
`{:ok, pid}` but the handler is not started. If the handler
returns `{:error, reason}` from `init/1`, this function returns
`{:error, {reason, info}}` where `info` is more information on
the backend that failed to start.
## Options
* `:flush` - when `true`, guarantees all messages currently sent
to `Logger` are processed before the backend is added
"""
@spec add(backend, keyword) :: Supervisor.on_start_child()
def add(backend, opts \\ []) do
_ = if opts[:flush], do: Logger.flush()
case LoggerBackends.Supervisor.add(backend) do
{:ok, _} = ok ->
ok
{:error, {:already_started, _pid}} ->
{:error, :already_present}
{:error, _} = error ->
error
end
end
@doc """
Removes a backend.
## Options
* `:flush` - when `true`, guarantees all messages currently sent
to `Logger` are processed before the backend is removed
"""
@spec remove(backend, keyword) :: :ok | {:error, term}
def remove(backend, opts \\ []) do
_ = if opts[:flush], do: Logger.flush()
LoggerBackends.Supervisor.remove(backend)
end
end