Lager
A logging framework for Erlang/OTP
Install / Use
/learn @erlang-lager/LagerREADME
Overview
Lager (as in the beer) is a logging framework for Erlang. Its purpose is to provide a more traditional way to perform logging in an erlang application that plays nicely with traditional UNIX logging tools like logrotate and syslog.
Features
- Finer grained log levels (debug, info, notice, warning, error, critical, alert, emergency)
- Logger calls are transformed using a parse transform to allow capturing Module/Function/Line/Pid information
- When no handler is consuming a log level (eg. debug) no event is sent to the log handler
- Supports multiple backends, including console and file.
- Supports multiple sinks
- Rewrites common OTP error messages into more readable messages
- Support for pretty printing records encountered at compile time
- Tolerant in the face of large or many log messages, won't out of memory the node
- Optional feature to bypass log size truncation ("unsafe")
- Supports internal time and date based rotation, as well as external rotation tools
- Syslog style log level comparison flags
- Colored terminal output (requires R16+)
- Map support (requires 17+)
- Optional load shedding by setting a high water mark to kill (and reinstall) a sink after a configurable cool down timer
Contributing
We welcome contributions from the community. We are always excited to get ideas for improving lager.
If you are looking for an idea to help out, please take a look at our open issues - a number of them are tagged with Help Wanted and Easy - some of them are tagged as both! We are happy to mentor people get started with any of these issues, and they don't need prior discussion.
That being said, before you send large changes please open an issue first to discuss the change you'd like to make along with an idea of your proposal to implement that change.
PR guidelines
- Large changes without prior discussion are likely to be rejected.
- Changes without test cases are likely to be rejected.
- Please use the style of the existing codebase when submitting PRs.
We review PRs and issues at least once a month as described below.
OTP Support Policy
The lager maintainers intend to support the past three OTP releases from current on the main 3.x branch of the project. As of August 2019 that includes 22, 21 20
Lager may or may not run on older OTP releases but it will only be guaranteed tested on the previous three OTP releases. If you need a version of lager which runs on older OTP releases, we recommend you use either the 3.4.0 release or the 2.x branch.
Monthly triage cadence
We have (at least) monthly issue and PR triage for lager in the #lager room on the freenode IRC network every third Thursday at 2 pm US/Pacific, 10 pm UTC. You are welcome to join us there to ask questions about lager or participate in the triage.
Usage
To use lager in your application, you need to define it as a rebar dep or have some other way of including it in Erlang's path. You can then add the following option to the erlang compiler flags:
{parse_transform, lager_transform}
Alternately, you can add it to the module you wish to compile with logging enabled:
-compile([{parse_transform, lager_transform}]).
Before logging any messages, you'll need to start the lager application. The
lager module's start function takes care of loading and starting any dependencies
lager requires.
lager:start().
You can also start lager on startup with a switch to erl:
erl -pa path/to/lager/ebin -s lager
Once you have built your code with lager and started the lager application, you can then generate log messages by doing the following:
lager:error("Some message")
Or:
lager:warning("Some message with a term: ~p", [Term])
The general form is lager:Severity() where Severity is one of the log levels
mentioned above.
Configuration
To configure lager's backends, you use an application variable (probably in your app.config):
{lager, [
{log_root, "/var/log/hello"},
{handlers, [
{lager_console_backend, [{level, info}]},
{lager_file_backend, [{file, "error.log"}, {level, error}]},
{lager_file_backend, [{file, "console.log"}, {level, info}]}
]}
]}.
log_root variable is optional, by default file paths are relative to CWD.
The available configuration options for each backend are listed in their module's documentation.
Sinks
Lager has traditionally supported a single sink (implemented as a
gen_event manager) named lager_event to which all backends were
connected.
Lager now supports extra sinks; each sink can have different sync/async message thresholds and different backends.
Sink configuration
To use multiple sinks (beyond the built-in sink of lager and lager_event), you need to:
- Setup rebar.config
- Configure the backends in app.config
Names
Each sink has two names: one atom to be used like a module name for
sending messages, and that atom with _lager_event appended for backend
configuration.
This reflects the legacy behavior: lager:info (or critical, or
debug, etc) is a way of sending a message to a sink named
lager_event. Now developers can invoke audit:info or
myCompanyName:debug so long as the corresponding audit_lager_event or
myCompanyName_lager_event sinks are configured.
rebar.config
In rebar.config for the project that requires lager, include a list
of sink names (without the _lager_event suffix) in erl_opts:
{lager_extra_sinks, [audit]}
Runtime requirements
To be useful, sinks must be configured at runtime with backends.
In app.config for the project that requires lager, for example,
extend the lager configuration to include an extra_sinks tuple with
backends (aka "handlers") and optionally async_threshold and
async_threshold_window values (see Overload Protection
below). If async values are not configured, no overload protection
will be applied on that sink.
[{lager, [
{log_root, "/tmp"},
%% Default handlers for lager/lager_event
{handlers, [
{lager_console_backend, [{level, info}]},
{lager_file_backend, [{file, "error.log"}, {level, error}]},
{lager_file_backend, [{file, "console.log"}, {level, info}]}
]},
%% Any other sinks
{extra_sinks,
[
{audit_lager_event,
[{handlers,
[{lager_file_backend,
[{file, "sink1.log"},
{level, info}
]
}]
},
{async_threshold, 500},
{async_threshold_window, 50}]
}]
}
]
}
].
Custom Formatting
All loggers have a default formatting that can be overridden. A formatter is any module that
exports format(#lager_log_message{},Config#any()). It is specified as part of the configuration
for the backend:
{lager, [
{handlers, [
{lager_console_backend, [{level, info}, {formatter, lager_default_formatter},
{formatter_config, [time," [",severity,"] ", message, "\n"]}]},
{lager_file_backend, [{file, "error.log"}, {level, error}, {formatter, lager_default_formatter},
{formatter_config, [date, " ", time," [",severity,"] ",pid, " ", message, "\n"]}]},
{lager_file_backend, [{file, "console.log"}, {level, info}]}
]}
]}.
Included is lager_default_formatter. This provides a generic, default
formatting for log messages using a structure similar to Erlang's
iolist which we
call "semi-iolist":
- Any traditional iolist elements in the configuration are printed verbatim.
- Atoms in the configuration are treated as placeholders for lager metadata and
extracted from the log message.
- The placeholders
date,time,message,sevandseveritywill always exist. sevis an abbreviated severity which is interpreted as a capitalized single letter encoding of the severity level (e.g.'debug'->$D)- The placeholders
pid,file,line,module,function, andnodewill always exist if the parse transform is used. - The placeholder
applicationmay exist if the parse transform is used. It is dependent on finding the applicationsapp.srcfile. - If the error logger integration is used, the placeholder
pidwill always exist and the placeholdernamemay exist. - Applications can define their own metadata placeholder.
- A tuple of
{atom(), semi-iolist()}allows for a fallback for the atom placeholder. If the value represented by the atom cannot be found, the semi-iolist will be interpreted instead. - A tuple of
{atom(), semi-iolist(), semi-iolist()}represents a conditional operator: if a value for the atom placeholder can be found, the first semi-iolist will be output; otherwise, the second will be used. - A tuple of
{pterm, atom()}will attempt to lookup the value of the specified atom from the persistent_term feature added in OTP 21.2. The default value is"". The default value will be used if the key cannot be found or if this formatting term is specified on an OTP release before OTP 21. - A tuple of `{pterm, atom(), semi-iolist
- The placeholders
