Current section

Files

Jump to
ring_logger lib ring_logger.ex
Raw

lib/ring_logger.ex

defmodule RingLogger do
@behaviour :gen_event
@moduledoc """
This is an in-memory ring buffer backend for the Elixir Logger.
Install it by adding it to your `config.exs`:
```elixir
use Mix.Config
# Add the RingLogger backend. This removes the
# default :console backend.
config :logger, backends: [RingLogger]
# Set the number of messages to hold in the circular buffer
config :logger, RingLogger, max_size: 1024
```
Or add manually:
```elixir
Logger.add_backend(RingLogger)
Logger.configure(RingLogger, max_size: 1024)
```
Once added as a backend, you have two options depending on whether you're
accessing the `RingLogger` via the IEx prompt or via code. If you're at the
IEx prompt, use the helper methods in here like `attach`, `detach`, `next`,
`tail`, `grep`, etc. They'll automate a few things behind the scenes. If
you're writing a program that needs to get log messages, use `get` or
`start_link` a `RingLogger.Client` and call its methods directly.
"""
alias RingLogger.{Server, Autoclient}
@typedoc "Option values used by the ring logger"
@type server_option :: {:max_size, pos_integer()}
@typedoc "Callback function for printing/paging tail, grep, and next output"
@type pager_fun :: (IO.device(), iodata() -> :ok | {:error, term()})
@typedoc "Option values used by client-side functions like `attach` and `tail`"
@type client_option ::
{:io, term}
| {:pager, pager_fun()}
| {:color, term}
| {:metadata, Logger.metadata()}
| {:format, String.t() | custom_formatter}
| {:level, Logger.level()}
| {:module_levels, map()}
@typedoc "A tuple holding a raw, unformatted log entry"
@type entry ::
{module(), Logger.level(), Logger.message(), Logger.Formatter.time(), Logger.metadata()}
@typep custom_formatter :: {module, function}
#
# API
#
@doc """
Attach the current IEx session to the logger. It will start printing log messages.
Options include:
* `:io` - output location when printing. Defaults to `:stdio`
* `:colors` - a keyword list of coloring options
* `:metadata` - a keyword list of additional metadata
* `:format` - the format message used to print logs
* `:level` - the minimum log level to report by this backend. Note that the `:logger`
application's `:level` setting filters log messages prior to `RingLogger`.
* `:module_levels` - a map of log level overrides per module. For example,
%{MyModule => :error, MyOtherModule => :none}
"""
@spec attach([client_option]) :: :ok
defdelegate attach(opts \\ []), to: Autoclient
@doc """
Detach the current IEx session from the logger.
"""
@spec detach() :: :ok
defdelegate detach(), to: Autoclient
@doc """
Print the next messages in the log.
Options include:
* Options from `attach/1`
* `:pager` - a function for printing log messages to the console. Defaults to `IO.binwrite/2`.
"""
@spec next([client_option]) :: :ok | {:error, term()}
defdelegate next(opts \\ []), to: Autoclient
@doc """
Print the last n messages in the log.
Options include:
* Options from `attach/1`
* `:pager` - a function for printing log messages to the console. Defaults to `IO.binwrite/2`.
"""
@spec tail(non_neg_integer(), [client_option]) :: :ok | {:error, term()}
def tail(), do: Autoclient.tail(10, [])
def tail(opts) when is_list(opts), do: Autoclient.tail(10, opts)
def tail(n) when is_integer(n), do: Autoclient.tail(n, [])
def tail(n, opts), do: Autoclient.tail(n, opts)
@doc """
Reset the index into the log for `tail/1` to the oldest entry.
"""
@spec reset([client_option]) :: :ok | {:error, term()}
defdelegate reset(opts \\ []), to: Autoclient
@doc """
Run a regular expression on each entry in the log and print out the matchers.
For example:
iex> RingLogger.grep(~r/something/)
:ok
Options include:
* Options from `attach/1`
* `:pager` - a function for printing log messages to the console. Defaults to `IO.binwrite/2`.
"""
@spec grep(Regex.t() | String.t(), [client_option]) :: :ok | {:error, term()}
defdelegate grep(regex_or_string, opts \\ []), to: Autoclient
@doc """
Helper method for formatting log messages per the current client's
configuration.
"""
@spec format(entry()) :: :ok
defdelegate format(message), to: Autoclient
@doc """
Get n log messages starting at the specified index.
Set n to 0 to get entries to the end
"""
@spec get(non_neg_integer(), non_neg_integer()) :: [entry()]
defdelegate get(index \\ 0, n \\ 0), to: Server
@doc """
Update the logger configuration.
Options include:
* `:max_size` - the max number of log messages to store at a time
"""
@spec configure([server_option]) :: :ok
defdelegate configure(opts), to: Server
#
# Logger backend callbacks
#
@spec init(module()) :: {:ok, term()} | {:error, term()}
def init(__MODULE__) do
init({__MODULE__, []})
end
@spec init({module(), list()}) :: {:ok, term()} | {:error, term()}
def init({__MODULE__, opts}) when is_list(opts) do
env = Application.get_env(:logger, __MODULE__, [])
opts = Keyword.merge(env, opts)
Application.put_env(:logger, __MODULE__, opts)
Server.start_link(opts)
{:ok, configure(opts)}
end
def handle_call({:configure, opts}, _state) do
env = Application.get_env(:logger, __MODULE__, [])
opts = Keyword.merge(env, opts)
Application.put_env(:logger, __MODULE__, opts)
{:ok, :ok, configure(opts)}
end
def handle_event({level, _group_leader, message}, state) do
Server.log(level, message)
{:ok, state}
end
def handle_event(:flush, state) do
# No flushing needed for RingLogger
{:ok, state}
end
def handle_info(_, state) do
# Ignore everything else since it's hard to justify RingLogger crashing
# on a bad message.
{:ok, state}
end
def code_change(_old_vsn, state, _extra) do
{:ok, state}
end
def terminate(_reason, _state) do
Server.stop()
:ok
end
end