Logging
Comprehensive structured logging system with multiple channels, configurable handlers, and proper separation of concerns.
StructuredChannelsHandlersRotationContextJSON
Quick Start
Basic Logging
dart
// Create a logger instance
final logger = Logger();
// Basic logging methods
logger.debug('Detailed debug information');
logger.info('General application information');
logger.warning('Warning about potential issues');
logger.error('Error that occurred');
logger.critical('Critical system failure');
// Logging with different channels
logger.info('User action', channel: 'auth');
logger.error('Database error', channel: 'database');
// Close logger when done
logger.close();
💡 Note: Logger automatically handles different log levels and channels
⚡ Tip: Use structured context for better debugging and monitoring
Log Levels
dart
// Log level comparison
LogLevel current = LogLevel.info;
LogLevel message = LogLevel.warning;
// Check if message should be logged
if (message.isAtLeast(current)) {
print('Message will be logged');
}
// Parse from string
LogLevel level = LogLevel.fromString('error');
// Get string representations
print(LogLevel.debug.name); // 'debug'
print(LogLevel.debug.nameUpper); // 'DEBUG'
// Level hierarchy (increasing severity)
const levels = [
LogLevel.debug, // 0 - Most verbose
LogLevel.info, // 1
LogLevel.warning, // 2
LogLevel.error, // 3
LogLevel.critical, // 4 - Most severe
];
Log Level Hierarchy
DEBUG
Detailed info
INFO
General info
WARNING
Potential issues
ERROR
Application errors
CRITICAL
Critical failures
Structured Logging with Context
dart
// Simple context
logger.info('User logged in', context: {
'userId': 123,
'username': 'john_doe',
'ip': '192.168.1.100',
});
// Complex nested context
logger.debug('Payment processed', context: {
'transaction': {
'id': 'txn_123456',
'amount': 99.99,
'currency': 'USD',
'method': 'credit_card',
},
'user': {
'id': 456,
'email': 'user@example.com',
},
'metadata': {
'timestamp': DateTime.now().toIso8601String(),
'version': '1.2.3',
'environment': 'production',
},
});
// Error context
try {
processPayment(paymentData);
} catch (e) {
logger.error('Payment processing failed', context: {
'error': e.toString(),
'paymentId': paymentData.id,
'amount': paymentData.amount,
'userId': paymentData.userId,
});
}
// Performance context
logger.info('API request completed', context: {
'method': 'POST',
'endpoint': '/api/users',
'statusCode': 200,
'responseTime': 150, // milliseconds
'requestSize': 2048, // bytes
'responseSize': 1024, // bytes
});
Context Benefits
- • Better debugging with structured data
- • Easier log analysis and filtering
- • Consistent metadata across log entries
- • Support for complex objects and nested data
Multiple Channels
dart
// Set default channel
logger.setDefaultChannel('app');
// Log to default channel
logger.info('Application started');
// Log to specific channels
logger.info('User authentication successful', channel: 'auth');
logger.warning('Cache miss rate is high', channel: 'cache');
logger.error('Database connection timeout', channel: 'database');
logger.debug('API request details', channel: 'api');
// Add handlers to specific channels
logger.addHandler(ConsoleLogHandler(), channel: 'app');
logger.addHandler(FileLogHandler(filePath: 'logs/auth.log'), channel: 'auth');
logger.addHandler(FileLogHandler(filePath: 'logs/database.log'), channel: 'database');
// Channel-specific configuration
class PaymentService {
void processPayment(Payment payment) {
logger.info('Processing payment', channel: 'payment', context: {
'paymentId': payment.id,
'amount': payment.amount,
});
try {
// Payment processing logic
_chargeCard(payment);
logger.info('Payment successful', channel: 'payment', context: {
'paymentId': payment.id,
'transactionId': 'txn_123',
});
} catch (e) {
logger.error('Payment failed', channel: 'payment', context: {
'paymentId': payment.id,
'error': e.toString(),
});
throw e;
}
}
}
Channel Use Cases
- •
app
- General application logs - •
database
- Database operations and queries - •
api
- API requests and responses - •
auth
- Authentication and authorization - •
cache
- Cache operations and performance - •
payment
- Payment processing logs
Configuration
dart
// Load configuration from app config
final logger = Logger();
final config = AppConfig(); // Your config implementation
logger.loadFromConfig(config);
// Manual configuration
final logger = Logger(minimumLevel: LogLevel.warning);
logger.setDefaultChannel('app');
// Add handlers programmatically
logger.addHandler(ConsoleLogHandler(colorize: true));
logger.addHandler(FileLogHandler(
filePath: 'storage/logs/app.log',
formatJson: true,
rotateOnSize: true,
maxFileSizeBytes: 5 * 1024 * 1024, // 5MB
maxBackupCount: 5,
));
Configuration Structure
dart
// config/app.dart
Map<String, dynamic> config = {
'logging': {
'minimum_level': 'info', // debug, info, warning, error, critical
'default': 'app', // Default channel name
'handlers': {
'console': {
'enabled': true,
'colorize': true, // Colorize console output
},
'file': {
'enabled': true,
'path': 'storage/logs/app.log',
'format_json': true, // Use JSON format
'rotate_on_size': true, // Rotate when file reaches max size
'rotate_daily': false, // Rotate daily
'max_size': 5242880, // 5MB in bytes
'max_backups': 5, // Keep 5 backup files
},
},
},
};
Handlers
Console Handler
dart
// Basic console handler
final consoleHandler = ConsoleLogHandler();
logger.addHandler(consoleHandler);
// Colored console output (default)
final coloredHandler = ConsoleLogHandler(colorize: true);
logger.addHandler(coloredHandler);
// Disable colors for certain environments
final plainHandler = ConsoleLogHandler(colorize: false);
logger.addHandler(plainHandler);
// Color mapping:
// DEBUG: White
// INFO: Green
// WARNING: Yellow
// ERROR: Red
// CRITICAL: Magenta
File Handler
dart
// Basic file handler
final fileHandler = FileLogHandler(
filePath: 'storage/logs/app.log',
);
logger.addHandler(fileHandler);
// Advanced file handler with rotation
final advancedHandler = FileLogHandler(
filePath: 'storage/logs/app.log',
formatJson: true, // JSON format for structured logging
rotateOnSize: true, // Rotate when file reaches max size
rotateDaily: false, // Rotate daily instead of by size
maxFileSizeBytes: 10 * 1024 * 1024, // 10MB
maxBackupCount: 10, // Keep 10 backup files
);
// Daily rotation
final dailyHandler = FileLogHandler(
filePath: 'storage/logs/daily.log',
rotateOnSize: false,
rotateDaily: true,
maxBackupCount: 30, // Keep 30 days of logs
);
// Backup file naming:
// app.log.1 (most recent)
// app.log.2
// app.log.3
// ... up to maxBackupCount
Handler Features
- • Console handler with optional colorized output
- • File handler with automatic rotation (size/daily)
- • JSON and text formatting options
- • Configurable backup file management
- • Error handling that prevents logging failures
Formatters
JSON Formatter
dart
// JSON formatter for structured logging
final jsonFormatter = JsonLogFormatter();
// Output format:
{
"timestamp": "2024-01-15T10:30:00.000Z",
"level": "INFO",
"message": "User logged in",
"context": {
"userId": 123,
"username": "john_doe"
},
"stackTrace": null
}
// Benefits:
// - Machine-readable
// - Easy to parse and analyze
// - Compatible with log aggregation tools
// - Structured data preservation
Text Formatter
dart
// Text formatter for human-readable logs
final textFormatter = TextLogFormatter(
includeTimestamp: true,
includeLevel: true,
);
// Output format:
// [2024-01-15T10:30:00.000Z] [INFO] User logged in
// Context: {"userId":123,"username":"john_doe"}
// Stack Trace:
// #0 main (file:///path/to/file.dart:10:5)
// #1 _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:299:19)
// Benefits:
// - Human-readable
// - Easy to read in terminal/console
// - Includes full stack traces
// - Familiar format for developers
Formatter Output Examples
JSON: {"timestamp":"2024-01-15T10:30:00.000Z","level":"INFO","message":"User logged in","context":{"userId":123}}
Text: [2024-01-15T10:30:00.000Z] [INFO] User logged in
Context: {"userId":123}
Context: {"userId":123}
Error Handling and Stack Traces
dart
// Automatic stack trace capture
try {
riskyOperation();
} catch (e, stackTrace) {
logger.error('Operation failed', context: {
'error': e.toString(),
'errorType': e.runtimeType.toString(),
}, stackTrace: stackTrace);
}
// Manual stack trace
StackTrace? currentStack = StackTrace.current;
logger.warning('Potential issue detected', stackTrace: currentStack);
// Error with additional context
class ApiService {
Future<void> makeRequest(String url) async {
try {
final response = await http.get(Uri.parse(url));
logger.info('API request successful', context: {
'url': url,
'statusCode': response.statusCode,
'responseTime': DateTime.now().difference(startTime).inMilliseconds,
});
} catch (e, stackTrace) {
logger.error('API request failed', context: {
'url': url,
'error': e.toString(),
'method': 'GET',
'timestamp': DateTime.now().toIso8601String(),
}, stackTrace: stackTrace);
throw e; // Re-throw after logging
}
}
}
// Graceful error handling
void safeLoggingOperation() {
try {
// Some operation that might fail
performUnsafeOperation();
} catch (e) {
// Logging errors won't crash the application
logger.error('Safe operation failed', context: {'error': e.toString()});
// Continue execution...
}
}
Error Handling Features
- • Automatic stack trace capture
- • Exception details in context
- • Error recovery without crashing the application
- • Handler error isolation
Custom Handlers
dart
// Custom handler interface implementation
class DatabaseLogHandler implements LogHandler {
final DatabaseConnection _db;
DatabaseLogHandler(this._db);
@override
void log(LogLevel level, String message,
{Map<String, dynamic>? context, StackTrace? stackTrace}) {
try {
_db.insert('logs', {
'level': level.nameUpper,
'message': message,
'context': jsonEncode(context),
'stack_trace': stackTrace?.toString(),
'timestamp': DateTime.now().toIso8601String(),
});
} catch (e) {
// Fallback to console if database fails
print('Failed to log to database: $e');
}
}
@override
void close() {
// Close database connection
}
}
// Email alert handler
class EmailAlertHandler implements LogHandler {
final EmailService _email;
EmailAlertHandler(this._email);
@override
void log(LogLevel level, String message,
{Map<String, dynamic>? context, StackTrace? stackTrace}) {
if (level == LogLevel.critical || level == LogLevel.error) {
_email.sendAlert(
subject: 'Critical Error: $message',
body: '''
Level: ${level.nameUpper}
Message: $message
Context: ${jsonEncode(context)}
Stack Trace: ${stackTrace?.toString() ?? 'N/A'}
Timestamp: ${DateTime.now().toIso8601String()}
''',
);
}
}
@override
void close() {}
}
// Usage
logger.addHandler(DatabaseLogHandler(database));
logger.addHandler(EmailAlertHandler(emailService), channel: 'alerts');
Custom Handler Examples
- • Database log handler
- • External service handler (Logstash, Elasticsearch)
- • Email/SMS alert handler
- • Cloud logging services (AWS CloudWatch, GCP Logging)
- • Metrics collection handler
Middleware Integration
dart
// HTTP request logging middleware
class RequestLoggingMiddleware extends Middleware {
final Logger _logger;
RequestLoggingMiddleware(this._logger);
@override
Future<void> handle(Request request, Response response, Next next) async {
final startTime = DateTime.now();
final requestId = _generateRequestId();
// Log incoming request
_logger.info('Request started', channel: 'http', context: {
'requestId': requestId,
'method': request.method,
'url': request.url.toString(),
'headers': _sanitizeHeaders(request.headers),
'userAgent': request.header('user-agent'),
'ip': request.ip,
});
try {
await next();
final duration = DateTime.now().difference(startTime);
// Log successful response
_logger.info('Request completed', channel: 'http', context: {
'requestId': requestId,
'statusCode': response.statusCode,
'duration': duration.inMilliseconds,
'responseSize': response.length,
});
} catch (e, stackTrace) {
final duration = DateTime.now().difference(startTime);
// Log error response
_logger.error('Request failed', channel: 'http', context: {
'requestId': requestId,
'error': e.toString(),
'statusCode': response.statusCode,
'duration': duration.inMilliseconds,
}, stackTrace: stackTrace);
throw e;
}
}
String _generateRequestId() {
return DateTime.now().millisecondsSinceEpoch.toString();
}
Map<String, String> _sanitizeHeaders(Map<String, dynamic> headers) {
final sanitized = Map<String, String>.from(headers);
// Remove sensitive headers
sanitized.remove('authorization');
sanitized.remove('cookie');
return sanitized;
}
}
// Database operation logging
class DatabaseLoggingMiddleware {
final Logger _logger;
DatabaseLoggingMiddleware(this._logger);
Future<T> execute<T>(String operation, Future<T> Function() action) async {
final startTime = DateTime.now();
try {
final result = await action();
final duration = DateTime.now().difference(startTime);
_logger.debug('Database operation completed', channel: 'database', context: {
'operation': operation,
'duration': duration.inMilliseconds,
'success': true,
});
return result;
} catch (e, stackTrace) {
final duration = DateTime.now().difference(startTime);
_logger.error('Database operation failed', channel: 'database', context: {
'operation': operation,
'duration': duration.inMilliseconds,
'error': e.toString(),
}, stackTrace: stackTrace);
throw e;
}
}
}
Middleware Use Cases
- • Request/response logging
- • Performance monitoring
- • Security event logging
- • Error tracking and reporting
- • Audit trail generation
Production Configuration
dart
// Production configuration
Map<String, dynamic> productionConfig = {
'logging': {
'minimum_level': 'warning', // Reduce noise in production
'default': 'app',
'handlers': {
'console': {
'enabled': false, // Disable console in production
},
'file': {
'enabled': true,
'path': '/var/log/app/app.log',
'format_json': true, // JSON for log aggregation
'rotate_on_size': true,
'max_size': 100 * 1024 * 1024, // 100MB
'max_backups': 10,
},
// Additional file for errors only
'error_file': {
'enabled': true,
'path': '/var/log/app/errors.log',
'format_json': true,
'rotate_daily': true,
'max_backups': 30, // Keep 30 days
},
},
},
};
// Error-only handler for separate error log
class ErrorOnlyHandler implements LogHandler {
final FileLogHandler _fileHandler;
ErrorOnlyHandler(String filePath)
: _fileHandler = FileLogHandler(filePath: filePath, formatJson: true);
@override
void log(LogLevel level, String message,
{Map<String, dynamic>? context, StackTrace? stackTrace}) {
// Only log errors and above
if (level == LogLevel.error || level == LogLevel.critical) {
_fileHandler.log(level, message, context: context, stackTrace: stackTrace);
}
}
@override
void close() => _fileHandler.close();
}
Production Best Practices
- • Use higher minimum log levels (warning/error)
- • Enable file logging with rotation
- • Disable console logging in production
- • Use JSON format for better parsing
- • Configure appropriate file sizes and retention
- • Set up log monitoring and alerting
Performance Considerations
Optimization Tips
- • Log level filtering happens early to avoid unnecessary processing
- • Use appropriate minimum log levels in production
- • Consider async logging for high-volume scenarios
- • Batch log writes when possible
- • Monitor log file sizes and rotation performance
Memory Usage
- • Context data is kept in memory until logged
- • Large context objects may impact performance
- • Consider sampling for high-frequency debug logs
- • Monitor memory usage with large log volumes
Best Practices
✅ Do's
- • Use appropriate log levels for different types of messages
- • Include relevant context data for debugging
- • Use channels to organize logs by application areas
- • Configure logging for different environments (dev/prod)
- • Handle sensitive data appropriately (avoid logging passwords, tokens)
- • Use structured logging for machine-readable logs
- • Set up log rotation to manage disk space
- • Monitor log files and set up alerts for critical errors
- • Test logging configuration in staging environments
- • Document your logging strategy and conventions
❌ Don'ts
- • Don't log sensitive information (passwords, credit cards, tokens)
- • Don't use print() statements instead of proper logging
- • Don't log at debug level in production for high-traffic applications
- • Don't create too many channels (keep it organized)
- • Don't ignore log rotation configuration
- • Don't use logging for business logic or control flow
- • Don't forget to close loggers when shutting down
- • Don't use generic messages like "Error occurred"
- • Don't mix different log formats in the same application
- • Don't forget to handle logging errors gracefully
Testing Logging
dart
// Unit tests for logging
void main() {
group('Logger Tests', () {
late Logger logger;
late MockLogHandler mockHandler;
setUp(() {
logger = Logger(minimumLevel: LogLevel.debug);
mockHandler = MockLogHandler();
logger.addHandler(mockHandler);
});
test('Basic logging works', () {
logger.info('Test message');
expect(mockHandler.loggedMessages, contains('Test message'));
});
test('Log level filtering works', () {
logger = Logger(minimumLevel: LogLevel.warning);
logger.debug('Debug message'); // Should be filtered out
logger.warning('Warning message'); // Should be logged
expect(mockHandler.loggedMessages, isNot(contains('Debug message')));
expect(mockHandler.loggedMessages, contains('Warning message'));
});
test('Structured context logging works', () {
final context = {'userId': 123, 'action': 'login'};
logger.info('User action', context: context);
final loggedEntry = mockHandler.loggedEntries.last;
expect(loggedEntry.context, equals(context));
});
test('Channel-specific logging works', () {
logger.addHandler(MockLogHandler(), channel: 'test');
logger.info('Channel message', channel: 'test');
// Verify message went to correct channel
});
test('Error handling with stack traces works', () {
final stackTrace = StackTrace.current;
logger.error('Test error', stackTrace: stackTrace);
final loggedEntry = mockHandler.loggedEntries.last;
expect(loggedEntry.stackTrace, equals(stackTrace));
});
});
}
// Mock handler for testing
class MockLogHandler implements LogHandler {
final List<String> loggedMessages = [];
final List<LogEntry> loggedEntries = [];
@override
void log(LogLevel level, String message,
{Map<String, dynamic>? context, StackTrace? stackTrace}) {
loggedMessages.add(message);
loggedEntries.add(LogEntry(
level: level,
message: message,
context: context,
stackTrace: stackTrace,
timestamp: DateTime.now(),
));
}
@override
void close() {}
}
class LogEntry {
final LogLevel level;
final String message;
final Map<String, dynamic>? context;
final StackTrace? stackTrace;
final DateTime timestamp;
LogEntry({
required this.level,
required this.message,
this.context,
this.stackTrace,
required this.timestamp,
});
}
Testing Strategies
- • Test log level filtering
- • Test structured context logging
- • Test channel-specific logging
- • Test error handling and stack traces
- • Test configuration loading
- • Mock handlers for unit testing
- • Test log file rotation
- • Test custom handlers
Troubleshooting
Common Issues
- Logs not appearing: Check minimum log level configuration
- File permission errors: Ensure write access to log directories
- Performance issues: Review log volume and increase minimum log level
- Configuration not loading: Verify config structure matches expected format
- Handler errors: Check handler-specific configuration and permissions
- Memory issues: Monitor context data size and log frequency
Debug Logging
dart
// Enable debug logging for troubleshooting
final logger = Logger(minimumLevel: LogLevel.debug);
// Log configuration details
logger.debug('Logger initialized', context: {
'minimumLevel': logger.minimumLevel.name,
'defaultChannel': logger.defaultChannel,
'availableChannels': LogChannelManager().channels.toList(),
});
// Log handler information
logger.debug('Handlers configured', context: {
'console': {'enabled': true, 'colorize': true},
'file': {'enabled': true, 'path': 'storage/logs/app.log'},
});
// Test logging pipeline
logger.debug('Testing log pipeline', context: {
'testMessage': 'This is a test',
'timestamp': DateTime.now().toIso8601String(),
});
// Check file permissions
try {
final logFile = File('storage/logs/app.log');
logger.debug('Log file status', context: {
'exists': logFile.existsSync(),
'writable': logFile.existsSync() ? logFile.statSync().mode & 0x92 != 0 : false,
'size': logFile.existsSync() ? logFile.lengthSync() : 0,
});
} catch (e) {
logger.error('Failed to check log file', context: {'error': e.toString()});
}