Add Logger

This commit is contained in:
José Valim
2014-07-30 19:46:26 +02:00
parent 6db5091378
commit aeefd59b15
19 changed files with 1911 additions and 16 deletions
+4 -2
View File
@@ -68,7 +68,7 @@ erlang:
# Since Mix depends on EEx and EEx depends on
# Mix, we first compile EEx without the .app
# file, then mix and then compile EEx fully
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit eex iex
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
stdlib: $(KERNEL) VERSION
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
@@ -90,6 +90,7 @@ $(UNICODE): lib/elixir/unicode/*
$(Q) cd lib/elixir && ../../$(ELIXIRC) unicode/unicode.ex -o ebin;
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
$(eval $(call APP_TEMPLATE,logger,Logger))
$(eval $(call APP_TEMPLATE,eex,EEx))
$(eval $(call APP_TEMPLATE,mix,Mix))
$(eval $(call APP_TEMPLATE,iex,IEx))
@@ -133,6 +134,7 @@ docs: compile ../ex_doc/bin/ex_doc
$(call DOCS,Mix,mix,Mix)
$(call DOCS,IEx,iex,IEx)
$(call DOCS,ExUnit,ex_unit,ExUnit)
$(call DOCS,ExUnit,logger,Logger)
../ex_doc/bin/ex_doc:
@ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
@@ -164,7 +166,7 @@ $(TEST_EBIN)/%.beam: $(TEST_ERL)/%.erl
$(Q) mkdir -p $(TEST_EBIN)
$(Q) $(ERLC) -o $(TEST_EBIN) $<
test_elixir: test_stdlib test_ex_unit test_doc_test test_mix test_eex test_iex
test_elixir: test_stdlib test_ex_unit test_logger test_doc_test test_mix test_eex test_iex
test_doc_test: compile
@ echo "==> doctest (exunit)"
+7 -7
View File
@@ -2,7 +2,7 @@
%% private to the Elixir compiler and reserved to be used by Elixir only.
-module(elixir).
-behaviour(application).
-export([main/1, start_cli/0,
-export([start_cli/0,
string_to_quoted/4, 'string_to_quoted!'/4,
env_for_eval/1, env_for_eval/2, quoted_to_erl/2, quoted_to_erl/3,
eval/2, eval/3, eval_forms/3, eval_forms/4, eval_quoted/3]).
@@ -50,16 +50,16 @@ stop(_S) ->
config_change(_Changed, _New, _Remove) ->
ok.
%% escript entry point
main(Args) ->
{ok, _} = application:ensure_all_started(?MODULE),
'Elixir.Kernel.CLI':main(Args).
%% Boot and process given options. Invoked by Elixir's script.
start_cli() ->
{ok, _} = application:ensure_all_started(?MODULE),
%% We start the Logger so tools that depend on Elixir
%% always have the Logger directly accessible. However
%% Logger is not a dependency of the Elixir application,
%% which means releases that want to use Logger must
%% always list it as part of its applications.
_ = application:start(logger),
'Elixir.Kernel.CLI':main(init:get_plain_arguments()).
%% EVAL HOOKS
+464
View File
@@ -0,0 +1,464 @@
defmodule Logger do
use Application
@moduledoc ~S"""
A logger for Elixir applications.
It includes many features:
* Provides debug, info, warn and error levels.
* Supports multiple backends which are automatically
supervised when plugged into Logger.
* Formats and truncates messages on the client
to avoid clogging logger backends.
* Alternates between sync and async modes to keep
it performant when required but also apply back-
pressure when under stress.
* Wraps OTP's error_logger to avoid it from
overflowing.
## Levels
The supported levels are:
* `:debug` - for debug-related messages
* `:info` - for information of any kind
* `:warn` - for warnings
* `:error` - for errors
## Configuration
Logger supports a wide range of configuration.
This configuration is split in three categories:
* Application configuration - must be set before the logger
application is started
* Runtime configuration - can be set before the logger
application is started but changed during runtime
* Error logger configuration - configuration for the
wrapper around OTP's error_logger
### Application configuration
The following configuration must be set via config files
before the logger application is started.
* `:backends` - the backends to be used. Defaults to `[:console]`.
See the "Backends" section for more information.
* `:compile_time_purge_level` - purge all calls that have log level
lower than the configured value at compilation time. This means the
Logger call will be completely removed at compile time, occuring
no overhead at runtime. By default, defaults to `:debug` and only
applies to the `Logger.debug`, `Logger.info`, etc style of calls.
### Runtime Configuration
All configuration below can be set via the config files but also
changed dynamically during runtime via `Logger.configure/1`.
* `:level` - the logging level. Attempting to log any message
with severity less than the configured level will simply
cause the message to be ignored. Keep in mind that each backend
may have its specific level too.
* `: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. Defaults
to 8192 bytes. Note this configuration is approximate. Truncated
messages will have " (truncated)" at the end.
* `:sync_threshold` - if the logger manager has more than
`sync_threshold` messages in its queue, logger will change
to sync mode, to apply back-pressure to the clients.
Logger will return to sync mode once the number of messages
in the queue reduce to `sync_threshold * 0.75` messages.
Defaults to 20 messages.
### Error logger configuration
The following configuration applies to the Logger wrapper around
Erlang's error_logger. All the configurations below must be set
before the logger application starts.
* `:handle_otp_reports` - redirects OTP reports to Logger so
they are formatted in Elixir terms. This uninstalls Erlang's
logger that prints terms to terminal.
* `:discard_threshold_for_error_logger` - a value that, when
reached, triggers the error logger to discard messages. This
value must be a positive number that represents the maximum
number of messages accepted per second. Once above this
threshold, the error_logger enters in discard mode for the
remaining of that second. Defaults to 500 messages.
Furthermore, Logger allows messages sent by Erlang's `error_logger`
to be translated into an Elixir format via translators. Translator
can be dynamically added at any time with the `add_translator/1`
and `remove_translator/1` APIs. Check `Logger.Translator` for more
information.
## Backends
Logger supports different backends where log messages are written to.
The available backends by default are:
* `:console` - Logs messages to the console (enabled by default)
Developers may also implement their own backends, an option that
is explored with detail below.
The initial backends are loaded via the `:backends` configuration,
which must be set before the logger application is started. However,
backends can be added or removed dynamically via the `add_backend/2`,
`remove_backend/1` and `configure_backend/2` functions. Note though
that dynamically added backends are not restarded in case of crashes.
### Console backend
The console backend logs message to the console. It supports the
following options:
* `:level` - the level to be logged by this backend.
Note though messages are first filtered by the general
`:level` configuration in `:logger`
* `:format` - the format message used to print logs.
Defaults to: "$time $metadata[$level] $message\n"
* `:metadata` - the metadata to be printed by `$metadata`.
Defaults to an empty list (no metadata)
Here is an example on how to configure the `:console` in a
`config/config.exs` file:
config :logger, :console,
format: "$date $time [$level] $metadata$message\n",
metadata: [:user_id]
You can read more about formatting in `Logger.Formatter`.
### Custom backends
Any developer can create their own backend for Logger.
Since Logger is an event manager powered by `GenEvent`,
writing a new backend is a matter of creating an event
handler, as described in the `GenEvent` module.
From now on, we will be using event handler to refer to
your custom backend, as we head into implementation details.
The `add_backend/1` function is used to start a new
backend, which installs the given event handler to the
Logger event manager. This event handler is automatically
supervised by Logger.
Once added, the handler should be able to handle events
in the following format:
{level, group_leader,
{Logger, message, timestamp, metadata}}
The level is one of `:error`, `:info`, `:warn` or `:error`,
as previously described, the group leader is the group
leader of the process who logged the message, followed by
a tuple starting with the atom `Logger`, the message as
iodata, the timestamp and a keyword list of metadata.
It is recommended that handlers ignore messages where
the group leader is in a different node than the one
the handler is installed.
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`. You may simply
return `:ok` if you don't perform any kind of validation.
It is recommended that backends support at least the following
configuration values:
* level - the logging level for that backend
* format - the logging format for that backend
* metadata - the metadata to include the backend
Check the implementation for `Logger.Backends.Console` for
examples on how to handle the recommendations in this section
and how to process the existing options.
"""
@type backend :: GenEvent.handler
@type level :: :error | :info | :warn | :debug
@levels [:error, :info, :warn, :debug]
@doc false
def start(_type, _args) do
import Supervisor.Spec
otp_reports? = Application.get_env(:logger, :handle_otp_reports)
threshold = Application.get_env(:logger, :discard_threshold_for_error_logger)
handlers =
for backend <- Application.get_env(:logger, :backends) do
{Logger, translate_backend(backend), []}
end
options = [strategy: :rest_for_one, name: Logger.Supervisor]
children = [worker(GenEvent, [[name: Logger]]),
worker(Logger.Watcher, [Logger, Logger.Config, []],
[id: Logger.Config, function: :watcher]),
supervisor(Logger.Watcher, [handlers]),
worker(Logger.Watcher,
[:error_logger, Logger.ErrorHandler, {otp_reports?, threshold}],
[id: Logger.ErrorHandler, function: :watcher])]
case Supervisor.start_link(children, options) do
{:ok, _} = ok ->
deleted = delete_error_logger_handler(otp_reports?, :error_logger_tty_h, [])
store_deleted_handlers(deleted)
ok
{:error, _} = error ->
error
end
end
@doc false
def stop(_) do
Application.get_env(:logger, :deleted_handlers)
|> Enum.each(&:error_logger.add_report_handler/1)
# We need to do this in another process as the Application
# Controller is currently blocked shutting down this app.
spawn_link(fn -> Logger.Config.clear_data end)
:ok
end
defp store_deleted_handlers(list) do
Application.put_env(:logger, :deleted_handlers, Enum.into(list, HashSet.new))
end
defp delete_error_logger_handler(should_delete?, handler, deleted) do
if should_delete? and
:error_logger.delete_report_handler(handler) != {:error, :module_not_found} do
[handler|deleted]
else
deleted
end
end
@metadata :logger_metadata
@doc """
Adds the given keyword list to the current process metadata.
"""
def metadata(dict) do
Process.put(@metadata, dict ++ metadata)
end
@doc """
Reads the current process metadata.
"""
def metadata() do
Process.get(@metadata) || []
end
@doc """
Retrieves the logger level.
The logger level can be changed via `configure/1`.
"""
@spec level() :: level
def level() do
check_logger!
%{level: level} = Logger.Config.__data__
level
end
@doc """
Compare log levels.
Receives to log levels and compares the `left`
against `right` and returns `:lt`, `:eq` or `:gt`.
"""
@spec compare_levels(level, level) :: :lt | :eq | :gt
def compare_levels(level, level), do:
:eq
def compare_levels(left, right), do:
if(level_to_number(left) > level_to_number(right), do: :gt, else: :lt)
defp level_to_number(:debug), do: 0
defp level_to_number(:info), do: 1
defp level_to_number(:warn), do: 2
defp level_to_number(:error), do: 3
@doc """
Configures the logger.
See the "Runtime Configuration" section in `Logger` module
documentation for the available options.
"""
@valid_options [:compile_time_purge_level, :sync_threshold, :truncate, :level, :utc_log]
def configure(options) do
Logger.Config.configure(Dict.take(options, @valid_options))
end
@doc """
Adds a new backend.
"""
def add_backend(backend) do
Logger.Watcher.watch(Logger, translate_backend(backend), [])
end
@doc """
Removes a backend.
"""
def remove_backend(backend) do
Logger.Watcher.unwatch(Logger, translate_backend(backend))
end
@doc """
Adds a new translator.
"""
def add_translator({mod, fun} = translator) when is_atom(mod) and is_atom(fun) do
Logger.Config.add_translator(translator)
end
@doc """
Removes a translator.
"""
def remove_translator({mod, fun} = translator) when is_atom(mod) and is_atom(fun) do
Logger.Config.remove_translator(translator)
end
@doc """
Configures the given backend.
"""
@spec configure_backend(backend, Keywowrd.t) :: term
def configure_backend(backend, options) when is_list(options) do
GenEvent.call(Logger, translate_backend(backend), {:configure, options})
end
defp translate_backend(:console), do: Logger.Backends.Console
defp translate_backend(other), do: other
@doc """
Logs a message.
Developers should rather use the macros `Logger.debug/2`,
`Logger.warn/2`, `Logger.info/2` or `Logger.error/2` instead
of this function as they automatically include caller metadata
and can eliminate the Logger call altogether at compile time if
desired.
Use this function only when there is a need to log dynamically
or you want to explicitly avoid embedding metadata.
"""
@spec log(level, IO.chardata | (() -> IO.chardata), Keyword.t) :: :ok
def log(level, chardata, metadata \\ []) when level in @levels and is_list(metadata) do
check_logger!
%{mode: mode, truncate: truncate,
level: min_level, utc_log: utc_log?} = Logger.Config.__data__
if compare_levels(level, min_level) != :lt do
tuple = {Logger, truncate(chardata, truncate), Logger.Utils.timestamp(utc_log?),
[pid: self()] ++ metadata() ++ metadata}
notify(mode, {level, Process.group_leader(), tuple})
end
:ok
end
@doc """
Logs a warning.
## Examples
Logger.warn "knob turned too much to the right"
Logger.warn fn -> "expensive to calculate warning" end
"""
defmacro warn(chardata, metadata \\ []) do
macro_log(:warn, chardata, metadata, __CALLER__)
end
@doc """
Logs some info.
## Examples
Logger.info "mission accomplished"
Logger.info fn -> "expensive to calculate info" end
"""
defmacro info(chardata, metadata \\ []) do
macro_log(:info, chardata, metadata, __CALLER__)
end
@doc """
Logs an error.
## Examples
Logger.error "oops"
Logger.error fn -> "expensive to calculate error" end
"""
defmacro error(chardata, metadata \\ []) do
macro_log(:error, chardata, metadata, __CALLER__)
end
@doc """
Logs a debug message.
## Examples
Logger.debug "hello?"
Logger.debug fn -> "expensive to calculate debug" end
"""
defmacro debug(chardata, metadata \\ []) do
macro_log(:debug, chardata, metadata, __CALLER__)
end
defp macro_log(level, chardata, metadata, caller) do
min_level = Application.get_env(:logger, :compile_time_purge_level, :debug)
if compare_levels(level, min_level) != :lt do
%{module: module, function: function, line: line} = caller
caller = [module: module, function: function, line: line]
quote do
Logger.log(unquote(level), unquote(chardata), unquote(caller) ++ unquote(metadata))
end
else
:ok
end
end
defp truncate(data, n) when is_function(data, 0),
do: Logger.Utils.truncate(data.(), n)
defp truncate(data, n) when is_list(data) or is_binary(data),
do: Logger.Utils.truncate(data, n)
defp notify(:sync, msg), do: GenEvent.sync_notify(Logger, msg)
defp notify(:async, msg), do: GenEvent.notify(Logger, msg)
defp check_logger! do
unless Process.whereis(Logger) do
raise "Cannot log messages, the :logger application is not running"
end
end
end
+47
View File
@@ -0,0 +1,47 @@
defmodule Logger.Backends.Console do
use GenEvent
def init(_) do
if user = Process.whereis(:user) do
Process.group_leader(self(), user)
{:ok, configure([])}
else
{:error, :ignore}
end
end
def handle_call({:configure, options}, _state) do
{:ok, :ok, configure(options)}
end
def handle_event({_level, gl, _event}, state) when node(gl) != node() do
{:ok, state}
end
def handle_event({level, _gl, {Logger, msg, ts, md}}, %{level: min_level} = state) do
if nil?(min_level) or Logger.compare_levels(level, min_level) != :lt do
log_event(level, msg, ts, md, state)
end
{:ok, state}
end
## Helpers
defp configure(options) do
console = Keyword.merge(Application.get_env(:logger, :console, []), options)
Application.put_env(:logger, :console, console)
format = console
|> Keyword.get(:format)
|> Logger.Formatter.compile
level = Keyword.get(console, :level)
metadata = Keyword.get(console, :metadata, [])
%{format: format, metadata: metadata, level: level}
end
defp log_event(level, msg, ts, md, %{format: format, metadata: metadata}) do
:io.put_chars :user,
Logger.Formatter.format(format, level, msg, ts, Dict.take(md, metadata))
end
end
+117
View File
@@ -0,0 +1,117 @@
defmodule Logger.Config do
@moduledoc false
use GenEvent
@name __MODULE__
@data :__data__
def start_link do
GenServer.start_link(__MODULE__, :ok, name: @name)
end
def configure(options) do
GenEvent.call(Logger, @name, {:configure, options})
end
def add_translator(translator) do
GenEvent.call(Logger, @name, {:add_translator, translator})
end
def remove_translator(translator) do
GenEvent.call(Logger, @name, {:remove_translator, translator})
end
def __data__() do
Application.get_env(:logger, @data)
end
def clear_data() do
Application.delete_env(:logger, @data)
end
def restart do
set = Application.get_env(:logger, :deleted_handlers)
Application.put_env(:logger, :deleted_handlers, HashSet.new)
Application.stop(:logger)
Enum.each(set, &:error_logger.add_report_handler/1)
Application.start(:logger)
end
## Callbacks
def init(_) do
# Use previous data if available in case this handler crashed.
state = __data__ || compute_state(:async)
{:ok, state}
end
def handle_event({_type, gl, _msg} = event, state) when node(gl) != node() do
# Cross node messages are always async which also
# means this handler won't crash in case there is
# no logger installed in the other node.
GenEvent.notify({Logger, node(gl)}, event)
{:ok, state}
end
def handle_event(_event, state) do
{:message_queue_len, len} = Process.info(self(), :message_queue_len)
cond do
len > state.sync_threshold and state.mode == :async ->
state = %{state | mode: :sync}
persist(state)
{:ok, state}
len < state.async_threshold and state.mode == :sync ->
state = %{state | mode: :async}
persist(state)
{:ok, state}
true ->
{:ok, state}
end
end
def handle_call({:configure, options}, state) do
Enum.each options, fn {key, value} ->
Application.put_env(:logger, key, value)
end
{:ok, :ok, compute_state(state.mode)}
end
def handle_call({:add_translator, translator}, state) do
state = update_translators(state, fn t -> [translator|List.delete(t, translator)] end)
{:ok, :ok, state}
end
def handle_call({:remove_translator, translator}, state) do
state = update_translators(state, &List.delete(&1, translator))
{:ok, :ok, state}
end
## Helpers
defp update_translators(%{translators: translators} = state, fun) do
translators = fun.(translators)
Application.put_env(:logger, :translators, translators)
persist %{state | translators: translators}
end
defp compute_state(mode) do
level = Application.get_env(:logger, :level)
utc_log = Application.get_env(:logger, :utc_log)
truncate = Application.get_env(:logger, :truncate)
translators = Application.get_env(:logger, :translators)
sync_threshold = Application.get_env(:logger, :sync_threshold)
async_threshold = trunc(sync_threshold * 0.75)
persist %{level: level, mode: mode, truncate: truncate,
utc_log: utc_log, sync_threshold: sync_threshold,
async_threshold: async_threshold, translators: translators}
end
defp persist(state) do
Application.put_env(:logger, @data, state)
state
end
end
+117
View File
@@ -0,0 +1,117 @@
defmodule Logger.ErrorHandler do
use GenEvent
require Logger
def init({otp?, threshold}) do
{:ok, %{otp: otp?, threshold: threshold,
last_length: 0, last_time: :os.timestamp, dropped: 0}}
end
## Handle event
def handle_event({_type, gl, _msg}, state) when node(gl) != node() do
{:ok, state}
end
def handle_event(event, state) do
state = check_threshold(state)
log_event(event, state)
{:ok, state}
end
## Helpers
defp log_event({:error, _gl, {pid, format, data}}, %{otp: true}),
do: log_event(:error, :format, pid, {format, data})
defp log_event({:error_report, _gl, {pid, :std_error, format}}, %{otp: true}),
do: log_event(:error, :report, pid, {:std_error, format})
defp log_event({:warning_msg, _gl, {pid, format, data}}, %{otp: true}),
do: log_event(:warn, :format, pid, {format, data})
defp log_event({:warning_report, _gl, {pid, :std_warning, format}}, %{otp: true}),
do: log_event(:warn, :report, pid, {:std_warning, format})
defp log_event({:info_msg, _gl, {pid, format, data}}, %{otp: true}),
do: log_event(:info, :format, pid, {format, data})
defp log_event({:info_report, _gl, {pid, :std_info, format}}, %{otp: true}),
do: log_event(:info, :report, pid, {:std_info, format})
defp log_event(_, _state),
do: :ok
defp log_event(level, kind, pid, data) do
%{level: min_level, truncate: truncate,
utc_log: utc_log?, translators: translators} = Logger.Config.__data__
if Logger.compare_levels(level, min_level) != :lt &&
(message = translate(translators, min_level, level, kind, data, truncate)) do
message = Logger.Utils.truncate(message, truncate)
# Mode is always async to avoid clogging the error_logger
GenEvent.notify(Logger,
{level, Process.group_leader(),
{Logger, message, Logger.Utils.timestamp(utc_log?), [pid: ensure_pid(pid)]}})
end
:ok
end
defp ensure_pid(pid) when is_pid(pid), do: pid
defp ensure_pid(_), do: self()
defp check_threshold(%{last_time: last_time, last_length: last_length,
dropped: dropped, threshold: threshold} = state) do
{m, s, _} = current_time = :os.timestamp
current_length = message_queue_length()
cond do
match?({^m, ^s, _}, last_time) and current_length - last_length > threshold ->
count = drop_messages(current_time, 0)
%{state | dropped: dropped + count, last_length: message_queue_length()}
match?({^m, ^s, _}, last_time) ->
state
true ->
if dropped > 0 do
Logger.warn "Logger dropped #{dropped} OTP/SASL messages as it " <>
"exceeded the amount of #{threshold} messages/second"
end
%{state | dropped: 0, last_time: current_time, last_length: current_length}
end
end
defp message_queue_length() do
{:message_queue_len, len} = Process.info(self(), :message_queue_len)
len
end
defp drop_messages({m, s, _} = last_time, count) do
case :os.timestamp do
{^m, ^s, _} ->
receive do
{:notify, _event} -> drop_messages(last_time, count + 1)
after
0 -> count
end
_ ->
count
end
end
defp translate([{mod, fun}|t], min_level, level, kind, data, truncate) do
case apply(mod, fun, [min_level, level, kind, data]) do
{:ok, iodata} -> iodata
:skip -> nil
:none -> translate(t, min_level, level, kind, data, truncate)
end
end
defp translate([], _min_level, _level, :format, {format, args}, truncate) do
{format, args} = Logger.Utils.inspect(format, args, truncate)
:io_lib.format(format, args)
end
defp translate([], _min_level, _level, :report, {_type, data}, _truncate) do
Kernel.inspect(data)
end
end
+112
View File
@@ -0,0 +1,112 @@
import Kernel, except: [inspect: 2]
defmodule Logger.Formatter do
@moduledoc ~S"""
Conveniences for formatting data for logs.
This module allows developers to specify a string that
serves as template for log messages, for example:
$time $metadata[$level] $message\n
Will print error messages as:
18:43:12.439 user_id=13 [error] Hello\n
The valid parameters you can use are:
* `$time` - time the log message was sent
* `$date` - date the log message was sent
* `$message` - the log message
* `$level` - the log level
* `$node` - the node that prints the message
* `$metadata` - user controled data presented in "key=val key2=val2" format
Backends typically allow developers to supply such control
strings via configuration files. This module provides `compile/1`,
which compiles the string into a format for fast operations at
runtime and `format/5` to format the compiled pattern into an
actual IO data.
## Metadata
Metadata to be sent to the Logger can be read and written with
the `Logger.metadata/0` and `Logger.metadata/1` functions. For example,
you can set `Logger.metadata([user_id: 13])` to add user_id metadata
to the current process. The user can configure the backend to chose
which metadata it wants to print and it will replace the $metadata
value.
"""
@valid_patterns [:time, :date, :message, :level, :node, :metadata]
@default_pattern "$time $metadata[$level] $message\n"
@doc ~S"""
Compiles a format string into an array that the `format/5` can handle.
The valid parameters you can use are:
* $time
* $date
* $message
* $level
* $node
* $metadata - metadata is presented in key=val key2=val2 format.
If you pass nil into compile it will use the default
format of `$time $metadata [$level] $message`
If you would like to make your own custom formatter simply pass
`{module, function}` to compile and the rest is handled.
iex> Logger.Formatter.compile("$time $metadata [$level] $message\n")
[:time, " ", :metadata, " [", :level, "] ", :message, "\n"]
"""
@spec compile(binary | nil) :: list()
@spec compile({atom, atom}) :: {atom, atom}
def compile(nil), do: compile(@default_pattern)
def compile({mod, fun}) when is_atom(mod) and is_atom(fun), do: {mod, fun}
def compile(str) do
for part <- Regex.split(~r/(?<head>)\$[a-z]+(?<tail>)/, str, on: [:head, :tail], trim: true) do
case part do
"$" <> code -> compile_code(String.to_atom(code))
_ -> part
end
end
end
defp compile_code(key) when key in @valid_patterns, do: key
defp compile_code(key) when is_atom(key) do
raise(ArgumentError, message: "$#{key} is an invalid format pattern.")
end
@doc """
Takes a compiled format and injects the, level, timestamp, message and
metadata listdict and returns a properly formatted string.
"""
def format({mod, fun}, level, msg, ts, md) do
Module.function(mod, fun, 4).(level, msg, ts, md)
end
def format(config, level, msg, ts, md) do
for c <- config do
output(c, level, msg, ts, md)
end
end
defp output(:message, _, msg, _, _), do: msg
defp output(:date, _, _, {date, _time}, _), do: Logger.Utils.format_date(date)
defp output(:time, _, _, {_date, time}, _), do: Logger.Utils.format_time(time)
defp output(:level, level, _, _, _), do: Atom.to_string(level)
defp output(:node, _, _, _, _), do: Atom.to_string(node())
defp output(:metadata, _, _, _, []), do: ""
defp output(:metadata, _, _, _, meta) do
Enum.map(meta, fn {key, val} ->
[to_string(key), ?=, to_string(val), ?\s]
end)
end
defp output(other, _, _, _, _), do: other
end
+69
View File
@@ -0,0 +1,69 @@
defmodule Logger.Translator do
@moduledoc """
Default translation for Erlang log messages.
Logger allows developers to rewrite log messages provided by
Erlang applications into a format more compatible to Elixir
log messages by providing translator.
A translator is simply a tuple containing a module and a function
that can be added and removed via the `add_translator/1` and
`remove_translator/1` functions and is invoked for every Erlang
message above the minimum log level with four arguments:
* `min_level` - the current Logger level
* `level` - the level of the message being translator
* `kind` - if the message is a report or a format
* `data` - the data to format. If it is a report, it is a tuple
with `{report_type, report_data}`, if it is a format, it is a
tuple with `{format_message, format_args}`
The function must return:
* `{:ok, iodata}` - if the message was translated with its translation
* `:skip` - if the message is not meant to be translated nor logged
* `:none` - if there is no translation, which triggers the next translator
See the function `translate/4` in this module for an example implementation
and the default messages translated by Logger.
"""
def translate(min_level, :error, :format, message) do
case message do
{'** Generic server ' ++ _, [name, last, state, reason]} ->
msg = "GenServer #{inspect name} terminating\n"
if min_level == :debug do
msg = msg <> "Last message: #{inspect last}\n"
<> "State: #{inspect state}\n"
end
{:ok, msg <> "** (exit) " <> Exception.format_exit(reason)}
{'** gen_event handler ' ++ _, [name, manager, last, state, reason]} ->
msg = "GenEvent handler #{inspect name} installed in #{inspect manager} terminating\n"
if min_level == :debug do
msg = msg <> "Last message: #{inspect last}\n"
<> "State: #{inspect state}\n"
end
{:ok, msg <> "** (exit) " <> Exception.format_exit(reason)}
{'** Task ' ++ _, [name, starter, function, args, reason]} ->
msg = "Task #{inspect name} started from #{inspect starter} terminating\n" <>
"Function: #{inspect function}\n" <>
" Args: #{inspect args}\n" <>
"** (exit) " <> Exception.format_exit(reason)
{:ok, msg}
_ ->
:none
end
end
def translate(_min_level, :info, :report,
{:std_info, [application: app, exited: reason, type: _type]}) do
{:ok, "Application #{app} exited with reason #{Exception.format_exit(reason)}"}
end
def translate(_min_level, _level, _kind, _message) do
:none
end
end
+267
View File
@@ -0,0 +1,267 @@
defmodule Logger.Utils do
@moduledoc false
@doc """
Truncates a char data into n bytes.
There is a chance we truncate in the middle of a grapheme
cluster but we never truncate in the middle of a binary
codepoint. For this reason, truncation is not exact.
"""
@spec truncate(IO.chardata, non_neg_integer) :: IO.chardata
def truncate(chardata, n) when n >= 0 do
{chardata, n} = truncate_n(chardata, n)
if n >= 0, do: chardata, else: [chardata, " (truncated)"]
end
defp truncate_n(_, n) when n < 0 do
{"", n}
end
defp truncate_n(binary, n) when is_binary(binary) do
remaining = n - byte_size(binary)
if remaining < 0 do
# There is a chance we are cutting at the wrong
# place so we need to fix the binary.
{fix_binary(binary_part(binary, 0, n)), remaining}
else
{binary, remaining}
end
end
defp truncate_n(int, n) when int in 0..127, do: {int, n-1}
defp truncate_n(int, n) when int in 127..0x07FF, do: {int, n-2}
defp truncate_n(int, n) when int in 0x800..0xFFFF, do: {int, n-3}
defp truncate_n(int, n) when int >= 0x10000 and is_integer(int), do: {int, n-4}
defp truncate_n(list, n) when is_list(list) do
truncate_n_list(list, n, [])
end
defp truncate_n_list(_, n, acc) when n < 0 do
{:lists.reverse(acc), n}
end
defp truncate_n_list([h|t], n, acc) do
{h, n} = truncate_n(h, n)
truncate_n_list(t, n, [h|acc])
end
defp truncate_n_list([], n, acc) do
{:lists.reverse(acc), n}
end
defp truncate_n_list(t, n, acc) do
{t, n} = truncate_n(t, n)
{:lists.reverse(acc, t), n}
end
defp fix_binary(binary) do
# Use a thirteen-bytes offset to look back in the binary.
# This should allow at least two codepoints of 6 bytes.
suffix_size = min(byte_size(binary), 13)
prefix_size = byte_size(binary) - suffix_size
<<prefix :: binary-size(prefix_size), suffix :: binary-size(suffix_size)>> = binary
prefix <> fix_binary(suffix, "")
end
defp fix_binary(<<h::utf8, t::binary>>, acc) do
acc <> <<h::utf8>> <> fix_binary(t, "")
end
defp fix_binary(<<h, t::binary>>, acc) do
fix_binary(t, <<h, acc::binary>>)
end
defp fix_binary(<<>>, _acc) do
<<>>
end
@doc """
Receives a format string and arguments and replace `~p`,
`~P`, `~w` and `~W` by its inspected variants.
"""
def inspect(format, args, truncate, opts \\ %Inspect.Opts{})
def inspect(format, args, truncate, opts) when is_atom(format) do
do_inspect(Atom.to_char_list(format), args, truncate, opts)
end
def inspect(format, args, truncate, opts) when is_binary(format) do
do_inspect(:binary.bin_to_list(format), args, truncate, opts)
end
def inspect(format, args, truncate, opts) when is_list(format) do
do_inspect(format, args, truncate, opts)
end
defp do_inspect(format, [], _truncate, _opts), do: {format, []}
defp do_inspect(format, args, truncate, opts) do
# A pre-pass that removes binaries from
# arguments according to the truncate limit.
{args, _} = Enum.map_reduce(args, truncate, fn arg, acc ->
if is_binary(arg) do
truncate_n(arg, acc)
else
{arg, acc}
end
end)
do_inspect(format, args, [], [], opts)
end
defp do_inspect([?~|t], args, used_format, used_args, opts) do
{t, args, cc_format, cc_args} = collect_cc(:width, t, args, [?~], [], opts)
do_inspect(t, args, cc_format ++ used_format, cc_args ++ used_args, opts)
end
defp do_inspect([h|t], args, used_format, used_args, opts),
do: do_inspect(t, args, [h|used_format], used_args, opts)
defp do_inspect([], [], used_format, used_args, _opts),
do: {:lists.reverse(used_format), :lists.reverse(used_args)}
## width
defp collect_cc(:width, [?-|t], args, used_format, used_args, opts),
do: collect_value(:width, t, args, [?-|used_format], used_args, opts, :precision)
defp collect_cc(:width, t, args, used_format, used_args, opts),
do: collect_value(:width, t, args, used_format, used_args, opts, :precision)
## precision
defp collect_cc(:precision, [?.|t], args, used_format, used_args, opts),
do: collect_value(:precision, t, args, [?.|used_format], used_args, opts, :pad_char)
defp collect_cc(:precision, t, args, used_format, used_args, opts),
do: collect_cc(:pad_char, t, args, used_format, used_args, opts)
## pad char
defp collect_cc(:pad_char, [?.,?*|t], [arg|args], used_format, used_args, opts),
do: collect_cc(:encoding, t, args, [?*,?.|used_format], [arg|used_args], opts)
defp collect_cc(:pad_char, [?.,p|t], args, used_format, used_args, opts),
do: collect_cc(:encoding, t, args, [p,?.|used_format], used_args, opts)
defp collect_cc(:pad_char, t, args, used_format, used_args, opts),
do: collect_cc(:encoding, t, args, used_format, used_args, opts)
## encoding
defp collect_cc(:encoding, [?l|t], args, used_format, used_args, opts),
do: collect_cc(:done, t, args, [?l|used_format], used_args, %{opts | char_lists: false})
defp collect_cc(:encoding, [?t|t], args, used_format, used_args, opts),
do: collect_cc(:done, t, args, [?t|used_format], used_args, opts)
defp collect_cc(:encoding, t, args, used_format, used_args, opts),
do: collect_cc(:done, t, args, used_format, used_args, opts)
## done
defp collect_cc(:done, [?W|t], [data, limit|args], _used_format, _used_args, opts),
do: collect_inspect(t, args, data, %{opts | limit: limit, width: :infinity})
defp collect_cc(:done, [?w|t], [data|args], _used_format, _used_args, opts),
do: collect_inspect(t, args, data, %{opts | width: :infinity})
defp collect_cc(:done, [?P|t], [data, limit|args], _used_format, _used_args, opts),
do: collect_inspect(t, args, data, %{opts | limit: limit})
defp collect_cc(:done, [?p|t], [data|args], _used_format, _used_args, opts),
do: collect_inspect(t, args, data, opts)
defp collect_cc(:done, [h|t], args, used_format, used_args, _opts) do
{args, used_args} = collect_cc(h, args, used_args)
{t, args, [h|used_format], used_args}
end
defp collect_cc(?x, [a,prefix|args], used), do: {args, [prefix, a|used]}
defp collect_cc(?X, [a,prefix|args], used), do: {args, [prefix, a|used]}
defp collect_cc(?s, [a|args], used), do: {args, [a|used]}
defp collect_cc(?e, [a|args], used), do: {args, [a|used]}
defp collect_cc(?f, [a|args], used), do: {args, [a|used]}
defp collect_cc(?g, [a|args], used), do: {args, [a|used]}
defp collect_cc(?b, [a|args], used), do: {args, [a|used]}
defp collect_cc(?B, [a|args], used), do: {args, [a|used]}
defp collect_cc(?+, [a|args], used), do: {args, [a|used]}
defp collect_cc(?#, [a|args], used), do: {args, [a|used]}
defp collect_cc(?c, [a|args], used), do: {args, [a|used]}
defp collect_cc(?i, [a|args], used), do: {args, [a|used]}
defp collect_cc(?~, args, used), do: {args, used}
defp collect_cc(?n, args, used), do: {args, used}
defp collect_inspect(t, args, data, opts) do
data =
data
|> Inspect.Algebra.to_doc(opts)
|> Inspect.Algebra.format(opts.width)
{t, args, 'st~', [data]}
end
defp collect_value(current, [?*|t], [arg|args], used_format, used_args, opts, next)
when is_integer(arg) do
collect_cc(next, t, args, [?*|used_format], [arg|used_args],
put_value(opts, current, arg))
end
defp collect_value(current, [c|t], args, used_format, used_args, opts, next)
when is_integer(c) and c >= ?0 and c <= ?9 do
{t, c} = collect_value([c|t], [])
collect_cc(next, t, args, c ++ used_format, used_args,
put_value(opts, current, c |> :lists.reverse |> List.to_integer))
end
defp collect_value(_current, t, args, used_format, used_args, opts, next),
do: collect_cc(next, t, args, used_format, used_args, opts)
defp collect_value([c|t], buffer)
when is_integer(c) and c >= ?0 and c <= ?9,
do: collect_value(t, [c|buffer])
defp collect_value(other, buffer),
do: {other, buffer}
defp put_value(opts, key, value) do
if Map.has_key?(opts, key) do
Map.put(opts, key, value)
else
opts
end
end
@doc """
Returns a timestamp that includes miliseconds.
"""
def timestamp(utc_log?) do
{_, _, micro} = now = :os.timestamp()
{date, {hours, minutes, seconds}} =
case utc_log? do
true -> :calendar.now_to_universal_time(now)
false -> :calendar.now_to_local_time(now)
end
{date, {hours, minutes, seconds, div(micro, 1000)}}
end
@doc """
Formats time to an iodata.
"""
def format_time({hh, mi, ss, ms}) do
[pad2(hh), ?:, pad2(mi), ?:, pad2(ss), ?., pad3(ms)]
end
@doc """
Formats date to an iodata.
"""
def format_date({yy, mm, dd}) do
[Integer.to_string(yy), ?-, pad2(mm), ?-, pad2(dd)]
end
defp pad3(int) when int < 100 and int > 10, do: [?0, Integer.to_string(int)]
defp pad3(int) when int < 10, do: [?0, ?0, Integer.to_string(int)]
defp pad3(int), do: Integer.to_string(int)
defp pad2(int) when int < 10, do: [?0, Integer.to_string(int)]
defp pad2(int), do: Integer.to_string(int)
end
+91
View File
@@ -0,0 +1,91 @@
defmodule Logger.Watcher do
@moduledoc false
require Logger
use GenServer
@name Logger.Watcher
@doc """
Starts the watcher supervisor.
"""
def start_link(handlers) do
options = [strategy: :one_for_one, name: @name]
case Supervisor.start_link([], options) do
{:ok, _} = ok ->
_ = for {mod, handler, args} <- handlers do
{:ok, _} = watch(mod, handler, args)
end
ok
{:error, _} = error ->
error
end
end
@doc """
Removes the given handler.
"""
def unwatch(mod, handler) do
case Supervisor.terminate_child(@name, {mod, handler}) do
:ok -> Supervisor.delete_child(@name, {mod, handler})
res -> res
end
end
@doc """
Watches the given handler as part of the handler supervision tree.
"""
def watch(mod, handler, args) do
import Supervisor.Spec
id = {mod, handler}
child = worker(__MODULE__, [mod, handler, args],
[id: id, function: :watcher, restart: :transient])
case Supervisor.start_child(@name, child) do
{:ok, _pid} = result ->
result
{:error, :already_present} ->
_ = Supervisor.delete_child(@name, id)
watch(mod, handler, args)
{:error, _reason} = error ->
error
end
end
@doc """
Starts a watcher server.
This is useful when there is a need to start a handler
outside of the handler supervision tree.
"""
def watcher(mod, handler, args) do
GenServer.start_link(__MODULE__, {mod, handler, args})
end
## Callbacks
def init({mod, handler, args}) do
case :gen_event.add_sup_handler(mod, handler, args) do
:ok -> {:ok, {mod, handler}}
{:error, :ignore} -> :ignore
{:error, reason} -> {:stop, reason}
{:EXIT, reason} -> {:stop, reason}
end
end
def handle_info({:gen_event_EXIT, handler, reason}, {_, handler} = state)
when reason in [:normal, :shutdown] do
{:stop, reason, state}
end
def handle_info({:gen_event_EXIT, handler, reason}, {mod, handler} = state) do
Logger.error "GenEvent handler #{inspect handler} installed at #{inspect mod}\n" <>
"** (exit) #{format_exit(reason)}"
{:stop, reason, state}
end
def handle_info(_msg, state) do
{:noreply, state}
end
defp format_exit({:EXIT, reason}), do: Exception.format_exit(reason)
defp format_exit(other), do: inspect(other)
end
+24
View File
@@ -0,0 +1,24 @@
defmodule Logger.Mixfile do
use Mix.Project
def project do
[app: :logger,
version: System.version,
build_per_environment: false]
end
def application do
[registered: [Logger, Logger.Supervisor, Logger.Watcher],
mod: {Logger, []},
env: [level: :debug,
utc_log: false,
truncate: 8096,
backends: [:console],
translators: [{Logger.Translator, :translate}],
sync_threshold: 20,
handle_otp_reports: true,
compile_time_purge_level: :debug,
discard_threshold_for_error_logger: 500,
console: []]]
end
end
@@ -0,0 +1,52 @@
defmodule Logger.Backends.ConsoleTest do
use Logger.Case
require Logger
setup do
on_exit fn ->
:ok = Logger.configure_backend(:console, [format: nil, level: nil, metadata: []])
end
end
test "does not start when there is no user" do
user = Process.whereis(:user)
try do
Process.unregister(:user)
assert GenEvent.add_handler(Logger, Logger.Backends.Console, []) ==
{:error, :ignore}
after
Process.register(user, :user)
end
end
test "can configure format" do
Logger.configure_backend(:console, format: "$message [$level]")
assert capture_log(fn ->
Logger.debug("hello")
end) =~ "hello [debug]"
end
test "can configure metadata" do
Logger.configure_backend(:console, format: "$metadata$message", metadata: [:user_id])
assert capture_log(fn ->
Logger.debug("hello")
end) =~ "hello"
Logger.metadata(user_id: 13)
assert capture_log(fn ->
Logger.debug("user_id=13 hello")
end) =~ "hello"
end
test "can configure level" do
Logger.configure_backend(:console, level: :info)
assert capture_log(fn ->
Logger.debug("hello")
end) == ""
end
end
@@ -0,0 +1,59 @@
defmodule Logger.ErrorHandlerTest do
use Logger.Case
test "survives after crashes" do
assert error_log(:info_msg, "~p~n", []) == ""
assert capture_log(fn ->
wait_for_handler(:error_logger, Logger.ErrorHandler)
end) =~ "[error] GenEvent handler Logger.ErrorHandler installed at :error_logger\n" <>
"** (exit) an exception was raised:"
assert error_log(:info_msg, "~p~n", [:hello]) =~ msg("[info] :hello\n")
end
test "formats error_logger info message" do
assert error_log(:info_msg, "hello", []) =~ msg("[info] hello")
assert error_log(:info_msg, "~p~n", [:hello]) =~ msg("[info] :hello\n")
end
test "formats error_logger info report" do
assert error_log(:info_report, "hello") =~ msg("[info] \"hello\"")
assert error_log(:info_report, :hello) =~ msg("[info] :hello\n")
assert error_log(:info_report, :special, :hello) == ""
end
test "formats error_logger error message" do
assert error_log(:error_msg, "hello", []) =~ msg("[error] hello")
assert error_log(:error_msg, "~p~n", [:hello]) =~ msg("[error] :hello\n")
end
test "formats error_logger error report" do
assert error_log(:error_report, "hello") =~ msg("[error] \"hello\"")
assert error_log(:error_report, :hello) =~ msg("[error] :hello\n")
assert error_log(:error_report, :special, :hello) == ""
end
test "formats error_logger warning message" do
# Warnings by default are logged as errors by Erlang
assert error_log(:warning_msg, "hello", []) =~ msg("[error] hello")
assert error_log(:warning_msg, "~p~n", [:hello]) =~ msg("[error] :hello\n")
end
test "formats error_logger warning report" do
# Warnings by default are logged as errors by Erlang
assert error_log(:warning_report, "hello") =~ msg("[error] \"hello\"")
assert error_log(:warning_report, :hello) =~ msg("[error] :hello\n")
assert error_log(:warning_report, :special, :hello) == ""
end
defp error_log(fun, format) do
do_error_log(fun, [format])
end
defp error_log(fun, format, args) do
do_error_log(fun, [format, args])
end
defp do_error_log(fun, args) do
capture_log(fn -> apply(:error_logger, fun, args) end)
end
end
+54
View File
@@ -0,0 +1,54 @@
defmodule Logger.FormatterTest do
use Logger.Case, async: true
doctest Logger.Formatter
import Logger.Formatter
defmodule CompileMod do
def format(_level, _msg, _ts, _md) do
true
end
end
test "compile/1 with nil" do
assert compile(nil) ==
[:time, " ", :metadata, "[", :level, "] ", :message, "\n"]
end
test "compile/1 with str" do
assert compile("$level $time $date $metadata $message $node") ==
Enum.intersperse([:level, :time, :date, :metadata, :message, :node], " ")
assert_raise ArgumentError,"$bad is an invalid format pattern.", fn ->
compile("$bad $good")
end
end
test "compile/1 with {mod, fun}" do
assert compile({CompileMod, :format}) == {CompileMod, :format}
end
test "format with {mod, fun}" do
assert format({CompileMod, :format}, nil, nil, nil,nil) == true
end
test "format with format string" do
compiled = compile("[$level] $message")
assert format(compiled, :error, "hello", nil, []) ==
["[", "error", "] ", "hello"]
compiled = compile("$node")
assert format(compiled, :error, nil, nil, []) == [Atom.to_string(node())]
compiled = compile("$metadata")
assert IO.iodata_to_binary(format(compiled, :error, nil, nil, [meta: :data])) ==
"meta=data "
assert IO.iodata_to_binary(format(compiled, :error, nil, nil, [])) ==
""
timestamp = {{2014, 12, 30}, {12, 6, 30, 100}}
compiled = compile("$date $time")
assert IO.iodata_to_binary(format(compiled, :error, nil, timestamp, [])) ==
"2014-12-30 12:06:30.100"
end
end
+102
View File
@@ -0,0 +1,102 @@
defmodule Logger.TranslatorTest do
use Logger.Case
defmodule MyGenServer do
use GenServer
def handle_call(:error, _, _) do
raise "oops"
end
end
defmodule MyGenEvent do
use GenEvent
def handle_call(:error, _) do
raise "oops"
end
end
test "translates GenServer crashes" do
{:ok, pid} = GenServer.start(MyGenServer, :ok)
assert capture_log(:info, fn ->
catch_exit(GenServer.call(pid, :error))
end) =~ """
[error] GenServer #{inspect pid} terminating
** (exit) an exception was raised:
** (RuntimeError) oops
"""
end
test "translates GenServer crashes on debug" do
{:ok, pid} = GenServer.start(MyGenServer, :ok)
assert capture_log(:debug, fn ->
catch_exit(GenServer.call(pid, :error))
end) =~ """
[error] GenServer #{inspect pid} terminating
Last message: :error
State: :ok
** (exit) an exception was raised:
** (RuntimeError) oops
"""
end
test "translates GenEvent crashes" do
{:ok, pid} = GenEvent.start()
:ok = GenEvent.add_handler(pid, MyGenEvent, :ok)
assert capture_log(:info, fn ->
GenEvent.call(pid, MyGenEvent, :error)
end) =~ """
[error] GenEvent handler Logger.TranslatorTest.MyGenEvent installed in #{inspect pid} terminating
** (exit) an exception was raised:
** (RuntimeError) oops
"""
end
test "translates GenEvent crashes on debug" do
{:ok, pid} = GenEvent.start()
:ok = GenEvent.add_handler(pid, MyGenEvent, :ok)
assert capture_log(:debug, fn ->
GenEvent.call(pid, MyGenEvent, :error)
end) =~ """
[error] GenEvent handler Logger.TranslatorTest.MyGenEvent installed in #{inspect pid} terminating
Last message: :error
State: :ok
** (exit) an exception was raised:
** (RuntimeError) oops
"""
end
test "translates Task crashes" do
{:ok, pid} = Task.start_link(__MODULE__, :task, [self()])
assert capture_log(fn ->
ref = Process.monitor(pid)
send(pid, :go)
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
end) =~ """
[error] Task #{inspect pid} started from #{inspect self} terminating
Function: &Logger.TranslatorTest.task/1
Args: [#{inspect self}]
** (exit) an exception was raised:
** (RuntimeError) oops
"""
end
test "translates application stop" do
:ok = Application.start(:eex)
assert capture_log(fn ->
Application.stop(:eex)
end) =~ msg("[info] Application eex exited with reason :stopped")
end
def task(parent) do
Process.unlink(parent)
receive do: (:go -> raise "oops")
end
end
+96
View File
@@ -0,0 +1,96 @@
defmodule Logger.UtilsTest do
use Logger.Case, async: true
import Logger.Utils
import Kernel, except: [inspect: 2]
defp inspect(format, args), do: Logger.Utils.inspect(format, args, 10)
test "truncate/2" do
# ASCII binaries
assert truncate("foo", 4) == "foo"
assert truncate("foo", 3) == "foo"
assert truncate("foo", 2) == ["fo", " (truncated)"]
# UTF-8 binaries
assert truncate("olá", 2) == ["ol", " (truncated)"]
assert truncate("olá", 3) == ["ol", " (truncated)"]
assert truncate("olá", 4) == "olá"
assert truncate("ááááá:", 10) == ["ááááá", " (truncated)"]
assert truncate("áááááá:", 10) == ["ááááá", " (truncated)"]
# Charlists
assert truncate('olá', 2) == ['olá', " (truncated)"]
assert truncate('olá', 3) == ['olá', " (truncated)"]
assert truncate('olá', 4) == 'olá'
# Chardata
assert truncate('ol' ++ "á", 2) == ['ol' ++ "", " (truncated)"]
assert truncate('ol' ++ "á", 3) == ['ol' ++ "", " (truncated)"]
assert truncate('ol' ++ "á", 4) == 'ol' ++ "á"
end
test "inspect/2 formats" do
assert inspect('~p', [1]) == {'~ts', [["1"]]}
assert inspect("~p", [1]) == {'~ts', [["1"]]}
assert inspect(:"~p", [1]) == {'~ts', [["1"]]}
end
test "inspect/2 sigils" do
assert inspect('~10.10tp', [1]) == {'~ts', [["1"]]}
assert inspect('~-10.10tp', [1]) == {'~ts', [["1"]]}
assert inspect('~10.10lp', [1]) == {'~ts', [["1"]]}
assert inspect('~10.10x~p~n', [1, 2, 3]) == {'~10.10x~ts~n', [1, 2, ["3"]]}
end
test "inspect/2 with modifier t has no effect (as it is the default)" do
assert inspect('~tp', [1]) == {'~ts', [["1"]]}
assert inspect('~tw', [1]) == {'~ts', [["1"]]}
end
test "inspect/2 with modifier l always prints lists" do
assert inspect('~lp', ['abc']) ==
{'~ts', [["[", "97", ",", " ", "98", ",", " ", "99", "]"]]}
assert inspect('~lw', ['abc']) ==
{'~ts', [["[", "97", ",", " ", "98", ",", " ", "99", "]"]]}
end
test "inspect/2 with modifier for width" do
assert inspect('~5lp', ['abc']) ==
{'~ts', [["[", "97", ",", "\n ", "98", ",", "\n ", "99", "]"]]}
assert inspect('~5lw', ['abc']) ==
{'~ts', [["[", "97", ",", " ", "98", ",", " ", "99", "]"]]}
end
test "inspect/2 with modifier for limit" do
assert inspect('~5lP', ['abc', 2]) ==
{'~ts', [["[", "97", ",", "\n ", "98", ",", "\n ", "...", "]"]]}
assert inspect('~5lW', ['abc', 2]) ==
{'~ts', [["[", "97", ",", " ", "98", ",", " ", "...", "]"]]}
end
test "inspect/2 truncates binaries" do
assert inspect('~ts', ["abcdeabcdeabcdeabcde"]) ==
{'~ts', ["abcdeabcde"]}
assert inspect('~ts~ts~ts', ["abcdeabcde", "abcde", "abcde"]) ==
{'~ts~ts~ts', ["abcdeabcde", "", ""]}
end
test "timestamp" do
assert {{_, _, _}, {_, _, _, _}} = timestamp(true)
end
test "format_date" do
date = {2015, 1, 30}
assert format_date(date) == ["2015", ?-, [?0, "1"], ?-, "30"]
end
test "format_time" do
time = {12, 30, 10, 1}
assert format_time(time) == ["12", ?:, "30", ?:, "10", ?., [?0, ?0, "1"]]
end
end
+175
View File
@@ -0,0 +1,175 @@
defmodule LoggerTest do
use Logger.Case
require Logger
test "add_translator/1 and remove_translator/1" do
defmodule CustomTranslator do
def t(:debug, :info, :format, {'hello: ~p', [:ok]}) do
:skip
end
def t(:debug, :info, :format, {'world: ~p', [:ok]}) do
{:ok, "rewritten"}
end
def t(_, _, _, _) do
:none
end
end
assert Logger.add_translator({CustomTranslator, :t})
assert capture_log(fn ->
:error_logger.info_msg('hello: ~p', [:ok])
end) == ""
assert capture_log(fn ->
:error_logger.info_msg('world: ~p', [:ok])
end) =~ "\[info\] rewritten"
after
assert Logger.remove_translator({CustomTranslator, :t})
end
test "add_backend/1 and remove_backend/1" do
assert :ok = Logger.remove_backend(:console)
assert Logger.remove_backend(:console) ==
{:error, :not_found}
assert capture_log(fn ->
assert Logger.debug("hello", []) == :ok
end) == ""
assert {:ok, pid} = Logger.add_backend(:console)
assert Logger.add_backend(:console) ==
{:error, {:already_started, pid}}
end
test "level/0" do
assert Logger.level == :debug
end
test "compare_levels/2" do
assert Logger.compare_levels(:debug, :debug) == :eq
assert Logger.compare_levels(:debug, :info) == :lt
assert Logger.compare_levels(:debug, :warn) == :lt
assert Logger.compare_levels(:debug, :error) == :lt
assert Logger.compare_levels(:info, :debug) == :gt
assert Logger.compare_levels(:info, :info) == :eq
assert Logger.compare_levels(:info, :warn) == :lt
assert Logger.compare_levels(:info, :error) == :lt
assert Logger.compare_levels(:warn, :debug) == :gt
assert Logger.compare_levels(:warn, :info) == :gt
assert Logger.compare_levels(:warn, :warn) == :eq
assert Logger.compare_levels(:warn, :error) == :lt
assert Logger.compare_levels(:error, :debug) == :gt
assert Logger.compare_levels(:error, :info) == :gt
assert Logger.compare_levels(:error, :warn) == :gt
assert Logger.compare_levels(:error, :error) == :eq
end
test "debug/2" do
assert capture_log(fn ->
assert Logger.debug("hello", []) == :ok
end) =~ msg("[debug] hello")
assert capture_log(:info, fn ->
assert Logger.debug("hello", []) == :ok
end) == ""
end
test "info/2" do
assert capture_log(fn ->
assert Logger.info("hello", []) == :ok
end) =~ msg("[info] hello")
assert capture_log(:warn, fn ->
assert Logger.info("hello", []) == :ok
end) == ""
end
test "warn/2" do
assert capture_log(fn ->
assert Logger.warn("hello", []) == :ok
end) =~ msg("[warn] hello")
assert capture_log(:error, fn ->
assert Logger.warn("hello", []) == :ok
end) == ""
end
test "error/2" do
assert capture_log(fn ->
assert Logger.error("hello", []) == :ok
end) =~ msg("[error] hello")
end
test "remove unused calls at compile time" do
Logger.configure(compile_time_purge_level: :info)
defmodule Sample do
def debug do
Logger.debug "hello"
end
def info do
Logger.info "hello"
end
end
assert capture_log(fn ->
assert Sample.debug == :ok
end) == ""
assert capture_log(fn ->
assert Sample.info == :ok
end) =~ msg("[info] hello")
after
Logger.configure(compile_time_purge_level: :debug)
end
test "log/2 truncates messages" do
Logger.configure(truncate: 4)
assert capture_log(fn ->
Logger.log(:debug, "hello")
end) =~ "hell (truncated)"
after
Logger.configure(truncate: 8096)
end
test "log/2 fails when the application is off" do
logger = Process.whereis(Logger)
Process.unregister(Logger)
try do
assert_raise RuntimeError,
"Cannot log messages, the :logger application is not running", fn ->
Logger.log(:debug, "hello")
end
after
Process.register(logger, Logger)
end
end
test "Logger.Config survives Logger exit" do
Process.whereis(Logger)
|> Process.exit(:kill)
wait_for_logger()
wait_for_handler(Logger, Logger.Config)
end
test "Logger.Config can restart the application" do
Application.put_env(:logger, :backends, [])
Logger.Config.restart()
assert capture_log(fn ->
assert Logger.debug("hello", []) == :ok
end) == ""
assert {:ok, pid} = Logger.add_backend(:console)
assert Logger.add_backend(:console) ==
{:error, {:already_started, pid}}
end
end
+47
View File
@@ -0,0 +1,47 @@
ExUnit.start seed: 362385, trace: true
defmodule Logger.Case do
use ExUnit.CaseTemplate
import ExUnit.CaptureIO
using _ do
quote do
import Logger.Case
end
end
def msg(msg) do
~r/^\d\d\:\d\d\:\d\d\.\d\d\d #{Regex.escape(msg)}$/
end
def wait_for_handler(manager, handler) do
unless handler in GenEvent.which_handlers(manager) do
:timer.sleep(10)
wait_for_handler(manager, handler)
end
end
def wait_for_logger() do
try do
GenEvent.which_handlers(Logger)
else
_ ->
:ok
catch
:exit, _ ->
:timer.sleep(10)
wait_for_logger()
end
end
def capture_log(level \\ :debug, fun) do
Logger.configure(level: level)
capture_io(:user, fn ->
fun.()
GenEvent.which_handlers(:error_logger)
GenEvent.which_handlers(Logger)
end)
after
Logger.configure(level: :debug)
end
end
+7 -7
View File
@@ -98,11 +98,11 @@ defmodule Mix.Tasks.New do
end
defp otp_app(_mod, false) do
" [applications: []]"
" [applications: [:logger]]"
end
defp otp_app(mod, true) do
" [applications: [],\n mod: {#{mod}, []}]"
" [applications: [:logger],\n mod: {#{mod}, []}]"
end
defp do_generate_umbrella(app, path, _opts) do
@@ -275,14 +275,14 @@ defmodule Mix.Tasks.New do
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application, it should
# be done in your mix.exs file.
# if you want to provide default values for your application for third-
# party users, it should be done in your mix.exs file.
# Sample configuration:
#
# config :my_dep,
# key: :value,
# limit: 42
# config :logger,
# level: :info,
# format: "$time $metadata[$level] $message\n"
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment