1 unstable release
0.1.0 | Mar 1, 2019 |
---|
#776 in Debugging
84KB
2K
SLoC
Astrolog
A logging system for Rust that aims to be easy and simple to use and flexible.
The main purpose of Astrolog is to be used in applications, not in crates or libraries. It focuses on simplicity rather than maximum efficiency. Performance is not ignored, but ease of use has higher priority during development.
How to use
Astrolog can be used in two ways: the global logger can be called statically from anywhere in your application, while if you want to have one or more instance(s) with different configurations, you can instantiate them separately and pass the relevant one to your application's components.
The global logger
This method is particularly useful if you are building a small app and/or don't want to inject your logger to functions
or objects that could use it. If you are used to the default rust's log
functionality or to crates like slog
that
provide logging macros, this will look familiar, but with static calls instead of macros.
fn main() {
astrolog::config(|logger| {
logger
.set_global("OS", env::consts::OS)
.push_handler(ConsoleHandler::new().with_levels_range(Level::Info, Level::Emergency));
});
normal_function();
function_with_error(42);
}
fn normal_function() {
astrolog::debug("A debug message");
}
fn function_with_error(i: i32) {
astrolog::with("i", i)
.with("line", line!())
.with("file", file!())
.error("An error with some debug info");
}
The normal logger
This method is useful if you want different loggers for different parts of your application, or if you want to pass around the logger via dependency injection, a service locator or a DIC (or using singletons).
fn main() {
let logger1 = Logger::new()
.with_global("OS", env::consts::OS)
.with_handler(ConsoleHandler::new().with_levels_range(Level::Trace, Level::Info));
let logger2 = Logger::new()
.with_global("OS", env::consts::OS)
.with_handler(ConsoleHandler::new().with_levels_range(Level::Info, Level::Emergency));
normal_function(&logger1);
function_with_parameter(42, &logger2);
}
fn normal_function(logger: &Logger) {
logger.debug("A debug message");
}
fn function_with_parameter(i: i32, logger: &Logger) {
logger
.with("i", i)
.with("line", line!())
.with("file", file!())
.error("An error with some debug info");
}
Syntax and configuration
Astrolog works by letting the user build log entries, and passing them to handlers. Each logger can have one or multiple handlers, each individually configurable to only accept a certain set of log levels.
For example, you might want a ConsoleHandler
(or TermHandler
for colored output) to handle trace
, debug
and
info
levels, but at the same time you want to save messages of all levels to a file, and maybe send all messages of level
warning
and higher to an external logging aggregator.
Entries are built via an implicit builder pattern and are sent to the handlers when the appropriate level function is called.
Let's make it simpler with an example:
fn main() {
logger.info("Some informative message")
}
This will build the entry and immediately send it to the handlers.
Using with
will instead start building the entry:
fn main() {
logger.with("some key", 42).info("Some informative message")
}
This will create an entry, store a key-value pair ("some key"
and 42
) in it and finally send it to the handlers.
Multiple calls to with
(or with_multi
or with_error
) can be chained before calling the level method.
Available levels
Astrolog uses more levels than normal loggers. This is the complete list in order of severity:
- Trace
- Profile
- Debug
- Info
- Notice
- Warning
- Error
- Critical
- Alert
- Emergency
The functions on the Logger
are named accordingly (.trace()
, .profile()
, ...). There is also a .log()
function accepting the Level
as first parameter, so that it can be decided programmatically at runtime.
Levels from debug
to emergency
mimic Unix/Linux's syslog levels.
Other logging systems tend to conflate into debug
all the debugging, profiling and tracing informations, while
Astrolog suggests using debug only for spurious "placeholder" messages.
profile
is meant to log profiling information (execution times, number of calls to a function or loop iterations,
etc.), while trace
is meant for tracing the program execution (for example when debugging)
This allows to better filter debug info and send them to specific handlers. For example you mey want to send profile
info to a Prometheus handler, debug
, info
and notice
to STDOUT, trace
to a file for easier analysis and
everything warning
and above to STDERR.
Examples
To run the examples, use:
cargo run --example simple
cargo run --example global
cargo run --example passing
cargo run --example errors
cargo run --example multithread
Each example shows different ways to use Astrolog.
simple
shows how to create a dedicated logger and use it via method calls, with or without extra parameters.
global
shows how to configure and use the global logger via static calls.
passing
shows how you can create a dedicated logger and pass it around by reference or in a Rc
.
errors
show how to pass Rust errors to the logging functions and print a trace of the errors.
multithread
shows how you can use Astrolog in a multithreaded application easily.
Handlers
The core Astrolog crate provides a few basic handlers, while others can be implemented in separate crates.
Console handler
The console handler simply prints all messages to the console, on a single line each, with a configurable format (see [Formatters] later on).
This handler can be configured to use either stdout
or stderr
to print the messages, but given the modularity
of Astrolog, two ConsoleHandler can be registered for different log levels to go to different outputs.
This handler does not provide output coloring (see the astrolog-term
crate for this).
Example:
fn main() {
let logger = Logger::new()
.with_handler(ConsoleHandler::new()
.with_stdout()
.with_levels_range(Level::Debug, Level::Notice)
)
.with_handler(ConsoleHandler::new()
.with_stderr()
.with_levels_range(Level::Warning, Level::Emergency)
);
}
Vec handler
This handler simply collects all the messages in a Vec
to allow to process them later. It is useful, for example,
to debug a new formatter, or to batch messages.
Using it requires a bit more work during setup due to Rust's ownership rules:
fn main() {
let handler = VecHandler::new();
let logs_store = handler.get_store();
let logger = Logger::new()
.with_handler(handler);
logger.info("A new message");
let logs = logs_store.take();
// logs is now a Vec<Record> over which you can iterate
}
Null handler
This handler is the /dev/null
of Astrolog. It simply discards any message it receives.
It can be used to decide at runtime to not log anything, with a minimal processing cost, keeping the logger instance in place.
fn main() {
let logger = Logger::new()
.with_handler(
NullHandler::new()
);
}
Formatters
Formatters allow to format the log record in different ways, depending on the user's preferences or the handler's required format
For example, logging to a file could be best done with a LineFormatter
, while printing an error on a web page
would benefit from an HtmlFormatter
, and sending it via a REST API could require a JsonFormatter
.
Each record formatter can then use different "sub-formatters" for the level indicator, the date and the context (the values associated to a record).
LineFormatter
This is the simplest and probably most useful formatter provided in the base Astrolog crate. It simply returns the log record info in a single line, formatted by a template. The template supports placeholders to insert parts of the record or of the context in the output.
fn main() {
let logger = Logger::new()
.with_handler(ConsoleHandler::new()
.with_formatter(LineFormatter::new()
.with_template("{{ [datetime]+ }}{{ level }}: {{ message }}{{ +context? }}")
)
);
}
This example shows the default configuration for the ConsoleHandler
, so it's not needed, but it gives an idea of how
to configure the formatter and its template.
The supported placeholders are:
datetime
: inserts the record's date and time, accordingly to the configured datetime formatter (see below)level
: inserts the record's level, accordingly to the configured level formatter (see below)message
: inserts the logged messagecontext
: inserts the context, accordingly to the configured context formatter (see below)
Any other placeholder is considered as the name of a variable in the context, and JSON pointers are supported to access embedded info.
All the placeholders, except for message
(to avoid loops) can be used in the message itself to add context
variables to the message. For example:
fn main() {
logger
.with("user", json!({"name": "Alex", "login": { "username": "dummy123" } }))
.debug("Logged in as {{ /user/login/username }}");
}
HtmlFormatter
This formatter is very similar to the LineFormatter, with the added feature of escaping automatically the strings in
the message and in the values so that &
, <
and >
are correctly encoded to entities and show on the page.
JsonFormatter
This formatter transforms the record into a JSON object and returns it as a string.
By default the top-level field names are time
, level
, message
and context
but they can be customised via
.set_field_names()
or .with_field_names()
Log parts formatters
The parts of a log messages can be formatted in different ways. Sometimes the handler requires a specific format, for example for dates, while sometimes it's just a matter of preference.
Date formatting
formatter::date::Format
is an enum supporting a number of predefined formats and the ability to use a custom
format, based on [chrono]'s format.
Level formatting
formatter::level::Format
is an enum supporting a combination of long and short (4-chars) representation of log
levels, in lowercase, uppercase or titlecase.
Context formatting
This module provides, in the core crate, two ways to format the context associated with a log record: JSON and JSON5.
While JSON is useful (or even mandatory) when transmitting logs to other services or to applications, JSON5 has a better readability for humans, so it's especially useful for console, file and syslog logs (and it's the default).
Fields naming
A special case is formatter::fields::Names
as it is used to define the names of the main fields in some formatters,
like the JSON one, allowing to customise how the final representation appears.
Example
fn main() {
let logger = Logger::new()
.with_handler(ConsoleHandler::new()
.with_formatter(LineFormatter::new()
.with_date_format(DateFormat::Email)
.with_level_format(LevelFormat::LowerShort)
)
);
}
License
As a clarification, when using Astrolog as a Rust crate, the crate is considered as a dynamic library and therefore the LGPL allows you to use it in closed source projects or projects with different open source licenses, while any modification to Astrolog itself must be released under the LGPL 2.1 license.
Dependencies
~6MB
~121K SLoC