Logging
Learn how to add logging to your Dart application to help with debugging and monitoring.
In this chapter, you'll learn how to add logging to your Dart application. Logging is a critical tool for debugging, monitoring, and understanding the behavior of your application in different environments.
Prerequisites
#Before you begin this chapter, ensure you:
-
Have completed Chapter 11 and have a working Dart development environment
with the
dartpediaproject. - Understand the basics of debugging and why it's important to track errors and events in your application.
Tasks
#
In this chapter, you'll add logging to the dartpedia CLI application to help
track errors and monitor its behavior. This will involve adding the logging
package, creating a Logger instance, and writing log messages to a file.
Task 1: Add the logging package
#
First, add the logging package to your project's dependencies.
Open the
cli/pubspec.yamlfile.Locate the
dependenciessection.-
Add the
loggingpackage to your dependencies:yamldependencies: http: ^1.3.0 command_runner: path: ../command_runner wikipedia: path: ../wikipedia # Add the following line logging: ^1.2.0 Run
dart pub getin theclidirectory to fetch the new dependency.
Task 2: Create a logger
#
Next, create a Logger instance and configure it to write log messages
to a file. This involves creating a new file for the logger and setting up
the necessary imports.
Create a new file called
cli/lib/src/logger.dart.-
Add the necessary imports and define the
initFileLoggerfunction.cli/lib/src/logger.dartdartimport 'dart:io'; import 'package:logging/logging.dart'; Logger initFileLogger(String name) { // Enables logging from child loggers. hierarchicalLoggingEnabled = true; // Create a logger instance with the provided name. final logger = Logger(name); final now = DateTime.now(); // The rest of the function will be added below. // ... return logger; } -
Add the code to find the project's root directory, create a
logsdirectory if one doesn't exist, and create a unique log file.dartLogger initFileLogger(String name) { hierarchicalLoggingEnabled = true; final logger = Logger(name); final now = DateTime.now(); // Get the path to the project directory from the current script. final segments = Platform.script.path.split('/'); final projectDir = segments.sublist(0, segments.length - 2).join('/'); // Create a 'logs' directory if it doesn't exist. final dir = Directory('$projectDir/logs'); if (!dir.existsSync()) dir.createSync(); // Create a log file with a unique name based on the current date and logger name. final logFile = File( '${dir.path}/${now.year}_${now.month}_${now.day}_$name.txt', ); // The rest of the function will be added below. // ... return logger; } -
Configure the logger's level and set up a listener to write log messages to the file.
dartLogger initFileLogger(String name) { hierarchicalLoggingEnabled = true; final logger = Logger(name); final now = DateTime.now(); final segments = Platform.script.path.split('/'); final projectDir = segments.sublist(0, segments.length - 2).join('/'); final dir = Directory('$projectDir/logs'); if (!dir.existsSync()) dir.createSync(); final logFile = File( '${dir.path}/${now.year}_${now.month}_${now.day}_$name.txt', ); // Set the logger level to ALL, so it logs all messages regardless of severity. // Level.ALL is useful for development and debugging, but you'll likely want to // use a more restrictive level like Level.INFO or Level.WARNING in production. logger.level = Level.ALL; // Listen for log records and write each one to the log file. logger.onRecord.listen((record) { final msg = '[${record.time} - ${record.loggerName}] ${record.level.name}: ${record.message}'; logFile.writeAsStringSync('$msg \n', mode: FileMode.append); }); return logger; }This code does the following:
- It enables hierarchical logging using
hierarchicalLoggingEnabled = true. - It creates a
Loggerinstance with the given name. - It gets the project directory from the
Platform.script.path. - It creates a
logsdirectory if it doesn't exist. - It creates a log file with the current date and the logger name.
- It sets the logger level to
Level.ALL, meaning it will log all messages. This is useful for development and debugging, but you'll likely want to use a more restrictive level likeLevel.INFOorLevel.WARNINGin production. - It listens for log records and writes them to the log file.
- It enables hierarchical logging using
-
Create a new file called
cli/lib/cli.dartand exportlogger.dart. This makes theinitFileLoggeravailable to other parts of your app.cli/lib/cli.dartdartexport 'src/commands/get_article.dart'; export 'src/commands/search.dart'; export 'src/logger.dart';
Task 3: Use the logger in cli.dart
#
Now, use the initFileLogger function in cli/bin/cli.dart to create a
logger instance and log messages to a file.
Open the
cli/bin/cli.dartfile.-
Add the import for the logger:
cli/bin/cli.dartdartimport 'package:cli/cli.dart'; import 'package:command_runner/command_runner.dart'; -
Modify the
mainfunction to initialize the logger and pass it to the commands:cli/bin/cli.dartdartimport 'package:cli/cli.dart'; import 'package:command_runner/command_runner.dart'; void main(List<String> arguments) async { final errorLogger = initFileLogger('errors'); final app = CommandRunner<String>( onOutput: (String output) async { await write(output); }, onError: (Object error) { if (error is Error) { errorLogger.severe( '[Error] ${error.toString()}\n${error.stackTrace}', ); throw error; } if (error is Exception) { errorLogger.warning(error); } }, ) ..addCommand(HelpCommand()) ..addCommand(SearchCommand(logger: errorLogger)) ..addCommand(GetArticleCommand(logger: errorLogger)); app.run(arguments); }This code does the following:
- It initializes a
Loggerinstance usinginitFileLogger('errors'). - It passes the
loggerinstance toCommandRunnerand individual commands.
- It initializes a
Task 4: Create the SearchCommand command
#
The core functionality of the CLI lives in its commands. Create the
SearchCommand and GetArticleCommand files and add the necessary code,
including the logging and error handling.
Create a new file named
/cli/lib/src/commands/search.dart.-
Add the imports and a basic class structure. This
SearchCommandclass extendsCommand<String>, and its constructor accepts aLoggerinstance. Accepting the logger in the constructor is a common pattern called dependency injection, which allows the command to log events without needing to create its own logger.dartimport 'dart:async'; import 'dart:io'; import 'package:command_runner/command_runner.dart'; import 'package:logging/logging.dart'; import 'package:wikipedia/wikipedia.dart'; class SearchCommand extends Command<String> { SearchCommand({required this.logger}); final Logger logger; @override String get description => 'Search for Wikipedia articles.'; @override String get name => 'search'; @override String get valueHelp => 'STRING'; @override String get help => 'Prints a list of links to Wikipedia articles that match the given term.'; @override FutureOr<String> run(ArgResults args) async { // The rest of the function will be added below. // ... } } -
Now, add the core logic to the
runmethod. This code checks for a valid argument, calls thesearch()function from thewikipediapackage, formats the results, and returns the results as a string.dartimport 'dart:async'; import 'dart:io'; import 'package:command_runner/command_runner.dart'; import 'package:logging/logging.dart'; import 'package:wikipedia/wikipedia.dart'; class SearchCommand extends Command<String> { SearchCommand({required this.logger}); final Logger logger; @override String get description => 'Search for Wikipedia articles.'; @override String get name => 'search'; @override String get valueHelp => 'STRING'; @override String get help => 'Prints a list of links to Wikipedia articles that match the given term.'; @override FutureOr<String> run(ArgResults args) async { if (requiresArgument && (args.commandArg == null || args.commandArg!.isEmpty)) { return 'Please include a search term'; } final buffer = StringBuffer('Search results:'); final SearchResults results = await search(args.commandArg!); for (var result in results.results) { buffer.writeln('${result.title} - ${result.url}'); } return buffer.toString(); } } -
Next, add the "I'm feeling lucky" feature by adding a flag to the constructor. Then, in the
runmethod, add the logic to check if the flag is set and, if so, get the summary of the top search result.dartimport 'dart:async'; import 'dart:io'; import 'package:command_runner/command_runner.dart'; import 'package:logging/logging.dart'; import 'package:wikipedia/wikipedia.dart'; class SearchCommand extends Command<String> { SearchCommand({required this.logger}) { addFlag( 'im-feeling-lucky', help: 'If true, prints the summary of the top article that the search returns.', ); } final Logger logger; @override String get description => 'Search for Wikipedia articles.'; @override String get name => 'search'; @override String get valueHelp => 'STRING'; @override String get help => 'Prints a list of links to Wikipedia articles that match the given term.'; @override FutureOr<String> run(ArgResults args) async { if (requiresArgument && (args.commandArg == null || args.commandArg!.isEmpty)) { return 'Please include a search term'; } final buffer = StringBuffer('Search results:'); final SearchResults results = await search(args.commandArg!); if (args.flag('im-feeling-lucky')) { final title = results.results.first.title; final Summary article = await getArticleSummaryByTitle(title); buffer.writeln('Lucky you!'); buffer.writeln(article.titles.normalized.titleText); if (article.description != null) { buffer.writeln(article.description); } buffer.writeln(article.extract); buffer.writeln(); buffer.writeln('All results:'); } for (var result in results.results) { buffer.writeln('${result.title} - ${result.url}'); } return buffer.toString(); } } -
Finally, wrap the main logic in a
try/catchblock. This allows you to handle potential exceptions that could arise from network issues or data formatting problems. You'll use the injectedloggerto record these errors to the log file.dartimport 'dart:async'; import 'dart:io'; import 'package:command_runner/command_runner.dart'; import 'package:logging/logging.dart'; import 'package:wikipedia/wikipedia.dart'; class SearchCommand extends Command<String> { SearchCommand({required this.logger}) { addFlag( 'im-feeling-lucky', help: 'If true, prints the summary of the top article that the search returns.', ); } final Logger logger; @override String get description => 'Search for Wikipedia articles.'; @override String get name => 'search'; @override String get valueHelp => 'STRING'; @override String get help => 'Prints a list of links to Wikipedia articles that match the given term.'; @override FutureOr<String> run(ArgResults args) async { if (requiresArgument && (args.commandArg == null || args.commandArg!.isEmpty)) { return 'Please include a search term'; } final buffer = StringBuffer('Search results:'); try { final SearchResults results = await search(args.commandArg!); if (args.flag('im-feeling-lucky')) { final title = results.results.first.title; final Summary article = await getArticleSummaryByTitle(title); buffer.writeln('Lucky you!'); buffer.writeln(article.titles.normalized.titleText); if (article.description != null) { buffer.writeln(article.description); } buffer.writeln(article.extract); buffer.writeln(); buffer.writeln('All results:'); } for (var result in results.results) { buffer.writeln('${result.title} - ${result.url}'); } return buffer.toString(); } on HttpException catch (e) { logger ..warning(e.message) ..warning(e.uri) ..info(usage); return e.message; } on FormatException catch (e) { logger ..warning(e.message) ..warning(e.source) ..info(usage); return e.message; } } }
Task 5: Create the GetArticleCommand command
#
Now, create the GetArticleCommand file and add the necessary code. The code is
similar to the previous SearchCommand, as it also uses a try/catch
block to handle
potential network or data errors.
Create a new file named cli/lib/src/commands/get_article.dart.
-
Add the following code to
get_article.dart.dartimport 'dart:async'; import 'dart:io'; import 'package:command_runner/command_runner.dart'; import 'package:logging/logging.dart'; import 'package:wikipedia/wikipedia.dart'; class GetArticleCommand extends Command<String> { GetArticleCommand({required this.logger}); final Logger logger; @override String get description => 'Read an article from Wikipedia'; @override String get name => 'article'; @override String get help => 'Gets an article by exact canonical wikipedia title.'; @override String get defaultValue => 'cat'; @override String get valueHelp => 'STRING'; @override FutureOr<String> run(ArgResults args) async { try { var title = args.commandArg ?? defaultValue; final List<Article> articles = await getArticleByTitle(title); // API returns a list of articles, but we only care about the closest hit. final article = articles.first; final buffer = StringBuffer('\n=== ${article.title.titleText} ===\n\n'); buffer.write(article.extract.split(' ').take(500).join(' ')); return buffer.toString(); } on HttpException catch (e) { logger ..warning(e.message) ..warning(e.uri) ..info(usage); return e.message; } on FormatException catch (e) { logger ..warning(e.message) ..warning(e.source) ..info(usage); return e.message; } } }Review the code you've just added. The
SearchCommandandGetArticleCommandnow:- Import the necessary packages like
command_runner,logging, andwikipediato use their classes and functions. - Accept a
Loggerinstance through their constructor. This is a common pattern called dependency injection, which allows the command to log events without needing to create its own logger. - Implement a
runmethod that defines the command's logic. This method calls the appropriate wikipedia API and formats the output. - Include
try/catchblocks to gracefully handle network errors (HttpException) and data parsing errors (FormatException), logging them for debugging.
- Import the necessary packages like
Task 6: Run the application and check the logs
#Now that you've added logging to your application, run it and check the log file to see the results.
-
Run the application with a command that might produce an error. For example, try searching for an article that doesn't exist or that causes a
FormatException.bashdart run bin/cli.dart search blahblahblahblah -
Check the
logsdirectory in your project. You should see a file with the current date and the nameerrors.txt. -
Open the log file and verify that the error message is logged.
[2025-02-20 16:23:45.678 - errors] WARNING: HttpException: HttpException: , uri = https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=blahblahblahblah [2025-02-20 16:23:45.678 - errors] INFO: Usage: dart bin/cli.dart <command> [commandArg?] [...options?]
Review
#In this lesson, you learned:
- How to add the
loggingpackage to your project. - How to create a
Loggerinstance and configure it to write to a file. - How to log errors and warnings to a file for later inspection.
- The importance of logging for debugging and monitoring your application.
Quiz
#Check your understanding
1 / 3logging package in Dart?
-
To provide a way to record events and errors in your application.
That's right!
The
loggingpackage provides a flexible system for recording events, warnings, errors, and other messages during application execution. -
To handle HTTP requests.
Not quite.
HTTP requests are handled by
package:http. Theloggingpackage serves a different purpose. -
To manage dependencies in your project.
Not quite.
Dependencies are managed in
pubspec.yaml. Theloggingpackage is about runtime behavior, not project setup. -
To create a command-line interface.
Not quite.
CLI creation uses packages like
argsor custom code. Theloggingpackage helps with a different aspect of applications.
hierarchicalLoggingEnabled = true; line do?
-
It enables a logger to capture events from child loggers.
That's right!
With hierarchical logging enabled, parent loggers can receive and process events from their child loggers.
-
It enables logging to a hierarchical file system.
Not quite.
This setting doesn't affect file systems or where logs are stored. "Hierarchical" refers to something else entirely.
-
It disables logging to the console.
Not quite.
Console output is controlled by listeners, not this setting. This setting affects how loggers interact with each other.
-
It enables logging of HTTP requests.
Not quite.
HTTP logging requires specific configuration in your HTTP code. This setting affects the logging system's internal structure.
logger.severe(), logger.warning(), and logger.info(). Why use different log levels instead of just print()?
-
You can filter logs by severity, showing only warnings and errors in production while seeing everything during development.
That's right!
Log levels let you set a threshold. In production, you might only log warnings and above, while in development you see info and debug messages too.
-
Different levels use different colors in the console.
Not quite.
Colors are a presentation choice, not the core reason. The real benefit is more fundamental than appearance.
-
Each level writes to a different file automatically.
Not quite.
File destinations must be configured separately. Levels don't automatically determine where logs are stored.
-
Using levels is required by Dart.
print()doesn't work in production.Not quite.
print()works everywhere in Dart. Levels are optional but provide significant advantages thatprint()can't offer.
Next lesson
#
Congratulations! You've now completed all the core chapters of the Dart Getting
Started tutorial. As a bonus, you can learn how to make your application into a
server using package:shelf in the next chapter.
Unless stated otherwise, the documentation on this site reflects Dart 3.10.3. Page last updated on 2025-12-21. View source or report an issue.