22 Serious Logging
I’ll take you through a comprehensive journey of the logging package, building from the foundation up to advanced configurations. Since you’re coming from Python, you’ll find many concepts familiar - the Dart logging package was actually inspired by Python’s logging module and Java’s java.util.logging.
22.1 Understanding the Core Concepts
Before we dive into code, let’s understand the mental model of how the logging package works. Imagine a tree structure where each logger is a node. At the root of this tree sits the root logger, and all other loggers are its descendants. This hierarchical structure allows for powerful configuration inheritance.
Logger.root
├── Logger('MyApp')
│ ├── Logger('MyApp.UI')
│ │ ├── Logger('MyApp.UI.HomePage')
│ │ └── Logger('MyApp.UI.SettingsPage')
│ └── Logger('MyApp.Services')
│ ├── Logger('MyApp.Services.API')
│ └── Logger('MyApp.Services.Database')
└── Logger('ThirdPartyLib')
22.2 Step 1: Basic Initialization
Let’s start with the simplest possible setup and gradually add complexity:
import 'package:logging/logging.dart';
void main() {
// Step 1: Enable logging globally
// By default, logging is set to Level.INFO and above
.root.level = Level.ALL; // This enables all log levels
Logger
// Step 2: Set up a listener to actually do something with the logs
// Without this, logs are generated but go nowhere!
.root.onRecord.listen((LogRecord record) {
Logger'${record.level.name}: ${record.time}: ${record.message}');
print(});
// Step 3: Create a logger and use it
final logger = Logger('MyFirstLogger');
.info('Hello from logging!');
logger
runApp();}
The key insight here is that logging has two parts: generating log records and consuming them. The logger generates records, but without a listener, nothing happens with them.
22.3 Step 2: Understanding Logger Hierarchy
One of the most powerful features is the hierarchical nature of loggers. Let’s explore this:
void demonstrateHierarchy() {
// Configure the root logger
.root.level = Level.INFO;
Logger.root.onRecord.listen((record) {
Logger'[${record.level.name}] ${record.loggerName}: ${record.message}');
print(});
// Create a parent logger
final appLogger = Logger('MyApp');
// Create child loggers using dot notation
final uiLogger = Logger('MyApp.UI');
final apiLogger = Logger('MyApp.API');
final dbLogger = Logger('MyApp.Database');
// All these loggers inherit configuration from their parents
.info('App starting'); // Will print
appLogger.info('UI initialized'); // Will print
uiLogger.fine('API details'); // Won't print (FINE < INFO)
apiLogger.warning('DB connection slow'); // Will print
dbLogger
// You can override levels for specific loggers
.level = Level.ALL; // Now this logger logs everything
apiLogger.fine('API details'); // Now this will print!
apiLogger}
This hierarchy allows you to control logging granularity. For example, you might want detailed logs from your API module during debugging but only warnings from the UI module.
22.4 Step 3: Comprehensive Configuration
Let’s create a robust logging configuration that you might use in a real application:
import 'package:logging/logging.dart';
import 'package:flutter/foundation.dart';
import 'dart:io';
class LoggingConfig {
static final _logFile = File('app_logs.txt');
static IOSink? _logSink;
/// Initialize logging based on the current environment
static void initialize({
bool verbose = false,
bool logToFile = false,
}) {
// Determine base level based on build mode
Level baseLevel;if (kDebugMode) {
= verbose ? Level.ALL : Level.INFO;
baseLevel } else if (kProfileMode) {
= Level.INFO;
baseLevel } else {
// Release mode - only log warnings and above
= Level.WARNING;
baseLevel }
.root.level = baseLevel;
Logger
// Clear any existing listeners to avoid duplicates
.root.clearListeners();
Logger
// Set up console output
.root.onRecord.listen((record) {
Logger, logToFile: logToFile);
_handleLogRecord(record});
// Configure specific loggers if needed
_configureSpecificLoggers();}
static void _handleLogRecord(LogRecord record, {bool logToFile = false}) {
// Format the timestamp
final time = record.time.toIso8601String().substring(11, 23);
// Color coding for different levels (works in most terminals)
final levelColor = _getLevelColor(record.level);
final resetColor = '\x1B[0m';
// Format the log message
final formattedMessage = _formatMessage(
: time,
time: record.level,
level: record.loggerName,
logger: record.message,
message: record.error,
error: record.stackTrace,
stackTrace
);
// Output to console with color
if (kDebugMode) {
'$levelColor$formattedMessage$resetColor');
print(} else {
// In release, use stdout/stderr appropriately
if (record.level >= Level.SEVERE) {
.writeln(formattedMessage);
stderr} else {
.writeln(formattedMessage);
stdout}
}
// Optionally write to file
if (logToFile) {
_writeToFile(formattedMessage);}
// In release mode, send errors to crash reporting
if (!kDebugMode && record.level >= Level.SEVERE) {
_sendToCrashReporting(record);}
}
static String _formatMessage({
String time,
required ,
required Level levelString logger,
required String message,
required Object? error,
? stackTrace,
StackTrace}) {
final buffer = StringBuffer();
// Basic format: TIME [LEVEL] LoggerName: Message
.write('$time [${level.name.padRight(7)}] ');
buffer.write('${logger.padRight(20)}: ');
buffer.write(message);
buffer
// Add error information if present
if (error != null) {
.write('\n Error: $error');
buffer}
// Add stack trace if present (indented for readability)
if (stackTrace != null) {
.write('\n Stack trace:\n');
bufferfinal stackLines = stackTrace.toString().split('\n');
for (final line in stackLines.take(10)) { // Limit stack trace length
.write(' $line\n');
buffer}
if (stackLines.length > 10) {
.write(' ... ${stackLines.length - 10} more lines ...\n');
buffer}
}
return buffer.toString();
}
static String _getLevelColor(Level level) {
if (level >= Level.SEVERE) return '\x1B[31m'; // Red
if (level >= Level.WARNING) return '\x1B[33m'; // Yellow
if (level >= Level.INFO) return '\x1B[32m'; // Green
if (level >= Level.CONFIG) return '\x1B[36m'; // Cyan
return '\x1B[37m'; // White for debug levels
}
static void _configureSpecificLoggers() {
// Example: Make API logger more verbose during development
if (kDebugMode) {
'MyApp.API').level = Level.ALL;
Logger(}
// Example: Silence a chatty third-party library
'NoisyLibrary').level = Level.WARNING;
Logger(}
static void _writeToFile(String message) {
try {
??= _logFile.openWrite(mode: FileMode.append);
_logSink !.writeln(message);
_logSink} catch (e) {
// If file writing fails, don't crash the app
'Failed to write to log file: $e');
print(}
}
static void _sendToCrashReporting(LogRecord record) {
// This would integrate with your crash reporting service
// Example: Sentry, Crashlytics, etc.
// For now, we'll just print a message
'Would send to crash reporting: ${record.message}');
print(}
/// Clean up resources
static Future<void> dispose() async {
await _logSink?.flush();
await _logSink?.close();
}
}
22.5 Step 4: Creating a Logger Mixin for Easy Use
To make logging easier throughout your application, create a mixin that provides logging functionality:
/// Mixin that provides logging functionality to any class
mixin LoggingMixin {
/// Gets a logger named after the current class
get logger => Logger(runtimeType.toString());
Logger
/// Convenience methods that mirror the logger's methods
void logFinest(String message) => logger.finest(message);
void logFiner(String message) => logger.finer(message);
void logFine(String message) => logger.fine(message);
void logConfig(String message) => logger.config(message);
void logInfo(String message) => logger.info(message);
void logWarning(String message, [Object? error, StackTrace? stackTrace]) {
.warning(message, error, stackTrace);
logger}
void logSevere(String message, [Object? error, StackTrace? stackTrace]) {
.severe(message, error, stackTrace);
logger}
}
// Usage example
class UserService with LoggingMixin {
Future<User?> fetchUser(String id) async {
'Fetching user with id: $id');
logInfo(
try {
'Making API request to /users/$id');
logFine(// Simulate API call
await Future.delayed(Duration(seconds: 1));
if (id == 'invalid') {
throw Exception('User not found');
}
final user = User(id: id, name: 'John Doe');
'Successfully fetched user: ${user.name}');
logInfo(return user;
} catch (e, stackTrace) {
'Failed to fetch user $id', e, stackTrace);
logSevere(return null;
}
}
}
22.6 Step 5: Advanced Patterns and Best Practices
Let’s explore some advanced patterns that make logging more powerful:
/// A structured logging approach using custom log records
class StructuredLogger {
final Logger _logger;
final Map<String, dynamic> _context = {};
String name) : _logger = Logger(name);
StructuredLogger(
/// Add persistent context that will be included in all logs
void addContext(String key, dynamic value) {
= value;
_context[key] }
/// Log with additional structured data
void logStructured({
,
required Level levelString message,
required Map<String, dynamic>? data,
Object? error,
? stackTrace,
StackTrace}) {
// Combine persistent context with provided data
final fullData = {..._context, ...?data};
// Create a structured message
final structuredMessage = StringBuffer(message);
if (fullData.isNotEmpty) {
.write(' | ');
structuredMessage.forEach((key, value) {
fullData.write('$key=$value ');
structuredMessage});
}
.log(level, structuredMessage.toString(), error, stackTrace);
_logger}
// Convenience methods
void info(String message, {Map<String, dynamic>? data}) {
: Level.INFO, message: message, data: data);
logStructured(level}
void error(String message, Object error, StackTrace stackTrace,
{Map<String, dynamic>? data}) {
logStructured(: Level.SEVERE,
level: message,
message: data,
data: error,
error: stackTrace,
stackTrace
);}
}
// Usage of structured logging
void demonstrateStructuredLogging() {
final logger = StructuredLogger('PaymentService');
// Add persistent context
.addContext('service_version', '2.1.0');
logger.addContext('environment', 'production');
logger
// Log with additional data
.info('Processing payment', data: {
logger'user_id': '12345',
'amount': 99.99,
'currency': 'USD',
'payment_method': 'credit_card',
});
// This will output something like:
// Processing payment | service_version=2.1.0 environment=production user_id=12345 amount=99.99 currency=USD payment_method=credit_card
}
22.7 Step 6: Integration with Flutter
Here’s how to properly integrate logging into a Flutter application:
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
void main() {
// Initialize logging before running the app
.initialize(
LoggingConfig: true, // Set based on your needs
verbose: false, // Enable for debugging
logToFile
);
// Set up Flutter error handling
.onError = (FlutterErrorDetails details) {
FlutterErrorfinal logger = Logger('FlutterError');
.severe(
logger'Flutter error occurred',
.exception,
details.stack,
details
);};
runApp(MyApp());}
class MyApp extends StatefulWidget {
@override
=> _MyAppState();
_MyAppState createState() }
class _MyAppState extends State<MyApp> with LoggingMixin {
@override
void initState() {
super.initState();
'MyApp initialized');
logInfo(}
@override
{
Widget build(BuildContext context) return MaterialApp(
: 'Logging Demo',
title: LoggingDemoScreen(),
home
);}
@override
void dispose() {
'MyApp disposed');
logInfo(.dispose(); // Clean up logging resources
LoggingConfigsuper.dispose();
}
}
class LoggingDemoScreen extends StatelessWidget with LoggingMixin {
@override
{
Widget build(BuildContext context) return Scaffold(
: AppBar(title: Text('Logging Demo')),
appBar: Center(
body: Column(
child: MainAxisAlignment.center,
mainAxisAlignment: [
children
ElevatedButton(: () => _simulateUserAction(),
onPressed: Text('Simulate User Action'),
child,
): 20),
SizedBox(height
ElevatedButton(: () => _simulateError(),
onPressed: Text('Simulate Error'),
child: ElevatedButton.styleFrom(
style: Colors.red,
backgroundColor,
),
),
],
),
)
);}
void _simulateUserAction() {
'User pressed action button');
logInfo('Starting simulated action...');
logFine(
// Simulate some work
Future.delayed(Duration(seconds: 1), () {
'Action completed successfully');
logInfo(});
}
void _simulateError() {
'User pressed error simulation button');
logWarning(
try {
throw StateError('This is a simulated error for demonstration');
} catch (e, stackTrace) {
'Simulated error occurred', e, stackTrace);
logSevere(}
}
}
22.8 Testing Your Logging Configuration
It’s important to test that your logging is working correctly. Here’s a simple test utility:
void testLoggingConfiguration() {
final testLogger = Logger('LoggingTest');
'\n=== Testing Logging Configuration ===\n');
print(
// Test each level
.finest('This is a FINEST message - most detailed');
testLogger.finer('This is a FINER message - quite detailed');
testLogger.fine('This is a FINE message - debugging info');
testLogger.config('This is a CONFIG message - configuration');
testLogger.info('This is an INFO message - general info');
testLogger.warning('This is a WARNING message - potential issue');
testLogger.severe('This is a SEVERE message - serious problem');
testLogger.shout('This is a SHOUT message - highest priority');
testLogger
// Test error logging
try {
throw FormatException('Test exception');
} catch (e, stack) {
.severe('Caught an exception', e, stack);
testLogger}
'\n=== Logging Test Complete ===\n');
print(}
22.9 Key Takeaways
The logging package gives you production-grade logging capabilities. Remember these key points:
- Always set up a listener - Without
onRecord.listen()
, your logs go nowhere - Use the hierarchy - Name your loggers with dots to create a logical structure
- Configure by environment - Different log levels for debug, profile, and release
- Structure your logs - Include context and metadata for better debugging
- Don’t log sensitive data - Be careful about logging passwords, tokens, or personal information
- Clean up resources - Close file handles and flush buffers when done
The power of this logging system becomes apparent when you’re debugging production issues. With proper logging, you can trace exactly what happened, when it happened, and in what context - turning mysterious bugs into solvable problems.