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 }