# Log
A `Logger` backend and frontend with enhanced filtering capabilities,
flexible configuration and utility macros.
## Usage
The package can be installed by adding `log` to your list of dependencies
in `mix.exs`:
```elixir
def deps do
[
{:log, ">= 0.3"}
]
end
```
Configure the `Logger` to use `Log.Backend` instead of `:console`
```elixir
config :logger,
backends: [Log.Backend]
```
To use the backend it's sufficient to use `Logger`:
```elixir
require Logger
Logger.info("This is a message", tags: [:a_message])
```
`Log` provides an advanced frontend to `Logger` which adds new levels,
output filtering and other features that can be useful during development
and in production.
To access these features is necessary to use the module `Log` instead of
`Logger`:
```elixir
require Log
Log.info("This is a message", tags: [:a_message])
```
More advanced functionalities are explained in
[Advanced Usage](#advanced-usage) section.
### Advanced Usage
- Tagging
- `do` syntax
- `inspect`
- `@log_tags` attribute
- Custom Logger with pre-defined tags
- Available Log Levels
- `Log.Args`
#### Tagging
The core feature of `Log` is to be able to tag messages and later on filter
messages not including some tags.
```elixir
Log.info("message 1", tags: [:tag1, :tag2])
Log.info("message 2", tag: :tag1)
```
With the environment variable `LOG_TAGS` it's possible to filter some
messages:
- `LOG_TAGS=tag2,tag1` displays both messages
- `LOG_TAGS=-tag2,tag1` displays only message 2
- `LOG_TAGS=-tag2` displays only message 2 (any message without tag2)
- `LOG_TAGS=-tag1` displays no messages
- `LOG_TAGS=tag3` displays no messages
##### Syntax
Example:
```
LOG_TAGS=tag1,tag2,tag3,tag4
```
Will allow output of any message with any (minimum 1) of the following tags:
- `tag1`
- `tag2`
- `tag3`
- `tag4`
The syntax for `LOG_TAGS` is the following:
- `,` (comma) is used as a separator for tags
- Empty strings are ignored, so `LOG_TAGS=tag1,,,,tag2` is valid and it will
filter any message without `tag1` or `tag2`
###### Special Tags
- `_untagged` includes any message without tags
- `_all` includes all messages, tagged and untagged. Must be used alone
###### Modifiers
A tag can be prefixed with `+` and `-` modifiers.
When a tag has `-` modifier, it must be **absent** from the message tags.
When a tag has `+` modifier, it must be **present** from the message tags.
```
LOG_TAGS=tag1,tag2,+tag3
```
A message is output only if it has:
- tag1 and tag3
- tag2 and tag3
- tag1, tag2 and tag3
#### Do Syntax
In addition to passing a string or a function as log message, to lazy
evaluate the content, it's also possible to use the `do` syntax to achieve
the same lazy-evaluation:
```elixir
require Log
Log.info(tags: [:a_tag, :another_tag]) do
"This message #{interpolates} some variables"
end
```
#### Inspect
`IO.inspect` is a widely used function. `Log` provides a custom logger
named `Log.Inspect` which behaves like
[IO.inspect/2](https://hexdocs.pm/elixir/IO.html#inspect/2),
while preserving the ability to filter log messages:
```elixir
a = 1
b = Log.Inspect.info(a) + 2
# b is now 3
```
Like `inspect`, the return value of `Log.Inspect` is the data structure
passed. Log output is:
```
[2000-01-01T01:01:01.001Z] INFO:
1
```
The same options available for `IO.inspect` are available, such as `:label`:
```elixir
Log.Inspect.info(%{some: "data"}, label: "a message")
```
Which outputs:
```
[2000-01-01T01:01:01.001Z] INFO: a message
%{some: "data"}
```
Notice that differently from `IO.inspect`, `Log.Inspect` defaults `pretty` to
`true`.
The last feature of `Log.Inspect` is that it tags all the messages with the
tag `:inspect`, so removing inspect messages can be easily performed by
setting the environment variable `LOG_TAGS` to include `-inspect`.
#### Log Tags Attribute
When the attribute `@log_tags` is defined, any `Log` message will include
the tags specified in the attribute
```elixir
defmodule MyModule do
require Log
@log_tags [:tag1, :tag2]
def hello do
Log.info("message 1")
Log.info("message 2")
end
@log_tags [:tag3]
def world do
Log.info("message 3")
end
def run do
hello()
world()
end
end
MyModule.run()
```
Message 1 and 2 will be both tagged with `:tag1` and `:tag2`, while
message 3 will be tagged only with `:tag3`.
#### Custom Logger With Pre-Defined Tags
It's possible to create a Logger with some tags always added to every message
where the module is used:
```elixir
defmodule MyLog do
use Log, tags: [:tag1, :tag2]
end
```
```elixir
defmodule MyModule do
require Log
@log_tags [:tag3]
def run do
Log.info("message")
end
end
MyModule.run()
```
"message" will have tags: `:tag1`, `:tag2` and `:tag3`
#### Available Log Levels
If using `Log` frontend module instead of `Logger` directly, the following
levels are available:
- `trace`
- `debug`
- `info`
- `warn`
- `error`
- `fatal`
Otherwise the log levels are limited to the `Logger` levels:
- `debug`
- `info`
- `warn`
- `error`
#### Log.Args
A common scenario is logging the entry point of a function, in such cases,
displaying the arguments of the function is important. The module
`Log.Args` helps by formatting variables in a readable way:
```elixir
require Log.Args
Log.Args.info({"a message", %{some_id: 123, some_name: "Jon"}})
```
Will output:
```
[2000-01-01T01:01:01.001Z] INFO: a message (SomeId: 123, SomeName: Jon)
```
If `String` keys are used, no transformation to pascal case is performed.
It's possible to use a keyword instead of a map.
## Configuration
Replace `Logger` `:console` backend with `Log.Backend` in your configuration.
### Output Filtering Configuration
The environment variables are an interface to filter log output dinamically.
The available environment variables are the following:
- `CONSOLE_DEVICE` = `stdout | stderr` defaults to **stderr**
- `LOG_TAGS` = `_all | _untagged | tag_name | -tag_name | +tag_name`
- `-` requires the tag to be missing
- `+` requires the tag to be present
- No sign means "One or more of no sign tags must be present"
- `LOG_LEVEL` =
`_none | trace | debug | info | warn | error | fatal`
- `_none` means no message will be logged
- User defined levels are also supported in `LOG_LEVEL`
- `LOG_DEBUG` = `on | off` when set to **on** prints debug messages and
errors, as well as tags information
- `LOG_FORMATTERS` = `on | off` when set to **on**, colorizes output
- `LOG_MODULE` = `on | off` when set to **on**, displays the module where
log line is being invoked
### Timestamp Configuration
It is recommended, but not required, to set logging timestamp to UTC, to avoid
confusing issues with DST changes. This can be done in `config.exs` using:
```elixir
import Config
config :logger, utc_log: true
```
Or with the native `Logger` function:
```elixir
Logger.configure(utc_log: true)
```
The timestamp is always formatted according to ISO-8601 format, however no
timezone modifier is displayed except for UTC.
In case timestam is configured to use UTC, `Z` is appended to the timestamp.
### Additional configuration
Other configuration options are provided that can be set directly on the
`Logger` backend:
- Alias a module namespace
- Exclude some namespaces from writing output
- Change output color on a per-level basis
These options can be set either through `config.exs`
```elixir
import Config
config :logger, Log.Backend,
module_alias: %{
LogTest.Deeply.Nested.Module.WithLog => ""
},
exclude_namespaces: [],
colors: %{}
```
Or with the native `Logger` function:
```elixir
Logger.configure_backend(
Log.Backend,
[
module_alias: %{},
exclude_namespaces: [],
colors: %{}
]
)
```
#### Alias a Module Namespace
Given the modules:
- `A.Module.Namespace.For.Something`
- `Other.Module.Namespace.For.SomethingElse`
and the configuration:
```elixir
Logger.configure_backend(
Log.Backend,
[
module_alias: %{
A.Module.Namespace => "",
Other.Module.Namespace => "OMN"
}
]
)
```
When the log message "a message" is written from module
`A.Module.Namespace.For.Something` it will be displayed as:
```
[2000-01-01T01:01:01.001Z] For.Something INFO: a message
```
When the log message "a message" is written from module
`Other.Module.Namespace.For.SomethingElse` it will be displayed as:
```
[2000-01-01T01:01:01.001Z] OMN.For.SomethingElse INFO: a message
```
#### Exclude Namespaces
Given the modules:
- `A.Module.Namespace.For.Something`
- `A.Module.Namespace.For.SomethingElse`
and the configuration:
```elixir
Logger.configure_backend(
Log.Backend,
[
exclude_namespaces: [
A.Module.Namespace
]
]
)
```
When the log message "a message" is written from module
`A.Module.Namespace.For.Something` or from
`A.Module.Namespace.For.SomethingElse`, no message is written.
#### Change Output Color
Given the following configuration:
```elixir
Logger.configure_backend(
Log.Backend,
[
colors: %{
debug: IO.ANSI.green(),
error: [IO.ANSI.red(), IO.ANSI.bright()]
}
]
)
```
- `error` color will is bold, red
- `debug` color is green
The `colors` map accepts level (as atoms) as keys, and
[IO.ANSI.ansidata](https://hexdocs.pm/elixir/IO.ANSI.html#t:ansidata/0) as
values.
## Customized Logger
It's possible to create a customized logger, which accepts a data structure
of your choice, as well as return a value of your choice.
It's sufficient to override the `bare_log` by following `Log.Args`
footprint:
```elixir
defmodule UpLog do
use Log, tags: [:upcase]
@impl true
def bare_log(data, meta)
def bare_log(data, meta) when is_function(data) do
Log.API.bare_log(
fn ->
data.() |> String.upcase()
end,
meta
)
end
def bare_log(data, meta) do
Log.API.bare_log(&String.upcase/1, meta)
end
end
```
`UpLog` can be used like `Log`:
```elixir
require UpLog
UpLog.info("a message")
```
And will output the following message:
```
[2000-01-01T01:01:01.001Z] INFO: A MESSAGE
```
## TODO
- [x] Guidelines for logging
- [ ] Restructure filters
- [ ] Testing
- [ ] Performance
## Thanks
This project is deeply inspired by [Eventide](https://eventide-project.org/)
and its [logger](http://docs.eventide-project.org/user-guide/logging).