debugPrint and the power of hiding and customizing your logs in Dart
When developing and debugging software, we usually log some kind of messages to assert its behaviour. In Dart, we can do this using the print :
flutter: Initializing the build method
flutter: UserId: user908976
This output doesn’t provide us with much information. Besides the printed message and a flutter
label, we don’t have an indication of the app name or a timestamp of when the message was printed.
Furthermore, if you use the command flutter logs
you will see the output of the print
function in all applications you have installed in the phone/emulator. This means that even if the app is in release mode
, it will still be printed in the terminal.
This poses two problems:
- Any print statement can be potentially seen by any user of the app, which can be a problem if we print sensitive information.
- If we are monitoring your app using
flutter logs
we won’t be able to distinguish between our apps and other flutter apps installed in your phone (even though there.
Let’s tackle the first problem.
Hiding Print Statements in a Production App
In our apps, we can use the concept of Flavors, which can be used to create different version of our application. This can be useful if our API uses different environments for development and production or if we want to specify a different behaviour for our app, as for example hiding the logs in the production app.
To be able to modify the behaviour of our print
function, we can take a look at the debugPrint method from the foundation library. From the documentation we can read:
The implementation of this function can be replaced by setting the debugPrint variable to a new implementation that matches the DebugPrintCallback signature. For example, flutter_test does this.
The default value is debugPrintThrottled. For a version that acts identically but does not throttle, use debugPrintSynchronously.
So, by overriding the DebugPrintCallback we can change the behaviour of our printed statements. Before changing this implementation, we need to make sure that every print
statement in our project gets replaced by a debugPrint
statement
So, assuming we are using different main.dart
files: main.dart
for production and main_dev.dart
for development, we can set a specific callback for each flavour.
void main() {
debugPrint = (String message, {int wrapWidth}) {};
runApp(MyApp());
}
Now, when the debugPrint
statements in our code are called, they won’t be printed to the console since we gave this function an empty callback.
Creating Custom Print Statements
With this information, we can now look into ways to manipulate the string we use in debugPrint
to provide more information when logging. But first, we must look into the documentation of debugPrint
again.
The default value is debugPrintThrottled. For a version that acts identically but does not throttle, use debugPrintSynchronously.
As we can see, we have two default callbacks to this function: debugPrintThrottled
and debugPrintSynchronously
.
Let’s review debugPrintThrottled
Implementation of debugPrint that throttles messages. This avoids dropping messages on platforms that rate-limit their logging (for example, Android).
Though this seems reasonable, we are warned in the debugPrint
’s documentation that this method can lead to out-of-order messages in the log if used also with print
:
By default, this function very crudely attempts to throttle the rate at which messages are sent to avoid data loss on Android. This means that interleaving calls to this function (directly or indirectly via, e.g., debugDumpRenderTree or debugDumpApp) and to the Dart print method can result in out-of-order messages in the logs.
Alternatively, debugPrintSynchronously
has a more straightforward implementation:
Alternative implementation of debugPrint that does not throttle. Used by tests.
So basically, if we want to avoid the printing limit of each OS, we can use debugPrintThrottled
, otherwise we can change our implementation to debugPrintSynchronously
.
To provide more information about the application being debugged, we can use package_info in order to retrieve the package name
, version
and build_number
. With this, we can now customise our debugPrint
statements:
import 'package:flutter/foundation.dart';
import 'package:package_info/package_info.dart';
void main() async {
var packageInfo = await PackageInfo.fromPlatform();
var version =
"${packageInfo.packageName} ${packageInfo.version} (${packageInfo.buildNumber})";
debugPrint = (String message, {int wrapWidth}) => debugPrintSynchronouslyWithText(message, version, wrapWidth: wrapWidth);
runApp(MyApp());
}
void debugPrintSynchronouslyWithText(String message, String version,
{int wrapWidth}) {
message =
"[${DateTime.now()} - $version]: $message";
debugPrintSynchronously(message, wrapWidth: wrapWidth);
}
Which will print each message with the following prefix:
flutter: [2019-03-16 15:35:50.176958 - com.vanethos.logExample 1.0.0 (1)]: Initializing the build method
flutter: [2019-03-16 15:35:50.180908 - com.vanethos.logExample 1.0.0 (1)]: UserId: user908976
And we’re done! 🕺 Now we can fully customise our console logs and hide them from prying eyes.
Want to get the latest articles and news? Subscribe to the newsletter here 👇
And for other articles, check the rest of the blog! Blog - Gonçalo Palma