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}

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

On this page