๐ Lesson 12: Logging & Debugging
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
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.
| Level | Value | Use Case | Production? |
|---|---|---|---|
| ALL | 0 | Show everything โ no filtering | โ Dev only |
| FINEST | 300 | Highly detailed trace โ every function call | โ |
| FINER | 400 | Detailed trace โ loop iterations | โ |
| FINE | 500 | Debug information โ variable values | โ |
| CONFIG | 700 | Configuration details โ settings loaded | โ ๏ธ Maybe |
| INFO | 800 | Normal operations โ user logged in, file saved | โ Yes |
| WARNING | 900 | Potential problems โ low disk space, slow query | โ Yes |
| SEVERE | 1000 | Errors โ operation failed, connection lost | โ Yes |
| SHOUT | 1200 | Critical โ system crash, data loss | โ Always |
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
record.level, record.time, record.message.info() and .fine() are below WARNING โ filtered.warning() and .severe() are at/above โ shown๐งช Simulate Log Level Filtering
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
logger.info() โ normal operation logginglogger.fine() โ debug detail (filtered in prod)logger.warning() โ non-200 status codeslogger.severe() โ caught exceptionsrethrow โ log AND propagate the errorrethrow 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.
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
Logger.root catches ALL loggersrecord.loggerName shows which loggerLevel: WARNING
Level: FINE (overridden)
Level: WARNING (inherited)
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);
}
});
}
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');
}
}
Creates loggers
Logger('MyApp.Search')
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.