A basic logging solution for Dart modeled after NLog, which uses implementations from Dart ILogger.
Usage
This implememtation comes with everthing needed to create your own loggers, and in fact you are encouraged to create your own for specific purposes that you encounter. However for the sake of usability, this package comes with two default implementations ready to go out of the box:
Basic Logger
final ILogger logger = BasicLogger(name: 'YourLogger');
logger.info('some message') // [09-12-2013T19:35:21+9:00] some message ||
The basic logger uses the ConsoleTarget
target to print to the stdout console.
Null Logger
final ILogger logger = NullLogger(name: 'YourNullLogger');
logger.info('some message'); // nothing will happen, this is a NOP
Log Levels
The following log levels are available. They're listed in asencing order of severity:
- trace
- debug
- info
- warn
- error
- fatal
- off
Off is a special level which indicates that it shouldn't be logged.
Loggers, Targets, Formatters, Sinks,
Heirarchy
The hierarchy of loggers is like this:
[ Logger ]
|
|
[ Target {1,n} ]
|
|
[ Sink ]
Loggers have n-many targets, and each target has 1 sink.
Loggers
A logger is an object that has methods for writing messages at various log levels. For example:
logger.warn('some warning message');
logger.fatal('an exceptionally bad message');
Exceptions and Event Parameters
Logging methods can take optional exceptions and eventParameters. These get their own special consideration when printing out logging statements:
try {
throw Exception("Didn't work");
} on Exception catch (e) {
logger.error('Failed to push the thing', exception: e);
}
If the ConsoleTarget is configured for this logger, which it is by default, the stdout will look like this:
9-12-203T19:54:32+9:00
ERROR
Failed to push the thing |Exception("Didn't work")|
Logging methods can also take an eventParameters
field which is an optional JSON Object of unspecified schema which can be used to pass extra data along:
void serveFile(String path) {
logger.info('Received request for file', eventParameters: {'path': path});
}
serveFile("addresses.txt");
Again using the ConsoleTarget, this will print:
9-12-203T19:54:32+9:00
INFO
Received request for file ||{'path': 'addresses.txt'}
Enabled Levels
Loggers can choose whether to enable to log or ignore events of certain levels.
You can check the status of each log level by the specific getters:
logger.isTraceEnabled; // true or false
or by sending a specific log level:
logLevel = LogLevel.trace;
logger.isLogLevelEnabled(logLevel); // true or false
Depending on the implementation of the logger you are using, you may be able to change this at runtime or not.
Targets
Premade Targets
The following targets are included by default:
- BasicConsoleTarget
- BasicFileTarget
The BasicConsoleTarget writes log events to stdout
, and the BasicFileTarget
, when given a path to a file (existing or not, doesn't matter), will write its contents to the file.
With respect to the file target, log rotation is included in this package.
Overview
Targets are the where & when of writing log file. Targets contain three methods:
class ITarget {
/// Formats the log event and writes to the sink asychronously. Not required.
///
/// For async, see [writeAsync]
void writeSync(LogEvent logEvent);
/// Formats the log event and writes to the sink asychronously. Required.
///
/// For sync, see [writeSync]
Future<void> writeAsync(LogEvent logEvent);
/// Whether this log event should be written to this logger
bool shouldWrite(LogEvent logEvent);
}
It is up to each target how to implement these methods, but just be aware that a target can choose to ignore a log message as it sees fit with the shouldWrite()
method.
That covers the "when" but the "where" is determined by the writeSync()
and writeAsync()
methods. Targets are constructed with two additional fields:
final IFormatter formatter;
final ISink sink;
which the write methods should use to format and send off the actual bitwise log message.
Here is the implementation for the writeSync BasicConsoleTarget
:
@override
void writeSync(LogEvent logEvent) {
final msg = formatter.format(logEvent);
sink.writeAsync(msg);
}
notice that it uses the formatter
to format the incoming log event, which rfeturns the full stringified log message as it will be written out in memory, then it sends the message to the sink
.
The sink takes care of the "how" to write, the formatter takes care of the "what", and the target takes care of orchestrating these things together.
Formatters
A formatter is an object owned by a Target that determines exactly how to produce a String from a log event. There are two formatters included by default:
BasicFormatter
JsonFormatter
JsonLinesFormatter
The BasicFormatter
produces strings in the following format:
$DateTime
$LogLevel
LoggerName
$message |$exception||$eventProperties
The JsonFormatter
produces strings in the following format:
{
'Timestamp': number,
'Level': string,
'Name': string,
'Message': string,
'Exception': string?,
'EventProperties': JsonObject?
}
The JsonLinesFormatter
produces strings in the following format:
{'Timestamp': number,'Level': string,'Name': string,'Message': string,'Exception': string?,'EventProperties': JsonObject?}
The difference between jsona nd json lines is that JsonLines uses the JSONLines Format, which is a good format for streaming log data.
Variables
The includes formatters, and therefore the default included set of sinks and targets, all process their Log Event messages for the eventParameters and perform substitution where appropriate.
For example:
logger.info('Received request for file at path {path}', eventParameters: {'path': 'addresses.txt'});
This will look at the parameters, if any, supplied to eventParameters
, and then look for any matching keys in curly braces in the original message and replace the curly braces with the value found in the eventParameters. This will produce a final message like this:
9-12-203T19:54:32+9:00
INFO
Received request for file at path addresses.txt ||{'path': 'addresses.txt'}
This functionality can be escaped with backslashes:
logger.info('Received request for file at path \{path\}', eventParameters: {'path': 'addresses.txt'});
9-12-203T19:54:32+9:00
INFO
Received request for file at path {path} ||{'path': 'addresses.txt'}
Sinks
Sinks are an actual end-point for writing a log message. This could include a file, a stdout/stderr pipe, a network connection, or a database connection.
Premade Sinks
The following sinks are included by default in this package:
BasicConsoleSink
BasicFileSink
The BasicConsoleSink
takes care of actually writing to stdout
, and the BasicFileSink
takes care of the much more complicated task of appending to a file. The file sink also has the ability to take a FileRotation object to help it roll over files when appropriate.
Overview
Sinks really only have two methods that are interesting:
/// Writes the final message to the sink. Not all writers require this.
/// For async write, see [writeAsync]
void writeSync(String formattedMessage);
/// Flushes any pending changes to this sink. Not all writers require this.
///
/// For async flush, see [flushAsync]
void flushSync();
A call to writeSync
may or may not actually immediately produce a write event to the underlying final location. Instead, it is up to the sink when it is most appropriate to perform those writes. That is, a sink may batch up writes in memory before sending them onward. If the caller of the library wishes however, they can force writes with thw flush
family of methods.
Log Rotation
This package includes a rudimentary system for performing File Rotations when using the FileTarget
. Of course, you are free to use this in your own loggers as well, but it is included fully packaged for your convenience.
class FileRotationSettings {
/// Rotate the log file every time this duration has passed from initial write
/// If null, do not rotate on time
final Duration? rotateOnEvery;
/// Rotate the log file when it reaches this many bytes written
/// If null, do not rotate on byte size
final int? rotateOnByteSize;
/// Keep this many past log files around
/// if null or negative, keep infinite log files.
/// If zero, keep no log files at all
final int? keepHowMany;
}
Structured Logging
Using the included JsonFormatter
or JsonLinesFormatter
you can achieve structured logging.
Future Development
Targets will be able to be defined by a Json File, which can be modified from outside the program. This fill will be read at runtime and can modify behavior of the logging system ad-hoc.
Loggers will be able to be reconfigrued at runtime, so that changes to their targets can be reloaded from within a running program.
Contributing
If you want to contribute to this repository, consider looking at the underlying Dart ILogger, or this repository at https://github.com/0xnf/dlog_basic