Skip to main content

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

Add and learn about the logging package
Configure log levels and file output
Integrate logging into your CLI commands

Prerequisites

#

Before you begin this chapter, ensure you:

  • Have completed Chapter 12 and have a working Dart development environment with the dartpedia project.
  • 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.

  1. Open the cli/pubspec.yaml file.

  2. Locate the dependencies section.

  3. Add the wikipedia and logging packages to your dependencies:

    yaml
    dependencies:
      http: ^1.3.0
      command_runner:
        path: ../command_runner
      # Add the following lines
      wikipedia:
        path: ../wikipedia
      logging: ^1.2.0
    
  4. Run dart pub get in the cli directory 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.

  1. Create a new file called cli/lib/src/logger.dart.

  2. Add the necessary imports and define the initFileLogger function.

    cli/lib/src/logger.dart
    dart
    import '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;
    }
    
  3. Add the code to find the project's root directory, create a logs directory if one doesn't exist, and create a unique log file.

    dart
    Logger 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;
    }
    
  4. Configure the logger's level and set up a listener to write log messages to the file.

    dart
    Logger 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 initFileLogger function returns a configured Logger instance that appends timestamped records to a file in the logs/ directory:

    • logger.level = Level.ALL: Captures all messages regardless of severity during development. Production apps typically use higher thresholds like Level.INFO or Level.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.

  1. Create a new file named cli/lib/src/commands/search.dart.

  2. Add the imports and a basic class structure. This SearchCommand class extends Command, and its constructor accepts a Logger instance. 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.dart
    dart
    import '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 '';
      }
    }
    
  3. 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();
      }
    // ...
    
  4. Finally, wrap the main logic in a try/catch block. This allows you to handle potential exceptions that could arise from network issues or data formatting problems. You'll use the injected logger to record these errors to the log file.

    cli/lib/src/commands/search.dart
    dart
    import '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.

  1. Create a new file named cli/lib/src/commands/get_article.dart.

  2. Add the following code to get_article.dart:

    cli/lib/src/commands/get_article.dart
    dart
    import '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 SearchCommand and GetArticleCommand now:

    • Import the necessary packages like command_runner, logging, and wikipedia to use their classes and functions.
    • Accept a Logger instance 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 run method that defines the command's logic. This method calls the appropriate wikipedia API and formats the output.
    • Include try/catch blocks to gracefully handle network errors (HttpException) and data parsing errors (FormatException), logging them for debugging.

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.

  1. Open the cli/lib/cli.dart file. Replace its placeholder content with exports for your logger and commands:

    cli/lib/cli.dart
    dart
    export '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, and GetArticleCommand so that cli/bin/cli.dart can import them from package:cli/cli.dart.

  2. Open the cli/bin/cli.dart file.

  3. Add the imports for cli and command_runner:

    cli/bin/cli.dart
    dart
    import 'package:cli/cli.dart';
    import 'package:command_runner/command_runner.dart';
    
  4. Modify the main function to initialize the logger and register the commands with CommandRunner:

    cli/bin/cli.dart
    dart
    import '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 errors file logger, passes it to SearchCommand and GetArticleCommand, and registers all commands with CommandRunner.

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).

  1. Run the CLI application to search for an article:

    bash
    dart run bin/cli.dart search "Dart programming"
    

    You should see terminal output listing Wikipedia articles matching your search term.

  2. Try running the command with the --im-feeling-lucky flag:

    bash
    dart run bin/cli.dart search "Dart" --im-feeling-lucky
    
  3. Run the application without the required search argument to test error logging:

    bash
    dart run bin/cli.dart search
    

    You should see the error printed to the terminal:

    ArgumentException: Please include a search term
    
  4. Check the cli/logs directory in your project. You should see a file named with the current date, such as <year>_<month>_<day>_errors.txt.

  5. 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
    
  6. 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 / 3
What is the purpose of the logging package in Dart?
  1. To provide a way to record events and errors in your application.

    That's right!

    The logging package provides a flexible system for recording events, warnings, errors, and other messages during application execution.

  2. To handle HTTP requests.

    Not quite.

    HTTP requests are handled by package:http. The logging package serves a different purpose.

  3. To manage dependencies in your project.

    Not quite.

    Dependencies are managed in pubspec.yaml. The logging package is about runtime behavior, not project setup.

  4. To create a command-line interface.

    Not quite.

    CLI creation uses packages like args or custom code. The logging package helps with a different aspect of applications.

What does the hierarchicalLoggingEnabled = true; line do?
  1. 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.

  2. 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.

  3. 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.

  4. 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.

This lesson uses logger.severe(), logger.warning(), and logger.info(). Why use different log levels instead of just print()?
  1. 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.

  2. 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.

  3. 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.

  4. 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 that print() 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.