import 'dart:io'; import 'package:logger/logger.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:marco/helpers/services/api_service.dart'; /// Global logger instance Logger? _appLogger; late final FileLogOutput _fileLogOutput; /// Store logs temporarily for API posting final List> _logBuffer = []; /// Lock flag to prevent concurrent posting bool _isPosting = false; /// Flag to allow API posting only after login bool _canPostLogs = false; /// Maximum number of logs before triggering API post const int _maxLogsBeforePost = 100; /// Maximum logs in memory buffer const int _maxBufferSize = 500; /// Enum → logger level mapping const _levelMap = { LogLevel.debug: Level.debug, LogLevel.info: Level.info, LogLevel.warning: Level.warning, LogLevel.error: Level.error, LogLevel.verbose: Level.verbose, }; /// Initialize logging Future initLogging() async { _fileLogOutput = FileLogOutput(); _appLogger = Logger( printer: PrettyPrinter( methodCount: 0, printTime: true, colors: true, printEmojis: true, ), output: MultiOutput([ ConsoleOutput(), _fileLogOutput, ]), level: Level.debug, ); } /// Enable API posting after login void enableRemoteLogging() { _canPostLogs = true; _postBufferedLogs(); // flush logs if any } /// Safe logger wrapper void logSafe( String message, { LogLevel level = LogLevel.info, dynamic error, StackTrace? stackTrace, bool sensitive = false, }) { if (sensitive || _appLogger == null) return; final loggerLevel = _levelMap[level] ?? Level.info; _appLogger!.log(loggerLevel, message, error: error, stackTrace: stackTrace); // Buffer logs for API posting _logBuffer.add({ "logLevel": level.name, "message": message, "timeStamp": DateTime.now().toUtc().toIso8601String(), "ipAddress": "this is test IP", // TODO: real IP "userAgent": "FlutterApp/1.0", // TODO: device_info_plus "details": error?.toString() ?? stackTrace?.toString(), }); if (_logBuffer.length >= _maxLogsBeforePost) { _postBufferedLogs(); } } /// Post buffered logs to API Future _postBufferedLogs() async { if (!_canPostLogs) return; // 🚫 skip if not logged in if (_isPosting || _logBuffer.isEmpty) return; _isPosting = true; final logsToSend = List>.from(_logBuffer); _logBuffer.clear(); try { final success = await ApiService.postLogsApi(logsToSend); if (!success) { _reinsertLogs(logsToSend, reason: "API call returned false"); } } catch (e) { _reinsertLogs(logsToSend, reason: "API exception: $e"); } finally { _isPosting = false; } } /// Reinsert logs into buffer if posting fails void _reinsertLogs(List> logs, {required String reason}) { _appLogger?.w("Failed to post logs, re-queuing. Reason: $reason"); if (_logBuffer.length + logs.length > _maxBufferSize) { _appLogger?.e("Buffer full. Dropping ${logs.length} logs to prevent crash."); return; } _logBuffer.insertAll(0, logs); } /// File-based log output (safe storage) class FileLogOutput extends LogOutput { File? _logFile; Future _init() async { if (_logFile != null) return; final baseDir = await getExternalStorageDirectory(); final directory = Directory('${baseDir!.path}/marco_logs'); if (!await directory.exists()) { await directory.create(recursive: true); } final date = DateFormat('yyyy-MM-dd').format(DateTime.now()); final filePath = '${directory.path}/log_$date.txt'; _logFile = File(filePath); if (!await _logFile!.exists()) { await _logFile!.create(); } await _cleanOldLogs(directory); } @override void output(OutputEvent event) async { await _init(); if (event.lines.isEmpty) return; final logMessage = event.lines.join('\n') + '\n'; await _logFile!.writeAsString( logMessage, mode: FileMode.append, flush: true, ); } Future getLogFilePath() async { await _init(); return _logFile!.path; } Future clearLogs() async { await _init(); await _logFile!.writeAsString(''); } Future readLogs() async { await _init(); return _logFile!.readAsString(); } Future _cleanOldLogs(Directory directory) async { final files = directory.listSync(); final now = DateTime.now(); for (var file in files) { if (file is File && file.path.endsWith('.txt')) { final stat = await file.stat(); if (now.difference(stat.modified).inDays > 3) { await file.delete(); } } } } } /// Custom log levels enum LogLevel { debug, info, warning, error, verbose }