190 lines
4.7 KiB
Dart
190 lines
4.7 KiB
Dart
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<Map<String, dynamic>> _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<void> 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<void> _postBufferedLogs() async {
|
|
if (!_canPostLogs) return; // 🚫 skip if not logged in
|
|
if (_isPosting || _logBuffer.isEmpty) return;
|
|
|
|
_isPosting = true;
|
|
final logsToSend = List<Map<String, dynamic>>.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<Map<String, dynamic>> 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<void> _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<String> getLogFilePath() async {
|
|
await _init();
|
|
return _logFile!.path;
|
|
}
|
|
|
|
Future<void> clearLogs() async {
|
|
await _init();
|
|
await _logFile!.writeAsString('');
|
|
}
|
|
|
|
Future<String> readLogs() async {
|
|
await _init();
|
|
return _logFile!.readAsString();
|
|
}
|
|
|
|
Future<void> _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 }
|