Logging System

Mosaic includes a sophisticated, multi-tag logging system designed for modular applications. It provides tag-based categorization, multiple output dispatchers, and production-ready features for debugging and monitoring.

Quick Start

Basic Setup

import 'package:mosaic/mosaic.dart';

void main() async {
  // Initialize logger with tags
  await logger.init(
    tags: ['app', 'network', 'ui', 'auth', 'analytics'],
    dispatchers: [
      ConsoleDispatcher(),
      FileLoggerDispatcher(path: 'logs'),
    ],
  );
  
  runApp(MyApp());
}

Basic Logging

class HomeModule extends Module {
  @override
  Future<void> onInit() async {
    // Log with tags for categorization
    logger.info('Home module initialized', ['app', 'ui']);
    logger.debug('Loading user preferences', ['app']);
    logger.warning('Cache miss for user data', ['network']);
  }
  
  void handleUserAction() {
    try {
      // Some risky operation
      performOperation();
      logger.info('Operation completed successfully', ['app']);
    } catch (error) {
      logger.error('Operation failed: $error', ['app'], error);
    }
  }
}

Logger API

Log Levels

// Available log levels (ordered by severity)
logger.trace('Detailed debugging info', ['debug']);      // Most verbose
logger.debug('General debugging info', ['app']);         // Development
logger.info('General information', ['app']);             // Production info
logger.warning('Something unexpected happened', ['app']); // Warnings
logger.error('An error occurred', ['app'], exception);   // Errors
logger.fatal('Critical system error', ['app'], exception); // Critical

Multi-Tag Logging

class ApiService {
  Future<User> getUser(String id) async {
    logger.info('Fetching user: $id', ['network', 'api', 'user']);
    
    try {
      final response = await http.get('/users/$id');
      logger.debug('API response: ${response.statusCode}', ['network', 'api']);
      
      if (response.statusCode == 200) {
        logger.info('User fetched successfully', ['network', 'user']);
        return User.fromJson(response.data);
      } else {
        logger.warning('API returned ${response.statusCode}', ['network', 'api']);
        throw ApiException('User not found');
      }
    } catch (error) {
      logger.error('Failed to fetch user', ['network', 'api', 'user'], error);
      rethrow;
    }
  }
}

Dispatchers

Console Dispatcher

class ConsoleDispatcher extends LogDispatcher {
  @override
  void dispatch(LogEntry entry) {
    final timestamp = entry.timestamp.toIso8601String();
    final level = entry.level.name.toUpperCase();
    final tags = entry.tags.join(', ');
    
    print('[$timestamp] $level [$tags] ${entry.message}');
    
    if (entry.exception != null) {
      print('Exception: ${entry.exception}');
      if (entry.stackTrace != null) {
        print('StackTrace: ${entry.stackTrace}');
      }
    }
  }
}

File Logger Dispatcher

class FileLoggerDispatcher extends LogDispatcher {
  final String path;
  final String Function(String tag)? fileNameRole;
  
  FileLoggerDispatcher({
    required this.path,
    this.fileNameRole,
  });
  
  @override
  void dispatch(LogEntry entry) {
    // Create separate files per tag
    for (final tag in entry.tags) {
      final fileName = fileNameRole?.call(tag) ?? 
                     '${tag}_${DateTime.now().millisecondsSinceEpoch}.log';
      final file = File('$path/$fileName');
      
      final logLine = formatLogEntry(entry);
      file.writeAsStringSync('$logLine\n', mode: FileMode.append);
    }
  }
}

Custom Dispatchers

// Remote logging dispatcher
class RemoteLogDispatcher extends LogDispatcher {
  final String endpoint;
  final String apiKey;
  
  RemoteLogDispatcher({required this.endpoint, required this.apiKey});
  
  @override
  void dispatch(LogEntry entry) async {
    if (entry.level.severity >= LogLevel.warning.severity) {
      try {
        await http.post(
          endpoint,
          headers: {'Authorization': 'Bearer $apiKey'},
          body: jsonEncode({
            'level': entry.level.name,
            'message': entry.message,
            'tags': entry.tags,
            'timestamp': entry.timestamp.toIso8601String(),
            'exception': entry.exception?.toString(),
            'stackTrace': entry.stackTrace?.toString(),
          }),
        );
      } catch (error) {
        // Fallback to console if remote logging fails
        print('Failed to send log to remote: $error');
      }
    }
  }
}

