Saltar al contenido principal

Agrega logging para depuración y monitoreo

Aprende cómo agregar logging a tu aplicación Dart para ayudar con la depuración y el monitoreo.

En este capítulo, aprenderás cómo agregar logging a tu aplicación Dart. El logging es una herramienta fundamental para la depuración, el monitoreo y la comprensión del comportamiento de tu aplicación en diferentes entornos.

Qué lograrás

Agregar y aprender sobre el paquete logging
Configurar niveles de registro (log levels) y salida a archivos
Integrar el logging en tus comandos de CLI

Prerrequisitos

#

Antes de comenzar este capítulo, asegúrate de:

  • Haber completado el Capítulo 11 y tener un entorno de desarrollo de Dart en funcionamiento con el proyecto dartpedia.
  • Entender los conceptos básicos de la depuración y por qué es importante realizar un seguimiento de los errores y eventos en tu aplicación.

Tareas

#

En este capítulo, agregarás logging a la aplicación de CLI dartpedia para ayudar a rastrear errores y monitorear su comportamiento. Esto implicará agregar el paquete logging, crear una instancia de Logger y escribir mensajes de log en un archivo.

Tarea 1: Agregar el paquete logging

#

Primero, agrega el paquete logging a las dependencias de tu proyecto.

  1. Abre el archivo cli/pubspec.yaml.

  2. Localiza la sección dependencies.

  3. Agrega el paquete logging a tus dependencias:

    yaml
    dependencies:
      http: ^1.3.0
      command_runner:
        path: ../command_runner
      wikipedia:
        path: ../wikipedia
      # Add the following line
      logging: ^1.2.0
    
  4. Ejecuta dart pub get en el directorio cli para obtener la nueva dependencia.

Tarea 2: Crear un logger

#

A continuación, crea una instancia de Logger y configúrala para escribir mensajes de log en un archivo. Esto implica crear un nuevo archivo para el logger y configurar las importaciones necesarias.

  1. Crea un nuevo archivo llamado cli/lib/src/logger.dart.

  2. Agrega las importaciones necesarias y define la función initFileLogger.

    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. Agrega el código para buscar el directorio raíz del proyecto, crea un directorio logs si no existe uno, y crea un archivo de log único.

    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. Configura el nivel del logger y establece un listener para escribir mensajes de log en el archivo.

    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;
    }
    

    Este código hace lo siguiente:

    • Habilita el registro jerárquico usando hierarchicalLoggingEnabled = true.
    • Crea una instancia de Logger con el nombre proporcionado.
    • Obtiene el directorio del proyecto a partir de Platform.script.path.
    • Crea un directorio logs si no existe.
    • Crea un archivo de log con la fecha actual y el nombre del logger.
    • Establece el nivel del logger en Level.ALL, lo que significa que registrará todos los mensajes. Esto es útil para el desarrollo y la depuración, pero probablemente querrás usar un nivel más restrictivo como Level.INFO o Level.WARNING en producción.
    • Escucha los registros de log y los escribe en el archivo de log.
  5. Crea un nuevo archivo llamado cli/lib/cli.dart y exporta logger.dart. Esto hace que initFileLogger esté disponible para otras partes de tu aplicación.

    cli/lib/cli.dart
    dart
     export 'src/commands/get_article.dart';
     export 'src/commands/search.dart';
     export 'src/logger.dart';
    

Tarea 3: Usar el logger en cli.dart

#

Ahora, usa la función initFileLogger en cli/bin/cli.dart para crear una instancia de logger y registrar mensajes en un archivo.

  1. Abre el archivo cli/bin/cli.dart.

  2. Agrega la importación para el logger:

    cli/bin/cli.dart
    dart
    import 'package:cli/cli.dart';
    import 'package:command_runner/command_runner.dart';
    
  3. Modifica la función main para inicializar el logger y pasarlo a los comandos:

    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);
    }
    

    Este código hace lo siguiente:

    • Inicializa una instancia de Logger usando initFileLogger('errors').
    • Pasa la instancia del logger a CommandRunner y a los comandos individuales.

