/ app / lib / core / utils / logger.dart
logger.dart
  1  import 'dart:async';
  2  import 'dart:io';
  3  
  4  import 'package:path_provider/path_provider.dart';
  5  
  6  /// Log levels for the logger service.
  7  enum LogLevel {
  8    debug,
  9    info,
 10    warning,
 11    error;
 12  
 13    String get label => switch (this) {
 14          LogLevel.debug => 'DEBUG',
 15          LogLevel.info => 'INFO ',
 16          LogLevel.warning => 'WARN ',
 17          LogLevel.error => 'ERROR',
 18        };
 19  }
 20  
 21  /// Singleton logger service with file-based logging.
 22  ///
 23  /// Provides structured logging with rotation support.
 24  ///
 25  /// Usage:
 26  /// ```dart
 27  /// final logger = LoggerService.instance;
 28  /// await logger.initialize();
 29  /// logger.info('MyTag', 'Something happened');
 30  /// ```
 31  class LoggerService {
 32    static LoggerService? _instance;
 33    bool _initialized = false;
 34  
 35    late final Directory _logDir;
 36    late final File _logFile;
 37    IOSink? _sink;
 38  
 39    static const int _maxFileSize = 5 * 1024 * 1024; // 5MB
 40    static const int _maxRotatedFiles = 3;
 41    static const String _logFileName = 'dead_drop.log';
 42  
 43    LoggerService._();
 44  
 45    /// Singleton instance
 46    static LoggerService get instance {
 47      _instance ??= LoggerService._();
 48      return _instance!;
 49    }
 50  
 51    /// Whether the service has been initialized
 52    bool get isInitialized => _initialized;
 53  
 54    /// Initialize the logger service.
 55    ///
 56    /// Must be called before logging. Creates log directory if needed.
 57    Future<void> initialize() async {
 58      if (_initialized) return;
 59  
 60      final appDir = await getApplicationDocumentsDirectory();
 61      _logDir = Directory('${appDir.path}/logs');
 62  
 63      if (!await _logDir.exists()) {
 64        await _logDir.create(recursive: true);
 65      }
 66  
 67      _logFile = File('${_logDir.path}/$_logFileName');
 68      await _openSink();
 69      _initialized = true;
 70  
 71      info('Logger', 'Logger initialized');
 72    }
 73  
 74    Future<void> _openSink() async {
 75      _sink = _logFile.openWrite(mode: FileMode.append);
 76    }
 77  
 78    /// Log a debug message.
 79    void debug(String tag, String message) {
 80      _log(LogLevel.debug, tag, message);
 81    }
 82  
 83    /// Log an info message.
 84    void info(String tag, String message) {
 85      _log(LogLevel.info, tag, message);
 86    }
 87  
 88    /// Log a warning message.
 89    void warning(String tag, String message) {
 90      _log(LogLevel.warning, tag, message);
 91    }
 92  
 93    /// Log an error message.
 94    void error(String tag, String message, [Object? error, StackTrace? stack]) {
 95      _log(LogLevel.error, tag, message);
 96      if (error != null) {
 97        _log(LogLevel.error, tag, 'Error: $error');
 98      }
 99      if (stack != null) {
100        _log(LogLevel.error, tag, 'Stack trace:\n$stack');
101      }
102    }
103  
104    void _log(LogLevel level, String tag, String message) {
105      if (!_initialized) return;
106  
107      final timestamp = DateTime.now().toIso8601String();
108      final line = '$timestamp [${level.label}] [$tag] $message\n';
109  
110      _sink?.write(line);
111  
112      // Also print to console in debug mode
113      assert(() {
114        // ignore: avoid_print
115        print(line.trimRight());
116        return true;
117      }());
118  
119      // Check for rotation asynchronously
120      _checkRotation();
121    }
122  
123    Future<void> _checkRotation() async {
124      try {
125        if (!await _logFile.exists()) return;
126  
127        final size = await _logFile.length();
128        if (size >= _maxFileSize) {
129          await _rotate();
130        }
131      } catch (_) {
132        // Ignore rotation errors
133      }
134    }
135  
136    Future<void> _rotate() async {
137      // Close current sink
138      await _sink?.flush();
139      await _sink?.close();
140  
141      // Rotate existing files
142      for (var i = _maxRotatedFiles - 1; i >= 0; i--) {
143        final suffix = i == 0 ? '' : '.$i';
144        final nextSuffix = '.${i + 1}';
145        final current = File('${_logDir.path}/$_logFileName$suffix');
146        final next = File('${_logDir.path}/$_logFileName$nextSuffix');
147  
148        if (await current.exists()) {
149          if (i == _maxRotatedFiles - 1) {
150            // Delete oldest
151            await current.delete();
152          } else {
153            // Rename to next number
154            await current.rename(next.path);
155          }
156        }
157      }
158  
159      // Rename current log file to .0
160      if (await _logFile.exists()) {
161        await _logFile.rename('${_logDir.path}/$_logFileName.0');
162      }
163  
164      // Reopen sink for new file
165      await _openSink();
166    }
167  
168    /// Flush pending writes to disk.
169    Future<void> flush() async {
170      await _sink?.flush();
171    }
172  
173    /// Close the logger and release resources.
174    Future<void> close() async {
175      if (!_initialized) return;
176  
177      info('Logger', 'Logger closing');
178      await _sink?.flush();
179      await _sink?.close();
180      _sink = null;
181      _initialized = false;
182    }
183  
184    /// Get all log files for export.
185    Future<List<File>> getLogFiles() async {
186      if (!_initialized) return [];
187  
188      final files = <File>[];
189  
190      // Current log file
191      if (await _logFile.exists()) {
192        files.add(_logFile);
193      }
194  
195      // Rotated files
196      for (var i = 0; i < _maxRotatedFiles; i++) {
197        final rotated = File('${_logDir.path}/$_logFileName.$i');
198        if (await rotated.exists()) {
199          files.add(rotated);
200        }
201      }
202  
203      return files;
204    }
205  
206    /// Get the log directory path.
207    String get logDirectoryPath => _logDir.path;
208  }