Current section

Files

Jump to
livebook lib livebook runtime erl_dist logger_gl_backend.ex
Raw

lib/livebook/runtime/erl_dist/logger_gl_backend.ex

defmodule Livebook.Runtime.ErlDist.LoggerGLBackend do
@moduledoc false
# A logger backend used to forward logs to Livebook,
# as with regular output.
#
# The backend is based on `Logger.Backends.Console`,
# but instead of logging to the console, it sends
# log output to the group leader of the source process,
# provided the group leader is an instance of
# `Livebook.Evaluator.IOProxy`.
#
# Basic configuration is taken from the console
# logger configuration to match its formatting.
@behaviour :gen_event
@type state :: %{
colors: keyword(),
format: binary(),
level: atom(),
metadata: list(atom())
}
@impl true
def init(__MODULE__) do
config = Application.get_env(:logger, :console)
{:ok, init_state(config)}
end
@impl true
def handle_event({level, gl, {Logger, msg, ts, md}}, state) do
%{level: log_level} = state
if meet_level?(level, log_level) do
log_event(level, msg, ts, md, gl, state)
end
{:ok, state}
end
def handle_event(_, state) do
{:ok, state}
end
@impl true
def code_change(_old_vsn, state, _extra) do
{:ok, state}
end
@impl true
def handle_call(_message, state) do
{:ok, :ok, state}
end
@impl true
def handle_info(_message, state) do
{:ok, state}
end
@impl true
def terminate(_reason, _state) do
:ok
end
defp init_state(config) do
level = Keyword.get(config, :level)
format = Logger.Formatter.compile(Keyword.get(config, :format))
colors = configure_colors(config)
metadata = Keyword.get(config, :metadata, []) |> configure_metadata()
%{format: format, metadata: metadata, level: level, colors: colors}
end
defp configure_metadata(:all), do: :all
defp configure_metadata(metadata), do: Enum.reverse(metadata)
defp configure_colors(config) do
colors = Keyword.get(config, :colors, [])
%{
debug: Keyword.get(colors, :debug, :cyan),
info: Keyword.get(colors, :info, :normal),
warn: Keyword.get(colors, :warn, :yellow),
error: Keyword.get(colors, :error, :red),
enabled: Keyword.get(colors, :enabled, IO.ANSI.enabled?())
}
end
defp meet_level?(_lvl, nil), do: true
defp meet_level?(lvl, min) do
Logger.compare_levels(lvl, min) != :lt
end
defp log_event(level, msg, ts, md, gl, state) do
if io_proxy?(gl) do
output = format_event(level, msg, ts, md, state)
async_io(gl, output)
end
end
defp io_proxy?(pid) do
info = Process.info(pid, [:dictionary])
info[:dictionary][:"$initial_call"] == {Livebook.Evaluator.IOProxy, :init, 1}
end
defp async_io(device, output) when is_pid(device) do
send(device, {:io_request, self(), make_ref(), {:put_chars, :unicode, output}})
end
defp format_event(level, msg, ts, md, state) do
%{format: format, metadata: keys, colors: colors} = state
format
|> Logger.Formatter.format(level, msg, ts, take_metadata(md, keys))
|> color_event(level, colors, md)
end
defp take_metadata(metadata, :all) do
metadata
end
defp take_metadata(metadata, keys) do
Enum.reduce(keys, [], fn key, acc ->
case Keyword.fetch(metadata, key) do
{:ok, val} -> [{key, val} | acc]
:error -> acc
end
end)
end
defp color_event(data, _level, %{enabled: false}, _md), do: data
defp color_event(data, level, %{enabled: true} = colors, md) do
color = md[:ansi_color] || Map.fetch!(colors, level)
IO.ANSI.format([color, data])
end
end