// Analytics dispatcher
class AnalyticsLogDispatcher extends LogDispatcher {
  @override
  void dispatch(LogEntry entry) {
    if (entry.tags.contains('analytics')) {
      // Send to analytics service
      Analytics.track(entry.message, {
        'level': entry.level.name,
        'tags': entry.tags,
        'timestamp': entry.timestamp.millisecondsSinceEpoch,
      });
    }
  }
}

Tag-Based Organization

Organizing by Feature

class TagConstants {
  // Core system tags
  static const app = 'app';
  static const system = 'system';
  static const performance = 'performance';
  
  // Feature tags
  static const auth = 'auth';
  static const user = 'user';
  static const payment = 'payment';
  static const notification = 'notification';
  
  // Technical tags
  static const network = 'network';
  static const database = 'database';
  static const cache = 'cache';
  static const ui = 'ui';
  
  // Environment tags
  static const debug = 'debug';
  static const analytics = 'analytics';
  static const security = 'security';
}

// Usage in modules
class AuthModule extends Module {
  @override
  Future<void> onInit() async {
    logger.info('Auth module starting', [TagConstants.app, TagConstants.auth]);
  }
  
  Future<void> login(String email, String password) async {
    logger.info('Login attempt for: $email', [TagConstants.auth, TagConstants.security]);
    
    try {
      final result = await authService.login(email, password);
      logger.info('Login successful', [TagConstants.auth, TagConstants.analytics]);
      
      events.emit<User>('auth/login_success', result.user);
    } catch (error) {
      logger.error('Login failed', [TagConstants.auth, TagConstants.security], error);
      events.emit<String>('auth/login_failure', error.toString());
    }
  }
}

Log Filtering

class FilteredConsoleDispatcher extends LogDispatcher {
  final List<String> allowedTags;
  final LogLevel minimumLevel;
  
  FilteredConsoleDispatcher({
    this.allowedTags = const [],
    this.minimumLevel = LogLevel.info,
  });
  
  @override
  void dispatch(LogEntry entry) {
    // Filter by level
    if (entry.level.severity < minimumLevel.severity) {
      return;
    }
    
    // Filter by tags
    if (allowedTags.isNotEmpty) {
      final hasAllowedTag = entry.tags.any((tag) => allowedTags.contains(tag));
      if (!hasAllowedTag) {
        return;
      }
    }
    
    // Dispatch if passes filters
    print('${entry.level.name.toUpperCase()}: ${entry.message}');
  }
}

// Setup with filters
await logger.init(
  tags: ['app', 'network', 'auth', 'debug'],
  dispatchers: [
    FilteredConsoleDispatcher(
      allowedTags: ['app', 'auth'], // Only show app and auth logs
      minimumLevel: LogLevel.info,   // Skip debug/trace in production
    ),
    FileLoggerDispatcher(path: 'logs'), // Log everything to files
  ],
);

Contextual Logging

Request/Session Context

class LogContext {
  static String? _sessionId;
  static String? _userId;
  static String? _requestId;
  
  static void setSession(String sessionId, String userId) {
    _sessionId = sessionId;
    _userId = userId;
  }
  
  static void setRequest(String requestId) {
    _requestId = requestId;
  }
  
  static Map<String, String> get context => {
    if (_sessionId != null) 'sessionId': _sessionId!,
    if (_userId != null) 'userId': _userId!,
    if (_requestId != null) 'requestId': _requestId!,
  };
}

// Enhanced logger with context
extension ContextualLogger on Logger {
  void infoWithContext(String message, List<String> tags) {
    final contextData = LogContext.context;
    final enrichedMessage = contextData.isNotEmpty 
        ? '$message ${jsonEncode(contextData)}'
        : message;
    info(enrichedMessage, tags);
  }
}

// Usage
class ApiService {
  Future<void> processRequest(String requestId) async {
    LogContext.setRequest(requestId);
    
    logger.infoWithContext('Processing request', ['api', 'network']);
    // Output: "Processing request {"sessionId":"sess_123","userId":"user_456","requestId":"req_789"}"
  }
}

Structured Logging

class StructuredLogEntry {
  final String message;
  final Map<String, dynamic> data;
  final List<String> tags;
  
  StructuredLogEntry({
    required this.message,
    required this.data,
    required this.tags,
  });
  
  String toJson() => jsonEncode({
    'message': message,
    'data': data,
    'tags': tags,
    'timestamp': DateTime.now().toIso8601String(),
  });
}