Tarea 4: Crear el comando SearchCommand

#

La funcionalidad principal de la CLI reside en sus comandos. Crea los archivos SearchCommand y GetArticleCommand y agrega el código necesario, incluyendo el logging y el manejo de errores.

  1. Crea un nuevo archivo llamado /cli/lib/src/commands/search.dart.

  2. Agrega las importaciones y una estructura de clase básica. Esta clase SearchCommand extiende de Command, y su constructor acepta una instancia de Logger. Aceptar el logger en el constructor es un patrón común llamado inyección de dependencias, que permite al comando registrar eventos sin necesidad de crear su propio logger.

    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});
    
      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 {
        // The rest of the function will be added below.
        // ...
      }
    }
    
  3. Ahora, agrega la lógica principal al método run. Este código verifica si hay un argumento válido, llama a la función search() del paquete wikipedia, da formato a los resultados y devuelve los resultados como una cadena.

    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});
    
      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');
        final SearchResults results = await search(args.commandArg!);
    
        for (var result in results.results) {
          buffer.writeln('${result.title} - ${result.url}');
        }
        return buffer.toString();
      }
    }
    
  4. A continuación, agrega la característica "Voy a tener suerte" añadiendo una bandera (flag) al constructor. Luego, en el método run, agrega la lógica para verificar si la bandera está establecida y, de ser así, obtener el resumen del primer resultado de búsqueda.

    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');
        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();
      }
    }
    
  5. Finalmente, envuelve la lógica principal en un bloque try/catch. Esto te permite manejar posibles excepciones que podrían surgir por problemas de red o de formato de datos. Usarás el logger inyectado para registrar estos errores en el archivo de log.

    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;
        }
      }
    }
    

Tarea 5: Crear el comando GetArticleCommand

#

Ahora, crea el archivo GetArticleCommand y agrega el código necesario. El código es similar al del anterior SearchCommand, ya que también utiliza un bloque try/catch para manejar posibles errores de red o de datos.

  1. Crea un nuevo archivo llamado cli/lib/src/commands/get_article.dart.

  2. Agrega el siguiente código a 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;
        }
      }
    }
    

    Revisa el código que acabas de agregar. Los comandos SearchCommand y GetArticleCommand ahora:

    • Importan los paquetes necesarios como command_runner, logging y wikipedia para usar sus clases y funciones.
    • Aceptan una instancia de Logger a través de su constructor. Este es un patrón común llamado inyección de dependencias, que permite al comando registrar eventos sin necesidad de crear su propio logger.
    • Implementan un método run que define la lógica del comando. Este método llama a la API de Wikipedia correspondiente y da formato a la salida.
    • Incluyen bloques try/catch para manejar de forma elegante errores de red (HttpException) y errores de análisis de datos (FormatException), registrándolos para depuración.

Tarea 6: Ejecutar la aplicación y verificar los logs

#

Ahora que has agregado logging a tu aplicación, ejecútala y verifica el archivo de log para ver los resultados.

  1. Ejecuta la aplicación con un comando que pueda producir un error. For ejemplo, intenta buscar un artículo que no exista o que cause una FormatException.

    bash
    dart run bin/cli.dart search blahblahblahblah
    
  2. Verifica el directorio logs en tu proyecto. Deberías ver un archivo con la fecha actual y el nombre errors.txt.

  3. Abre el archivo de log y verifica que el mensaje de error esté registrado.

    [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?]
    

Revisión

#

Qué lograste

Aquí tienes un resumen de lo que construiste y aprendiste en esta lección.
Agregaste y aprendiste sobre el paquete logging

Agregaste logging a tus dependencias y lo importaste en tu CLI. El paquete proporciona las clases Logger, Level y LogRecord para un registro estructurado con niveles de severidad configurables.

