Current section

Files

Jump to
logbook lib logbook.ex
Raw

lib/logbook.ex

defmodule Logbook do
@moduledoc ~S"""
A category (or tags) based logger for Elixir.
Logbook is a wrapper aroud Elixir Logger that enables a to specify one or more tags
for each invocation in order to be able to set different log levels for each tag.
Tagging logs is useful to track log informations around different modules and
enable only one (or more) specific tags at different log levels than the default
Logger instead of having (for example) all debug logs enabled.
In the following example when calling `delete_all` and having the `:audit` tag level
set to at least `:info`, both "Deleting user..." and "Deleting domain" logs will be produced.
If only `:domain` or `:user` tags have log level set to `:info` only the corresponding logs
will be produced.
## Example
require Logbook
def delete_user(user) do
Logbook.info([:audit, :user], "Deleting user #{inspect(user)}")
# ...
end
def delete_domain(domain) do
Logbook.info([:audit, :domain], "Deleting domain #{inspect(domain)}")
# ...
end
def delete_all(user, domain) do
delete_domain(domain)
delete_user(user)
end
Log levels for each tag can be set using `Logbook.set_level/2`:
# For a single tag
Logbook.set_level(:audit, :info)
# or for many tags at once
Logbook.set_level([:audit, :user, :domain], :info)
Is possible to set the default level for all tags, by setting the `:default_tag_level`
config option for `:logbook` app (defaults to `:warning`):
import Config
config :logbook, :default_tag_level, :warning
The `:default_tag_level` option is used when Logbook sees tags for the first time
during runtime and set them internally with the above level.
As a bonus, Logbook also creates a module-level tag automatically, in order to
be able to enable log statements at once in a single module:
defmodule Foo
require Logbook
def delete_user(user) do
Logbook.info([:audit, :user], "Deleting user #{inspect(user)}")
# ...
end
def delete_domain(domain) do
Logbook.info([:audit, :domain], "Deleting domain #{inspect(domain)}")
# ...
end
end
With the above example is possible to `Logbook.set_level(Foo, :info)` to enable
all Logbook calls inside the module `Foo`.
As with `:default_tag_level` is possible to set also default module-level logging
with:
import Config
config :logbook, :default_module_level, :warning
By default `:default_module_level` is set to `:none` (no module-level logging).
`Logbook` supports all `Logger` levels, along with the additional `:none` level
that disables it for the specified tag/module.
Being a wrapper for `Logger`, if the `Logger` log level is less that `Logbook`
log level, the logs will not be produced, because are filtered by `Logger` log levels.
Example:
Logger.configure(level: :warning)
Logbook.set_level(:foo, :debug)
Logbook.debug(:foo, "This will not be emitted")
"""
alias Logbook.LogTags
@logger_levels [:emergency, :alert, :critical, :error, :warning, :notice, :info, :debug]
@logbook_levels @logger_levels ++ [:none]
@type level ::
:emergency
| :alert
| :critical
| :error
| :warning
| :notice
| :info
| :debug
| :none
@type tag_or_tags :: atom | [atom]
@doc """
Sets log level for the specific tag or list of tags.
"""
def set_level(tag, level) when is_atom(tag) and level in @logbook_levels do
LogTags.set_level([tag], level)
end
def set_level(tags, level) when is_list(tags) and level in @logbook_levels do
LogTags.set_level(tags, level)
end
@doc """
Checks wheter the tag has the specified log level equal o higher than the configured one.
iex> Logbook.set_level(:foo, :info)
:ok
iex> Logbook.enabled?(:foo, :debug)
false
iex> Logbook.enabled?(:foo, :warning)
true
If a list of tags is passed, returns `true` if any of the tag log level is equal or lower than
the passed one.
"""
def enabled?(tag_or_tags, level) when level in @logbook_levels do
LogTags.enabled?(tag_or_tags, level)
end
@doc """
Like `enabled?/2` checks if the given module has a configured log level equal
or lower than the given level.
"""
def module_enabled?(module, level) when is_atom(module) and level in @logbook_levels do
LogTags.module_enabled?(module, level)
end
@spec reset() :: :ok
def reset do
LogTags.reset()
end
@doc """
Returns a map containing the tags/modules seen at runtime with the corresponding
configured log level. This list is built at runtime, so if a `Logbook` loggin fun
has never be called, the corresponding tag will not be shown here.
"""
@spec tags() :: map()
def tags do
LogTags.tags()
end
for level <- @logger_levels do
@doc since: "2.0.0"
defmacro unquote(level)(tag_or_tags, chardata_or_fun, metadata \\ []) do
do_log(unquote(level), tag_or_tags, chardata_or_fun, metadata, __CALLER__)
end
defp macro_logger(unquote(level)) do
level = unquote(level)
quote do
require Logger
&(unquote(Logger).unquote(level) / unquote(2))
end
end
end
defp do_log(level, tag_or_tags, chardata_or_fun, metadata, caller) do
logger = macro_logger(level)
{module, tags, tag_or_tags} = macro_preprocess(tag_or_tags, caller)
quote do
level = unquote(level)
logger = unquote(logger)
# enrich metadata
md = Keyword.put(unquote(metadata), :tags, %Logbook.Tags{tags: unquote(tags)})
should_log =
Logbook.enabled?(unquote(tag_or_tags), level) ||
Logbook.module_enabled?(unquote(module), level)
case should_log do
false ->
:ok
true ->
logger.(unquote(chardata_or_fun), md)
end
end
end
defp macro_preprocess(tag, caller) when is_atom(tag) do
%{module: module, function: _fun, file: _file, line: _line} = caller
{module, [tag], tag}
end
defp macro_preprocess(tags, caller) when is_list(tags) do
%{module: module, function: _fun, file: _file, line: _line} = caller
Enum.each(tags, fn tag when is_atom(tag) -> tag end)
{module, tags, tags}
end
defp macro_preprocess({:@, _, _} = module_attr, caller) do
module_attr
|> Macro.expand_once(caller)
|> macro_preprocess(caller)
end
end