extension StructuredLogger on Logger {
  void logStructured(StructuredLogEntry entry, LogLevel level) {
    switch (level) {
      case LogLevel.info:
        info(entry.toJson(), entry.tags);
        break;
      case LogLevel.warning:
        warning(entry.toJson(), entry.tags);
        break;
      case LogLevel.error:
        error(entry.toJson(), entry.tags);
        break;
      // Add other levels as needed
    }
  }
}

// Usage
logger.logStructured(
  StructuredLogEntry(
    message: 'User action performed',
    data: {
      'action': 'button_click',
      'button_id': 'submit_form',
      'form_data': {'name': 'John', 'email': 'john@example.com'},
      'user_agent': 'Mobile App v2.1.0',
      'performance': {'response_time_ms': 145},
    },
    tags: ['ui', 'analytics', 'performance'],
  ),
  LogLevel.info,
);

Production Configuration

Environment-Based Setup

class LoggerConfig {
  static Future<void> init() async {
    if (kReleaseMode) {
      await _initProductionLogger();
    } else if (kProfileMode) {
      await _initProfileLogger();
    } else {
      await _initDevelopmentLogger();
    }
  }
  
  static Future<void> _initDevelopmentLogger() async {
    await logger.init(
      tags: ['app', 'network', 'ui', 'auth', 'debug', 'analytics'],
      dispatchers: [
        ConsoleDispatcher(),
        FileLoggerDispatcher(
          path: 'logs/dev',
          fileNameRole: (tag) => '${tag}_${DateTime.now().day}.log',
        ),
      ],
    );
  }
  
  static Future<void> _initProductionLogger() async {
    await logger.init(
      tags: ['app', 'network', 'auth', 'analytics', 'security'],
      dispatchers: [
        FilteredConsoleDispatcher(
          allowedTags: ['security'], // Only security logs to console
          minimumLevel: LogLevel.warning,
        ),
        FileLoggerDispatcher(
          path: '/var/log/app',
          fileNameRole: (tag) => '${tag}_${DateTime.now().toIso8601String().split('T')[0]}.log',
        ),
        RemoteLogDispatcher(
          endpoint: 'https://logs.myapp.com/api/logs',
          apiKey: Environment.logApiKey,
        ),
      ],
    );
  }
  
  static Future<void> _initProfileLogger() async {
    await logger.init(
      tags: ['app', 'performance', 'network', 'ui'],
      dispatchers: [
        ConsoleDispatcher(),
        PerformanceLogDispatcher(), // Custom dispatcher for profiling
      ],
    );
  }
}

Log Rotation

class RotatingFileDispatcher extends LogDispatcher {
  final String basePath;
  final int maxFileSize; // in bytes
  final int maxFiles;
  
  RotatingFileDispatcher({
    required this.basePath,
    this.maxFileSize = 10 * 1024 * 1024, // 10MB
    this.maxFiles = 5,
  });
  
  @override
  void dispatch(LogEntry entry) {
    for (final tag in entry.tags) {
      final file = File('$basePath/${tag}.log');
      
      // Check if rotation is needed
      if (file.existsSync() && file.lengthSync() > maxFileSize) {
        _rotateFile(tag);
      }
      
      // Write log entry
      file.writeAsStringSync(
        '${formatLogEntry(entry)}\n',
        mode: FileMode.append,
      );
    }
  }
  
  void _rotateFile(String tag) {
    // Move current files: app.log -> app.1.log -> app.2.log etc.
    for (int i = maxFiles - 1; i >= 1; i--) {
      final oldFile = File('$basePath/$tag.${i}.log');
      final newFile = File('$basePath/$tag.${i + 1}.log');
      
      if (oldFile.existsSync()) {
        if (i == maxFiles - 1) {
          oldFile.deleteSync(); // Delete oldest
        } else {
          oldFile.renameSync(newFile.path);
        }
      }
    }
    
    // Move current log to .1
    final currentFile = File('$basePath/$tag.log');
    if (currentFile.existsSync()) {
      currentFile.renameSync('$basePath/$tag.1.log');
    }
  }
}

Performance Optimization

Async Logging

class AsyncLogDispatcher extends LogDispatcher {
  final LogDispatcher _innerDispatcher;
  final StreamController<LogEntry> _logStream = StreamController<LogEntry>();
  
