# 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. ```rust 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). ```rust 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: ```rust 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: ```rust 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: ```rust 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: ```rust 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 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. ```rust 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. ```rust 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 message - `context`: 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: ```rust 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](#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 ```rust 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.