The Flutter Logging package is pretty good but it is missing a bit of documentation that I would have found useful. In particular it supports hierarchical logging but I had to look at the code to see how to use it. I have documented what I learned in this post.
The Basics
Getting logging working is pretty trivial, it works like most logging packages, though it has a few more log levels. For the most up-to-date usage guide see the package documentation. Here’s what I found based on the example at the time I wrote this.
Configuring Logging
Logging ignores all messages until it is configured to handle them. Here is a very simple configuration that just prints all log messages. Note the first line of this example sets the level so that any log message will print. For release I would change this to Level.INFO
.
Logger.root.level = Level.ALL; // defaults to Level.INFO
Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.message}');
});
Logging Messages
Logging a message is simple, just create a named Logger and then call one of its many methods.
class MyClassName {
final log = Logger('MyClassName');
void someMethod( String param1 ) {
log.fine("someMethod: $param1");
// do stuff ...
}
}
So far I’ve settled on the following hierarchy:
- Logger.severe – means that something went very wrong and the application is unlikely to continue working, or the user will experience failures. Something like the DB failing to initialize for an application that requires a working DB connection to function.
- Logger.info – is something that should always be in the log. It represents either an important waypoint or something that should not happen in the normal course of operation but the application can recover from without harming the user experience. For example, a REST call failure that will be retried later.
- Logger.fine – for debug or troubleshooting messages. This level is typically turned off in the release version.
There may be a use for other log levels like finer or finest to filter out some very high volume messages, for example if the application would log too much in a loop. This is just my usage, feel free to use the levels however you want. I do find that it’s a good idea to establish some outline for yourself of how you intend the levels to be used.
I try to use .info logs whenever the code reaches an important waypoint: starting, stopping, initializing, coming back into focus, etc. Reading through a log of .info messages should provide a clear guide to what the application has done, maybe not in detail, but at a high level. It should be possible to leave the logging level at .info for release and have useful logs for troubleshooting.
Hierarchical Logging
Hierarchical logging is the ability to have parent child relationships between logs so that you can set the log level for all children at once. That makes it easy to turn logging on for a particular section of code to focus on something specific. The Logging package supports hierarchical logging but it wasn’t clear to me how until I read the code.
The bit that I think was missing is that you can separate Logger names into levels using periods. If you do that then you can turn logging on conditionally at each level. Here’s the example logging code from above using a hierarchical Logger name.
class MyClassName {
final log = Logger('db.MyClassName');
void someMethod( String param1 ) {
log.fine("someMethod: $param1");
// do stuff ...
}
}
Everything is the same except that instead of just ‘MyClassName’ the Logger now has the name ‘db.MyClassName’. From that name there are two Loggers created, one for Logger('db')
and one for Logger('MyClassName')
. The ‘db’ Logger is a child of the root logger and the ‘MyClassName’ logger is a child of the ‘db’ Logger. The root Logger always exists, there’s nothing magical about it, it’s just the Logger with an empty name Logger('')
. Assuming your code is organized by folder into logically related groups then using the folder name as the prefix, or a set of nested folders, would make sense. A name like Logger('ui.settings.UserSettings')
would create three loggers, and so on.
Using the Hierarchy
A reasonable question would be ‘Why bother?’. The answer is that naming Loggers like this gives you some good tools for troubleshooting in the future. To use those tools you have to turn hierarchical logging on and then you can enable logging at any level you like to get just the messages you’re interested in.
void setupLogging() {
Logger.root.level = Level.INFO; // defaults to Level.INFO
hierarchicalLoggingEnabled = true;
Logger("db").level = Level.ALL;
Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}');
});
}
In the example above the code is turning on all logging for any loggers that are children of the ‘db’ Logger. That means anything that starts with ‘db.’ will get this log level. You could make it more specific and just turn on ‘db.MyClassName’. This provides the ability to target specific sections of code and get a lot of detail while troubleshooting.
Extending the Log
Do have a look at all of the properties of the LogRecord object. I like the loggerName
in the logs so I can easily see where the message is from. Also, passing a closure instead of just the log message is great for supporting expensive log operations for troubleshooting cases without incurring the overhead in regular use.
Here’s an example showing how to configure the logger to print more detail from the LogRecord
.
Logger.root.onRecord.listen((record) {
print( '${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}');
});