  AsyncLogDispatcher(this._innerDispatcher) {
    // Process logs in background
    _logStream.stream.listen((entry) async {
      try {
        _innerDispatcher.dispatch(entry);
      } catch (error) {
        print('Log dispatch error: $error');
      }
    });
  }
  
  @override
  void dispatch(LogEntry entry) {
    // Queue log entry for background processing
    _logStream.add(entry);
  }
  
  void dispose() {
    _logStream.close();
  }
}

Batched Remote Logging

class BatchedRemoteDispatcher extends LogDispatcher {
  final String endpoint;
  final int batchSize;
  final Duration flushInterval;
  
  final List<LogEntry> _buffer = [];
  Timer? _flushTimer;
  
  BatchedRemoteDispatcher({
    required this.endpoint,
    this.batchSize = 50,
    this.flushInterval = const Duration(seconds: 30),
  }) {
    _startFlushTimer();
  }
  
  @override
  void dispatch(LogEntry entry) {
    _buffer.add(entry);
    
    if (_buffer.length >= batchSize) {
      _flush();
    }
  }
  
  void _startFlushTimer() {
    _flushTimer = Timer.periodic(flushInterval, (_) => _flush());
  }
  
  Future<void> _flush() async {
    if (_buffer.isEmpty) return;
    
    final batch = List<LogEntry>.from(_buffer);
    _buffer.clear();
    
    try {
      await http.post(
        endpoint,
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({
          'logs': batch.map((entry) => {
            'level': entry.level.name,
            'message': entry.message,
            'tags': entry.tags,
            'timestamp': entry.timestamp.toIso8601String(),
            'exception': entry.exception?.toString(),
          }).toList(),
        }),
      );
    } catch (error) {
      print('Failed to flush log batch: $error');
      // Could implement retry logic here
    }
  }
  
  void dispose() {
    _flushTimer?.cancel();
    _flush(); // Flush remaining logs
  }
}

Testing and Debugging

Log Testing

class TestLogDispatcher extends LogDispatcher {
  final List<LogEntry> capturedLogs = [];
  
  @override
  void dispatch(LogEntry entry) {
    capturedLogs.add(entry);
  }
  
  List<LogEntry> getLogsWithTag(String tag) {
    return capturedLogs.where((log) => log.tags.contains(tag)).toList();
  }
  
  List<LogEntry> getLogsWithLevel(LogLevel level) {
    return capturedLogs.where((log) => log.level == level).toList();
  }
  
  void clear() {
    capturedLogs.clear();
  }
}

// Test usage
void main() {
  group('Logger Tests', () {
    late TestLogDispatcher testDispatcher;
    
    setUp(() async {
      testDispatcher = TestLogDispatcher();
      await logger.init(
        tags: ['test', 'app'],
        dispatchers: [testDispatcher],
      );
    });
    
    test('should log with correct tags', () {
      // Act
      logger.info('Test message', ['test', 'app']);
      
      // Assert
      final logs = testDispatcher.getLogsWithTag('test');
      expect(logs.length, 1);
      expect(logs.first.message, 'Test message');
      expect(logs.first.tags, contains('test'));
      expect(logs.first.tags, contains('app'));
    });
    
    test('should capture errors with exceptions', () {
      // Arrange
      final exception = Exception('Test error');
      
      // Act
      logger.error('Error occurred', ['test'], exception);
      
      // Assert
      final errorLogs = testDispatcher.getLogsWithLevel(LogLevel.error);
      expect(errorLogs.length, 1);
      expect(errorLogs.first.exception, exception);
    });
  });
}

Debug Log Viewer

class DebugLogViewer extends StatefulWidget {
  @override
  _DebugLogViewerState createState() => _DebugLogViewerState();
}

class _DebugLogViewerState extends State<DebugLogViewer> {
  final DebugLogDispatcher _debugDispatcher = DebugLogDispatcher();
  List<String> _selectedTags = [];
  LogLevel _minimumLevel = LogLevel.debug;
  