Configuraste niveles de registro y salida a archivos

Creaste initFileLogger() para configurar el registro jerárquico en archivos con marca de tiempo en el directorio logs/. Esto te permite revisar el comportamiento de la aplicación a posteriori.

Integraste el logging en tus comandos de CLI

Pasaste el logger a los comandos mediante inyección de dependencias, luego usaste logger.warning(), logger.severe() y logger.info() para registrar errores de diferentes niveles junto con el contexto relevante. Esta configuración te ayuda a depurar y filtrar problemas más fácilmente tanto en desarrollo como en producción.

Cuestionario

#

Pon a prueba tu comprensión

1 / 3
What is the purpose of the logging package in Dart?
  1. Para gestionar dependencias en tu proyecto.

    No exactamente.

    Las dependencias se gestionan en pubspec.yaml. El paquete logging trata sobre el comportamiento en tiempo de ejecución, no sobre la configuración del proyecto.

  2. Para proporcionar una forma de registrar eventos y errores en tu aplicación.

    ¡Correcto!

    El paquete logging proporciona un sistema flexible para registrar eventos, advertencias, errores y otros mensajes durante la ejecución de la aplicación.

  3. Para manejar solicitudes HTTP.

    No exactamente.

    Las solicitudes HTTP son manejadas por package:http. El paquete logging sirve para un propósito diferente.

  4. Para crear una interfaz de línea de comandos.

    No exactamente.

    La creación de CLI utiliza paquetes como args o código personalizado. El paquete logging ayuda con un aspecto diferente de las aplicaciones.

What does the hierarchicalLoggingEnabled = true; line do?
  1. Habilita el registro de solicitudes HTTP.

    No exactamente.

    El registro de HTTP requiere una configuración específica en tu código HTTP. Esta configuración afecta a la estructura interna del sistema de registro.

  2. Deshabilita el registro en la consola.

    No exactamente.

    La salida de la consola se controla mediante listeners, no mediante esta configuración. Esta configuración afecta a cómo interactúan los loggers entre sí.

  3. Permite a un logger capturar eventos de loggers hijos.

    ¡Correcto!

    Con el registro jerárquico habilitado, los loggers padres pueden recibir y procesar eventos de sus loggers hijos.

  4. Habilita el registro en un sistema de archivos jerárquico.

    No exactamente.

    Esta configuración no afecta a los sistemas de archivos ni a dónde se almacenan los logs. "Jerárquico" se refiere a algo completamente diferente.

This lesson uses logger.severe(), logger.warning(), and logger.info(). Why use different log levels instead of just print()?
  1. Puedes filtrar los logs por severidad, mostrando solo advertencias y errores en producción mientras ves todo durante el desarrollo.

    ¡Correcto!

    Los niveles de log te permiten establecer un umbral. En producción, puede que solo registres advertencias y niveles superiores, mientras que en desarrollo también verás mensajes de información y depuración.

  2. Diferentes niveles usan diferentes colores en la consola.

    No exactamente.

    Los colores son una elección de presentación, no la razón principal. El beneficio real es más fundamental que la apariencia.

  3. Cada nivel escribe en un archivo diferente automáticamente.

    No exactamente.

    Los destinos de los archivos se deben configurar por separado. Los niveles no determinan automáticamente dónde se almacenan los logs.

  4. El uso de niveles es requerido por Dart. print() no funciona en producción.

    No exactamente.

    print() funciona en todas partes en Dart. Los niveles son opcionales pero proporcionan ventajas significativas que print() no puede ofrecer.

Siguiente lección

#

¡Felicitaciones! Has completado todos los capítulos principales del tutorial de inicio de Dart. ¿Quieres seguir aprendiendo? Revisa el siguiente paso en la ruta de aprendizaje de Dart y Flutter en el sitio de Flutter.