import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart' as foundation; import 'package:fimber/fimber.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:time/time.dart'; // FIXME: Only catch Exception? type. Something else needs to be done with Errors class Log { static late String logFolderPath; static RandomAccessFile? logFile; static Future init() async { if (foundation.kDebugMode) { Fimber.plantTree(DebugTree.elapsed(useColors: true)); } var cacheDir = await getTemporaryDirectory(); logFolderPath = p.join(cacheDir.path, "logs"); try { Directory(logFolderPath).createSync(); } catch (e) { // Ignore if it already exists } await setLogCapture(true); } static void v( String msg, { dynamic ex, StackTrace? stacktrace, Map? props, }) { if (stacktrace != null) { stacktrace = Trace.from(stacktrace).terse; } if (foundation.kDebugMode) { Fimber.log("V", msg, ex: ex, stacktrace: stacktrace, tag: LogTree.getTag(stackIndex: 2)); } _write('v', msg, ex, stacktrace, props); } static void d( String msg, { dynamic ex, StackTrace? stacktrace, Map? props, }) { if (stacktrace != null) { stacktrace = Trace.from(stacktrace).terse; } if (foundation.kDebugMode) { Fimber.log("D", msg, ex: ex, stacktrace: stacktrace, tag: LogTree.getTag(stackIndex: 2)); } _write('d', msg, ex, stacktrace, props); } static void i( String msg, { dynamic ex, StackTrace? stacktrace, Map? props, }) { if (stacktrace != null) { stacktrace = Trace.from(stacktrace).terse; } if (foundation.kDebugMode) { if (props != null && props.isNotEmpty) { msg += " $props"; } Fimber.log("I", msg, ex: ex, stacktrace: stacktrace, tag: LogTree.getTag(stackIndex: 2)); } _write('i', msg, ex, stacktrace, props); } static void e( String msg, { dynamic ex, StackTrace? stacktrace, Map? props, }) { if (stacktrace != null) { stacktrace = Trace.from(stacktrace).terse; } if (foundation.kDebugMode) { Fimber.log("E", msg, ex: ex, stacktrace: stacktrace, tag: LogTree.getTag(stackIndex: 2)); } _write('e', msg, ex, stacktrace, props); } static void w( String msg, { dynamic ex, StackTrace? stacktrace, Map? props, }) { if (stacktrace != null) { stacktrace = Trace.from(stacktrace).terse; } if (foundation.kDebugMode) { Fimber.log("W", msg, ex: ex, stacktrace: stacktrace, tag: LogTree.getTag(stackIndex: 2)); } _write('w', msg, ex, stacktrace, props); } static void _write( String level, String msg, dynamic ex, StackTrace? stackTrace, Map? props, ) { if (logFile == null) { return; } var logMsg = LogMessage( t: DateTime.now().millisecondsSinceEpoch, l: level, msg: msg.replaceAll('\n', ' '), ex: ex != null ? ex.toString().replaceAll('\n', ' ') : null, props: props, ); if (stackTrace != null) { logMsg.stack = stackTrace.toListOfMap(); } var str = json.encode(logMsg.toMap()); logFile!.writeStringSync(str + '\n'); } static Future setLogCapture(bool state) async { if (state) { var today = DateTime.now().toString().substring(0, 10); var logFilePath = p.join(logFolderPath, '$today.jsonl'); logFile = await File(logFilePath).open(mode: FileMode.append); print("Writing logs to file $logFilePath"); } else { if (logFile != null) { await logFile!.close(); } logFile = null; } } static Iterable fetchLogs() sync* { var today = DateTime.now(); var yesterday = today.add(-1.days); for (var msg in fetchLogsForDate(yesterday)) { yield msg; } for (var msg in fetchLogsForDate(today)) { yield msg; } } static Iterable fetchLogsForDate(DateTime date) sync* { var file = File(filePathForDate(date)); if (!file.existsSync()) { var dateOnly = date.toIso8601String().substring(0, 10); Log.i("No log file for $dateOnly"); return; } var str = file.readAsStringSync(); for (var line in LineSplitter.split(str)) { try { yield LogMessage.fromMap(json.decode(line)); } catch (e) { //Log.e("fetchLogsForDate: $e"); } } } static String filePathForDate(DateTime dt) { var date = dt.toString().substring(0, 10); return p.join(logFolderPath, '$date.jsonl'); } static List filePathsForDates(int n) { var today = DateTime.now(); var l = []; for (var i = 0; i < n; i++) { var fp = filePathForDate(today.subtract(i.days)); if (File(fp).existsSync()) { l.add(fp); } else { Log.i("Log file $fp not found"); } } return l; } } class LogMessage { late int t; late String l; late String msg; String? ex; List>? stack; Map? props; LogMessage({ required this.t, required this.l, required this.msg, this.ex, this.stack, this.props, }); Map toMap() { return { 't': t, 'l': l, 'msg': msg, if (ex != null && ex!.isNotEmpty) 'ex': ex, if (stack != null) 'stack': stack, if (props != null && props!.isNotEmpty) 'p': props, }; } // todo: Make sure type conversion doesn't fuck up anything LogMessage.fromMap(Map map) { t = map['t']; l = map['l']; msg = map['msg']; ex = _checkForStringNull(map['ex']); stack = _parseJson(map['stack']); props = _checkForStringNull(map['p']); } } List>? _parseJson(List? l) { if (l == null) { return null; } var list = >[]; for (var i in l) { list.add(i); } return list; } dynamic _checkForStringNull(dynamic e) { if (e == null) return e; if (e.runtimeType == String && e.toString().trim() == 'null') { return null; } return e; } extension TraceJsonEncoding on StackTrace { List> toListOfMap() { var list = >[]; for (var f in Trace.from(this).frames) { list.add(f.toMap()); } return list; } } extension FrameJsonEncoding on Frame { Map toMap() { return _removeNull({ 'column': column, 'uri': uri.toString(), 'line': line, 'member': member, 'isCore': isCore, 'library': library, 'location': location, 'package': package, }); } } Map _removeNull(Map map) { map.removeWhere((key, value) => value == null); return map; }