  @override
  void initState() {
    super.initState();
    // Add debug dispatcher to capture logs
    logger.addDispatcher(_debugDispatcher);
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Debug Logs'),
        actions: [
          IconButton(
            icon: Icon(Icons.clear),
            onPressed: () => _debugDispatcher.clear(),
          ),
        ],
      ),
      body: Column(
        children: [
          // Filters
          _buildFilters(),
          
          // Log list
          Expanded(
            child: ListView.builder(
              itemCount: _debugDispatcher.filteredLogs(_selectedTags, _minimumLevel).length,
              itemBuilder: (context, index) {
                final log = _debugDispatcher.filteredLogs(_selectedTags, _minimumLevel)[index];
                return _buildLogEntry(log);
              },
            ),
          ),
        ],
      ),
    );
  }
  
  Widget _buildLogEntry(LogEntry log) {
    final color = _getLogLevelColor(log.level);
    
    return Card(
      margin: EdgeInsets.all(4),
      child: ExpansionTile(
        leading: Container(
          width: 12,
          height: 12,
          decoration: BoxDecoration(
            color: color,
            shape: BoxShape.circle,
          ),
        ),
        title: Text(
          log.message,
          style: TextStyle(fontSize: 14),
        ),
        subtitle: Text(
          '${log.level.name.toUpperCase()}${log.tags.join(', ')}${log.timestamp.toString()}',
          style: TextStyle(fontSize: 12),
        ),
        children: [
          if (log.exception != null)
            Padding(
              padding: EdgeInsets.all(16),
              child: Text(
                'Exception: ${log.exception}\n\nStack Trace:\n${log.stackTrace}',
                style: TextStyle(fontFamily: 'monospace', fontSize: 12),
              ),
            ),
        ],
      ),
    );
  }
  
  Color _getLogLevelColor(LogLevel level) {
    switch (level) {
      case LogLevel.trace:
        return Colors.grey;
      case LogLevel.debug:
        return Colors.blue;
      case LogLevel.info:
        return Colors.green;
      case LogLevel.warning:
        return Colors.orange;
      case LogLevel.error:
        return Colors.red;
      case LogLevel.fatal:
        return Colors.purple;
    }
  }
  
  Widget _buildFilters() {
    return Container(
      padding: EdgeInsets.all(16),
      child: Column(
        children: [
          // Tag filter
          Wrap(
            children: ['app', 'network', 'ui', 'auth', 'debug'].map((tag) {
              return FilterChip(
                label: Text(tag),
                selected: _selectedTags.contains(tag),
                onSelected: (selected) {
                  setState(() {
                    if (selected) {
                      _selectedTags.add(tag);
                    } else {
                      _selectedTags.remove(tag);
                    }
                  });
                },
              );
            }).toList(),
          ),
          
          // Level filter
          DropdownButton<LogLevel>(
            value: _minimumLevel,
            items: LogLevel.values.map((level) {
              return DropdownMenuItem(
                value: level,
                child: Text(level.name.toUpperCase()),
              );
            }).toList(),
            onChanged: (level) {
              if (level != null) {
                setState(() {
                  _minimumLevel = level;
                });
              }
            },
          ),
        ],
      ),
    );
  }
}

class DebugLogDispatcher extends LogDispatcher {
  final List<LogEntry> _logs = [];
  
  @override
  void dispatch(LogEntry entry) {
    _logs.add(entry);
    
    // Keep only last 1000 logs
    if (_logs.length > 1000) {
      _logs.removeAt(0);
    }
  }
  
  List<LogEntry> filteredLogs(List<String> tags, LogLevel minimumLevel) {
    return _logs.where((log) {
      // Filter by level
      if (log.level.severity < minimumLevel.severity) {
        return false;
      }
      
      // Filter by tags
      if (tags.isNotEmpty) {
        return log.tags.any((tag) => tags.contains(tag));
      }
      
      return true;
    }).toList();
  }
  
  void clear() {
    _logs.clear();
  }
}

Advanced Patterns

Hierarchical Logging

class HierarchicalLogger {
  final String namespace;
  final Logger _baseLogger;
  
  HierarchicalLogger(this.namespace, this._baseLogger);
  
  HierarchicalLogger child(String childNamespace) {
    return HierarchicalLogger('$namespace.$childNamespace', _baseLogger);
  }
  
  void info(String message, [List<String> additionalTags = const []]) {
    final tags = [namespace, ...additionalTags];
    _baseLogger.info(message, tags);
  }
  
  void error(String message, [List<String> additionalTags = const [], Object? exception]) {
    final tags = [namespace, ...additionalTags];
    _baseLogger.error(message, tags, exception);
  }
}

// Usage
class UserModule extends Module {
  late final HierarchicalLogger _logger;
  
  @override
  Future<void> onInit() async {
    _logger = HierarchicalLogger('user', logger);
    _logger.info('User module initialized'); // Tags: ['user']
    
    _setupApiClient();
    _setupAuthHandler();
  }
  
