Logging
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.
text[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
#Question 1: What is the purpose of the logging package in Dart?
- A) To handle HTTP requests.
- B) To manage dependencies in your project.
- C) To provide a way to record events and errors in your application.
- D) To create a command-line interface.
Question 2: What does the hierarchicalLoggingEnabled = true; line do?
- A) It enables logging to a hierarchical file system.
- B) It enables a logger to capture events from child loggers.
- C) It disables logging to the console.
- D) It enables logging of HTTP requests.
Question 3: What are some benefits of logging?
- A) Helps prevent run-time crashes
- B) Helps with debugging, monitoring, and understanding the behavior of your application.
- C) Improves performance
- D) Reduces the number of dependencies.
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.9.2. Page last updated on 2025-9-16. View source or report an issue.