Add logging for debugging and monitoring
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.
What you'll accomplish
Prerequisites
#Before you begin this chapter, ensure you:
-
Have completed Chapter 12 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 complete the dartpedia CLI application by
integrating the wikipedia package commands and adding logging to
help track errors and monitor its behavior.
This involves adding package dependencies,
creating the CLI commands, configuring a Logger instance,
and running your complete application.
Task 1: Add dependencies to the cli package
#
First, add the wikipedia package (which you built in the previous chapters)
and the logging package to your CLI project's dependencies.
Open the
cli/pubspec.yamlfile.Locate the
dependenciessection.-
Add the
wikipediaandloggingpackages to your dependencies:yamldependencies: http: ^1.3.0 command_runner: path: ../command_runner # Add the following lines wikipedia: path: ../wikipedia logging: ^1.2.0 -
Run
dart pub getin theclidirectory to fetch the new dependencies.
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 scriptFile = File(Platform.script.toFilePath()); final projectDir = scriptFile.parent.parent.path; // 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 scriptFile = File(Platform.script.toFilePath()); final projectDir = scriptFile.parent.parent.path; 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; }The
initFileLoggerfunction returns a configuredLoggerinstance that appends timestamped records to a file in thelogs/directory:logger.level = Level.ALL: Captures all messages regardless of severity during development. Production apps typically use higher thresholds likeLevel.INFOorLevel.WARNING.logger.onRecord.listen(...): Subscribes to the stream of log events, formatting each entry with its timestamp, logger name, and severity level before writing to disk.
Task 3: 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 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, 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.cli/lib/src/commands/search.dartdartimport '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 { 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 bool get requiresArgument => true; @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 { // Command logic will be added below. // ... return ''; } } -
Implement the command logic to search Wikipedia and format the results.
dart// ... @override FutureOr<String> run(ArgResults args) async { if (requiresArgument && (args.commandArg == null || args.commandArg!.isEmpty)) { throw ArgumentException('Please include a search term', name); } final buffer = StringBuffer('Search results:\n'); 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.cli/lib/src/commands/search.dartdartimport '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 { 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 bool get requiresArgument => true; @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)) { throw ArgumentException('Please include a search term', name); } final buffer = StringBuffer('Search results:\n'); 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 4: 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:cli/lib/src/commands/get_article.dartdartimport '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 { 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 5: Export commands and wire up cli.dart
#
Now, export the logger and commands from the cli library,
then wire them up in cli/bin/cli.dart to
create the complete CLI application.
-
Open the
cli/lib/cli.dartfile. Replace its placeholder content with exports for your logger and commands:cli/lib/cli.dartdartexport 'src/commands/get_article.dart'; export 'src/commands/search.dart'; export 'src/logger.dart';This file acts as the library's public interface, exporting
initFileLogger,SearchCommand, andGetArticleCommandso thatcli/bin/cli.dartcan import them frompackage:cli/cli.dart. Open the
cli/bin/cli.dartfile.-
Add the imports for
cliandcommand_runner:cli/bin/cli.dartdartimport 'package:cli/cli.dart'; import 'package:command_runner/command_runner.dart'; -
Modify the
mainfunction to initialize the logger and register the commands withCommandRunner: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( 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); print(error); } }, ) ..addCommand(HelpCommand()) ..addCommand(SearchCommand(logger: errorLogger)) ..addCommand(GetArticleCommand(logger: errorLogger)); app.run(arguments); }This setup initializes an
errorsfile logger, passes it toSearchCommandandGetArticleCommand, and registers all commands withCommandRunner.
Task 6: Run the application and check the logs
#
Now that you've implemented the CLI commands,
wired up CommandRunner in bin/cli.dart, and configured logging,
test your application from the terminal.
Make sure you run these commands from your cli directory
(/dartpedia/cli).
-
Run the CLI application to search for an article:
bashdart run bin/cli.dart search "Dart programming"You should see terminal output listing Wikipedia articles matching your search term.
-
Try running the command with the
--im-feeling-luckyflag:bashdart run bin/cli.dart search "Dart" --im-feeling-lucky -
Run the application without the required search argument to test error logging:
bashdart run bin/cli.dart searchYou should see the error printed to the terminal:
ArgumentException: Please include a search term -
Check the
cli/logsdirectory in your project. You should see a file named with the current date, such as<year>_<month>_<day>_errors.txt. -
Open the log file and verify that the error message is logged:
[2025-02-20 16:23:45.678 - errors] WARNING: ArgumentException: Please include a search term -
Verify that your workspace structure matches the completed project:
dartpedia/ ├── pubspec.yaml # Workspace root configuration ├── cli/ │ ├── bin/ │ │ └── cli.dart # Application entrypoint │ ├── lib/ │ │ ├── cli.dart # Library exports │ │ └── src/ │ │ ├── commands/ │ │ │ ├── get_article.dart │ │ │ └── search.dart │ │ └── logger.dart # Logging configuration │ ├── logs/ │ │ └── <date>_errors.txt # Generated error logs │ └── pubspec.yaml ├── command_runner/ │ ├── lib/ │ │ ├── command_runner.dart │ │ └── src/ │ │ ├── arguments.dart │ │ ├── command_runner_base.dart │ │ ├── console.dart │ │ ├── exceptions.dart │ │ └── help_command.dart │ └── pubspec.yaml └── wikipedia/ ├── lib/ │ ├── wikipedia.dart │ └── src/ │ ├── api/ │ │ ├── get_article.dart │ │ ├── search.dart │ │ └── summary.dart │ └── model/ │ ├── article.dart │ ├── search_results.dart │ ├── summary.dart │ └── title_set.dart └── pubspec.yaml
Review
#What you accomplished
Here's a summary of what you built and learned in this lesson.Added and learned about the logging package
You added logging to your dependencies and imported it into your CLI. The package provides
Logger, Level, and LogRecord classes for structured logging with configurable severity levels.
Configured log levels and file output
You created initFileLogger() to set up hierarchical logging to timestamped files in the
logs/ directory. This enables you to review application behavior after the fact.
Integrated logging into your CLI commands
You passed the logger to commands using dependency injection, then used logger.warning(),
logger.severe(), and logger.info() to record errors of different levels alongside relevant context. This setup helps you more easily debug and filter issues in both development and production.
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. Want to continue learning? Check out the next step in the Dart and Flutter learning pathway on the Flutter site.
Unless stated otherwise, the documentation on this site reflects Dart 3.13.3. Page last updated on 2026-09-15. View source or report an issue.