๐Ÿ“ Lesson 12: Logging & Debugging

โฑ 25-35 min ๐Ÿ“Š Intermediate ๐Ÿ“– In-depth tutorial ๐Ÿ”— Official Source
1

Why Logging? โ€” Beyond print()

๐Ÿง  print() is fine for learning. Logging is what professionals use in production.

The print() function has no concept of severity, no way to filter output, and can't write to files. The logging package solves all of these problems with log levels, hierarchical loggers, and configurable output destinations.

โŒ Using print()

print('User logged in');
print('ERROR: Connection failed');
print('DEBUG: Query took 23ms');
// All look the same!
// Can't filter by severity
// Can't write to file
// Can't disable debug in production

๐Ÿ˜ฐ No structure, no filtering, no persistence

โœ… Using the logging Package

Logger.info('User logged in');
Logger.severe('Connection failed');
Logger.fine('Query took 23ms');
// Severity levels โ€” filterable!
// Can write to console AND file
// Set level to WARNING in production
// Hierarchical: module-level control

๐Ÿ˜ Structured, filterable, persistent, professional

2

Log Levels โ€” From ALL to SHOUT

๐Ÿง  Log levels let you control how much detail appears based on the environment

In development, you might want ALL messages. In production, you probably only want WARNING and above. The logging package defines 9 levels, each with a numeric value. Setting a level means messages below that value are filtered out.

LevelValueUse CaseProduction?
ALL0Show everything โ€” no filteringโŒ Dev only
FINEST300Highly detailed trace โ€” every function callโŒ
FINER400Detailed trace โ€” loop iterationsโŒ
FINE500Debug information โ€” variable valuesโŒ
CONFIG700Configuration details โ€” settings loadedโš ๏ธ Maybe
INFO800Normal operations โ€” user logged in, file savedโœ… Yes
WARNING900Potential problems โ€” low disk space, slow queryโœ… Yes
SEVERE1000Errors โ€” operation failed, connection lostโœ… Yes
SHOUT1200Critical โ€” system crash, data lossโœ… Always
๐Ÿ“Œ The Rule of Thumb: Use Level.INFO for normal events, Level.WARNING for things that might become problems, and Level.SEVERE for actual errors. In production, set your minimum level to Level.WARNING to avoid drowning in debug noise.

Setting Up the Logger with a Level

import 'package:logging/logging.dart';

void main() {
  // 1. Create a logger with a name
  final logger = Logger('MyApp');
  
  // 2. Listen for log records
  logger.onRecord.listen((record) {
    print('[\${record.level.name}] \${record.time}: \${record.message}');
  });
  
  // 3. Set the minimum level
  logger.level = Level.WARNING;
  
  // These will NOT appear (below WARNING)
  logger.info('App started');       // Filtered out
  logger.fine('Debug details');     // Filtered out
  
  // These WILL appear (WARNING and above)
  logger.warning('Disk space low');   // โœ… Shown
  logger.severe('Connection failed');// โœ… Shown
}

๐Ÿ“ Breakdown

1 Import the logging package
5 Create Logger with a name for identification
8-10 Listen to log records โ€” decide what to do
9 record.level, record.time, record.message
13 Set minimum level โ€” everything below is ignored
16-17 .info() and .fine() are below WARNING โ†’ filtered
20-21 .warning() and .severe() are at/above โ†’ shown

๐Ÿงช Simulate Log Level Filtering

[INFO] 10:30:00 โ€” Application started [CONFIG] 10:30:01 โ€” Config loaded from ./config.yaml [INFO] 10:30:02 โ€” User 'alice' logged in [FINE] 10:30:03 โ€” Database query took 23ms [WARNING] 10:30:04 โ€” Disk space below 20% [INFO] 10:30:05 โ€” File saved successfully [SEVERE] 10:30:06 โ€” Connection to API failed [FINE] 10:30:07 โ€” Cache hit ratio: 94%
3

The Official Tutorial Example โ€” Wikipedia Logger

๐Ÿ“– This example comes from the official Dart logging tutorial

The official tutorial shows how to add logging to the Wikipedia CLI app from the previous lesson. Below is the complete example with explanations.

๐Ÿง  The official tutorial demonstrates replacing print() statements with proper logging in a real CLI app

The example shows a WikipediaClient class that uses logging instead of print statements, with different log levels for different types of messages.

The Official Example โ€” Wikipedia Client with Logging

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';

class WikipediaClient {
  final Logger logger;
  
  WikipediaClient({required this.logger});
  
  Future<Map> fetchArticle(String title) async {
    logger.info('Fetching article: \$title');
    
    final url = Uri.parse(
      'https://en.wikipedia.org/api/rest_v1/page/summary/\$title'
    );
    
    try {
      final response = await http.get(url);
      
      if (response.statusCode == 200) {
        logger.fine('Received 200 OK for \$title');
        return jsonDecode(response.body);
      } else {
        logger.warning('HTTP \${response.statusCode} for \$title');
        throw Exception('Failed to load article');
      }
    } catch (e) {
      logger.severe('Error fetching \$title: \$e');
      rethrow;
    }
  }
}