  void _setupApiClient() {
    final apiLogger = _logger.child('api');
    apiLogger.info('API client configured'); // Tags: ['user.api']
  }
  
  void _setupAuthHandler() {
    final authLogger = _logger.child('auth');
    authLogger.info('Auth handler ready'); // Tags: ['user.auth']
  }
}

Metric Logging

class MetricLogger {
  static void timing(String operation, Duration duration, [List<String> tags = const []]) {
    logger.info(
      'METRIC: $operation completed in ${duration.inMilliseconds}ms',
      ['metrics', 'timing', ...tags],
    );
  }
  
  static void counter(String metric, int value, [List<String> tags = const []]) {
    logger.info(
      'METRIC: $metric = $value',
      ['metrics', 'counter', ...tags],
    );
  }
  
  static void gauge(String metric, double value, [List<String> tags = const []]) {
    logger.info(
      'METRIC: $metric = $value',
      ['metrics', 'gauge', ...tags],
    );
  }
}

// Usage with performance monitoring
class ApiService {
  Future<User> fetchUser(String id) async {
    final stopwatch = Stopwatch()..start();
    
    try {
      final user = await _performFetch(id);
      MetricLogger.timing('api.fetch_user', stopwatch.elapsed, ['api', 'user']);
      MetricLogger.counter('api.fetch_user.success', 1, ['api', 'user']);
      return user;
    } catch (error) {
      MetricLogger.timing('api.fetch_user', stopwatch.elapsed, ['api', 'user', 'error']);
      MetricLogger.counter('api.fetch_user.failure', 1, ['api', 'user']);
      rethrow;
    }
  }
}

Best Practices

1. Tag Organization

// ✅ Good - Hierarchical tag structure
class Tags {
  // Domain tags
  static const auth = 'auth';
  static const user = 'user';
  static const payment = 'payment';
  
  // Technical tags
  static const network = 'network';
  static const database = 'database';
  static const cache = 'cache';
  
  // Operational tags
  static const performance = 'performance';
  static const security = 'security';
  static const analytics = 'analytics';
}

// ❌ Avoid - Inconsistent tag naming
logger.info('User logged in', ['Auth', 'LOGIN', 'user_management']);

2. Message Formatting

// ✅ Good - Structured, searchable messages
logger.info('User login successful', ['auth'], {
  'userId': user.id,
  'loginMethod': 'email',
  'duration': loginDuration.inMilliseconds,
});

// ❌ Avoid - Unstructured messages
logger.info('User ${user.name} logged in using email after ${loginDuration}ms');

3. Error Logging

// ✅ Good - Comprehensive error logging
try {
  await criticalOperation();
} catch (error, stackTrace) {
  logger.error(
    'Critical operation failed',
    ['operation', 'critical', 'error'],
    error,
    stackTrace,
  );
  
  // Also emit event for error handling
  events.emit<String>('system/critical_error', error.toString());
}

// ❌ Avoid - Silent failures
try {
  await criticalOperation();
} catch (error) {
  // Silent failure - no logging
}

4. Production Considerations

// ✅ Good - Environment-aware logging
class ProductionLogger {
  static bool get shouldLogDebug => !kReleaseMode;
  static bool get shouldLogTrace => kDebugMode;
  
  static void debug(String message, List<String> tags) {
    if (shouldLogDebug) {
      logger.debug(message, tags);
    }
  }
  
  static void trace(String message, List<String> tags) {
    if (shouldLogTrace) {
      logger.trace(message, tags);
    }
  }
}

Troubleshooting

Common Issues

Logs not appearing:

// Check if logger is initialized
await logger.init(tags: ['app'], dispatchers: [ConsoleDispatcher()]);

// Verify dispatcher is added
logger.addDispatcher(ConsoleDispatcher());

Performance issues:

// Use async dispatcher for heavy logging
logger.addDispatcher(AsyncLogDispatcher(FileLoggerDispatcher()));

// Limit log levels in production
FilteredConsoleDispatcher(minimumLevel: LogLevel.warning);

File logging not working:

// Ensure directory exists
Directory('logs').createSync(recursive: true);

// Check permissions
final file = File('logs/app.log');
if (!file.parent.existsSync()) {
  file.parent.createSync(recursive: true);
}

Next Steps

The logging system in Mosaic provides comprehensive debugging and monitoring capabilities that scale from development to production environments.