๐Ÿ“ What This Code Does

3 Import the logging package
6-8 Logger injected via constructor (DI pattern)
11 logger.info() โ€” normal operation logging
19 logger.fine() โ€” debug detail (filtered in prod)
22 logger.warning() โ€” non-200 status codes
26 logger.severe() โ€” caught exceptions
27 rethrow โ€” log AND propagate the error
8 DI allows testing with mock loggers
๐Ÿ”‘ Key Pattern โ€” Log AND Rethrow: The catch block logs the error at SEVERE level, then uses rethrow to pass it up. This means the error is recorded in the log AND the calling code can still handle it. This is the professional pattern for error logging.
4

Hierarchical Loggers โ€” Module-Level Control

๐Ÿง  Logger names use dot notation to create parent-child relationships

A logger named 'MyApp' is the parent of 'MyApp.Network' and 'MyApp.Database'. When you enable hierarchicalLoggingEnabled, setting a level on a parent affects all children. Each child can also have its own level.

Hierarchical Logger Setup โ€” Line by Line

import 'package:logging/logging.dart';

void main() {
  // 1. Enable hierarchical logging
  hierarchicalLoggingEnabled = true;
  
  // 2. Create parent logger
  final rootLogger = Logger('MyApp');
  rootLogger.level = Level.WARNING;
  
  // 3. Create child loggers (inherit from parent)
  final networkLogger = Logger('MyApp.Network');
  networkLogger.level = Level.FINE; // Override: more verbose
  
  final dbLogger = Logger('MyApp.Database');
  // Inherits WARNING from parent (no override)
  
  // Listen to ALL loggers at once
  Logger.root.onRecord.listen((record) {
    print('[\${record.loggerName}] \${record.message}');
  });
}

๐Ÿ“ How It Works

5 Must enable before creating loggers
8-9 Parent: only WARNING and above
12-13 Child: overrides with FINE for more detail
15-16 Child: inherits WARNING from parent
19-21 Logger.root catches ALL loggers
20 record.loggerName shows which logger
๐ŸŒณ MyApp
Level: WARNING
โ†’ children โ†’
๐ŸŒฟ MyApp.Network
Level: FINE (overridden)
๐ŸŒฟ MyApp.Database
Level: WARNING (inherited)
5

Writing Logs to Files

๐Ÿง  Console output disappears. File logs persist for debugging and auditing.

In the onRecord listener, you can write to both the console AND a file. Use FileMode.append to add to existing logs rather than overwriting them. Include timestamps for chronological tracking.

Dual Output: Console + File

import 'dart:io';
import 'package:logging/logging.dart';

void main() {
  final logger = Logger('MyApp');
  final logFile = File('./logs/app.log');
  
  // Ensure log directory exists
  logFile.createSync(recursive: true);
  
  logger.onRecord.listen((record) {
    final formatted = '[\${record.time}] \${record.level.name}: \${record.message}';
    
    // 1. Always write to file
    logFile.writeAsStringSync(
      '\$formatted\n',
      mode: FileMode.append,
    );
    
    // 2. Only print to console for WARNING and above
    if (record.level >= Level.WARNING) {
      print(formatted);
    }
  });
}
๐Ÿ“Œ Production Strategy: Write ALL log levels to file (for post-mortem debugging), but only show WARNING and above in the console (so operators see only what matters). This gives you both comprehensive records AND a clean terminal.
6

Dependency Injection โ€” Passing Loggers to Services

๐Ÿง  Never use global loggers. Pass loggers through constructors.

Dependency Injection (DI) means each class receives its logger from the outside. This makes testing easy (you can inject a mock logger) and keeps your code modular. Each service can have its own logger with its own name and level.

The DI Pattern for Loggers

// main.dart โ€” Create loggers and inject them
void main() {
  hierarchicalLoggingEnabled = true;
  
  // Create named loggers for each component
  final searchCmd = SearchCommand(
    logger: Logger('MyApp.Search'),
  );
  
  final fetchCmd = FetchCommand(
    logger: Logger('MyApp.Fetch'),
  );
}

// SearchCommand โ€” receives logger via constructor
class SearchCommand {
  final Logger logger;
  
  SearchCommand({required this.logger});
  
  Future<void> execute(String query) async {
    logger.info('Searching for: \$query');
    // ... search logic
    logger.fine('Search completed in 150ms');
  }
}
๐Ÿ  main()
Creates loggers
โ†’
๐Ÿ” SearchCommand
Logger('MyApp.Search')
โ†’
๐Ÿ“„ FetchCommand
Logger('MyApp.Fetch')

๐ŸŽฏ Logging Quiz

Score: 0/5 | Hints: 3

Loading question...

๐Ÿš€

Practice Projects

Apply what you've learned by building these hands-on projects.

๐Ÿ“ฆ Project 1 ๐Ÿ“ฆ Project 2
โ† Previous Lesson
Final Project โ†’