import 'dart:async'; import 'dart:collection'; import 'dart:io'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:guru_utils/datetime/datetime_utils.dart'; import 'package:guru_utils/number/number_utils.dart'; import 'package:guru_utils/random/random_utils.dart'; import 'package:logger/logger.dart'; import 'package:persistent/log/persistent_log.dart'; import 'dart:developer' as developer; /// Created by Haoyi on 4/13/21 /// /// class LogLevel { static const verbose = 0; static const debug = 1; static const info = 2; static const warning = 3; static const error = 4; static const wtf = 5; static const nothing = -1; static const List levels = [ Level.trace, Level.debug, Level.info, Level.warning, Level.error, Level.fatal, ]; } abstract class _TerminalColor { String call(String msg) => msg; } class CmdColor extends _TerminalColor { final String cmd; CmdColor(this.cmd); @override String call(String msg) { return "$cmd$msg${AnsiColor.ansiDefault}"; } } /// This class handles colorizing of terminal output. class AnsiColor extends _TerminalColor { /// ANSI Control Sequence Introducer, signals the terminal for new settings. static const ansiEsc = '\x1B['; /// Reset all colors and options for current SGRs to terminal defaults. static const ansiDefault = '${ansiEsc}0m'; final int? fg; final int? bg; final bool color; AnsiColor.none() : fg = null, bg = null, color = false; AnsiColor.fg(this.fg) : bg = null, color = true; AnsiColor.bg(this.bg) : fg = null, color = true; @override String toString() { if (fg != null) { return '${ansiEsc}38;5;${fg}m'; } else if (bg != null) { return '${ansiEsc}48;5;${bg}m'; } else { return ''; } } @override String call(String msg) { if (!kIsWeb && color && Platform.isAndroid) { return '${this}$msg$ansiDefault'; } else { return msg; } } AnsiColor toFg() => AnsiColor.fg(bg); AnsiColor toBg() => AnsiColor.bg(fg); /// Defaults the terminal's foreground color without altering the background. String get resetForeground => color ? '${ansiEsc}39m' : ''; /// Defaults the terminal's background color without altering the foreground. String get resetBackground => color ? '${ansiEsc}49m' : ''; static int grey(double level) => 232 + (level.clamp(0.0, 1.0) * 23).round(); } class LogRecord { final int sequence; final DateTime time; final Level level; final String? tag; final String msg; const LogRecord(this.sequence, this.time, this.level, this.tag, this.msg); } class Log { static final errorFgColor = AnsiColor.fg(1); static String _appName = "GAME"; static final levelFgColors = [ AnsiColor.none(), AnsiColor.fg(33), AnsiColor.fg(40), AnsiColor.fg(208), AnsiColor.fg(1), AnsiColor.fg(129), ]; static final tagCandidateColor = [ AnsiColor.fg(1), AnsiColor.fg(2), AnsiColor.fg(3), AnsiColor.fg(4), AnsiColor.fg(5), AnsiColor.fg(6), AnsiColor.fg(9), AnsiColor.fg(10), AnsiColor.fg(11), AnsiColor.fg(12), AnsiColor.fg(13), AnsiColor.fg(14), AnsiColor.fg(28), AnsiColor.fg(29), AnsiColor.fg(30), AnsiColor.fg(31), AnsiColor.fg(32), AnsiColor.fg(33), AnsiColor.fg(34), AnsiColor.fg(35), AnsiColor.fg(36), AnsiColor.fg(37), AnsiColor.fg(38), AnsiColor.fg(39), AnsiColor.fg(40), AnsiColor.fg(41), AnsiColor.fg(42), AnsiColor.fg(43), AnsiColor.fg(44), AnsiColor.fg(45), AnsiColor.fg(46), AnsiColor.fg(47), AnsiColor.fg(48), AnsiColor.fg(49), AnsiColor.fg(50), AnsiColor.fg(51), AnsiColor.fg(68), AnsiColor.fg(69), AnsiColor.fg(70), AnsiColor.fg(71), AnsiColor.fg(72), AnsiColor.fg(73), AnsiColor.fg(74), AnsiColor.fg(75), AnsiColor.fg(76), AnsiColor.fg(77), AnsiColor.fg(78), AnsiColor.fg(79), AnsiColor.fg(80), AnsiColor.fg(81), AnsiColor.fg(82), AnsiColor.fg(83), AnsiColor.fg(84), AnsiColor.fg(85), AnsiColor.fg(86), AnsiColor.fg(87), AnsiColor.fg(88), AnsiColor.fg(89), AnsiColor.fg(90), AnsiColor.fg(91), AnsiColor.fg(92), AnsiColor.fg(93), AnsiColor.fg(94), AnsiColor.fg(95), AnsiColor.fg(96), AnsiColor.fg(97), AnsiColor.fg(98), AnsiColor.fg(99), AnsiColor.fg(100), AnsiColor.fg(101), AnsiColor.fg(102), AnsiColor.fg(103), AnsiColor.fg(104), AnsiColor.fg(105), AnsiColor.fg(106), AnsiColor.fg(107), AnsiColor.fg(108), AnsiColor.fg(109), AnsiColor.fg(110), AnsiColor.fg(111), AnsiColor.fg(112), AnsiColor.fg(113), AnsiColor.fg(114), AnsiColor.fg(115), AnsiColor.fg(116), AnsiColor.fg(117), AnsiColor.fg(118), AnsiColor.fg(119), AnsiColor.fg(120), AnsiColor.fg(121), AnsiColor.fg(122), AnsiColor.fg(123), AnsiColor.fg(124), AnsiColor.fg(125), AnsiColor.fg(126), AnsiColor.fg(127), AnsiColor.fg(128), AnsiColor.fg(129), AnsiColor.fg(130), AnsiColor.fg(131), AnsiColor.fg(132), AnsiColor.fg(133), AnsiColor.fg(134), AnsiColor.fg(135), AnsiColor.fg(136), AnsiColor.fg(137), AnsiColor.fg(138), AnsiColor.fg(139), AnsiColor.fg(140), AnsiColor.fg(141), AnsiColor.fg(142), AnsiColor.fg(143), AnsiColor.fg(144), AnsiColor.fg(145), AnsiColor.fg(146), AnsiColor.fg(147), AnsiColor.fg(148), AnsiColor.fg(149), AnsiColor.fg(150), AnsiColor.fg(151), AnsiColor.fg(152), AnsiColor.fg(153), AnsiColor.fg(154), AnsiColor.fg(155), AnsiColor.fg(156), AnsiColor.fg(157), AnsiColor.fg(158), AnsiColor.fg(159), AnsiColor.fg(160), AnsiColor.fg(161), AnsiColor.fg(162), AnsiColor.fg(163), AnsiColor.fg(164), AnsiColor.fg(165), AnsiColor.fg(166), AnsiColor.fg(167), AnsiColor.fg(168), AnsiColor.fg(169), AnsiColor.fg(170), AnsiColor.fg(171), AnsiColor.fg(172), AnsiColor.fg(173), AnsiColor.fg(174), AnsiColor.fg(175), AnsiColor.fg(176), AnsiColor.fg(177), AnsiColor.fg(178), AnsiColor.fg(179), AnsiColor.fg(180), AnsiColor.fg(181), AnsiColor.fg(182), AnsiColor.fg(183), AnsiColor.fg(184), AnsiColor.fg(185), AnsiColor.fg(186), AnsiColor.fg(187), AnsiColor.fg(188), AnsiColor.fg(189), AnsiColor.fg(190), AnsiColor.fg(191), AnsiColor.fg(192), AnsiColor.fg(193), AnsiColor.fg(194), AnsiColor.fg(195), AnsiColor.fg(196), AnsiColor.fg(197), AnsiColor.fg(198), AnsiColor.fg(199), AnsiColor.fg(200), AnsiColor.fg(201), AnsiColor.fg(202), AnsiColor.fg(203), AnsiColor.fg(204), AnsiColor.fg(205), AnsiColor.fg(206), AnsiColor.fg(207), AnsiColor.fg(208), AnsiColor.fg(209), AnsiColor.fg(210), AnsiColor.fg(211), AnsiColor.fg(212), AnsiColor.fg(213), AnsiColor.fg(214), AnsiColor.fg(215), AnsiColor.fg(216), AnsiColor.fg(217), AnsiColor.fg(218), AnsiColor.fg(219), AnsiColor.fg(220), AnsiColor.fg(221), AnsiColor.fg(222), AnsiColor.fg(223), AnsiColor.fg(224), AnsiColor.fg(225), ]; static final levelBgColors = [ AnsiColor.bg(0), AnsiColor.bg(27), AnsiColor.bg(22), AnsiColor.bg(130), AnsiColor.bg(1), AnsiColor.bg(129), ]; static final levelTags = [ AnsiColor.bg(0)(" V "), AnsiColor.bg(27)(" D "), AnsiColor.bg(22)(" I "), AnsiColor.bg(130)(" W "), AnsiColor.bg(1)(" E "), AnsiColor.bg(129)("WTF") ]; static final Map tagColors = {}; static final Logger _logger = Logger(printer: PrettyPrinter(methodCount: 0)); static const persistentLogName = "guru_app"; static int recordCount = 0; static final DoubleLinkedQueue latestLogRecords = DoubleLinkedQueue(); static final StreamController _logStream = StreamController.broadcast(); static bool listening = false; static List> dumpRecords({String? filterTag}) { return latestLogRecords .where((record) => filterTag == null || filterTag == _appName || record.tag == filterTag) .map((record) => MapEntry( "[${record.sequence}] ${record.tag == null ? "" : "#${record.tag}#"} ${DateTimeUtils.standardDateFormat.format(record.time)}", record.msg)) .toList(); } static bool isDebug = kDebugMode; static int _persistentLevel = LogLevel.debug; static Stream get observableLogStream => _logStream.stream; static Future init(String appName, {int persistentLogFileSize = 1024 * 1024 * 10, int persistentLogCount = 7, int persistentLevel = LogLevel.debug}) async { Log._appName = appName; tagColors[_appName] = CmdColor("${AnsiColor.ansiEsc}33;1m"); _persistentLevel = persistentLevel; if (persistentLevel != LogLevel.nothing) { await PersistentLog.createLogger( logName: persistentLogName, fileSizeLimit: persistentLogFileSize, fileCount: persistentLogCount); } } static void setListen(bool listen) { listening = listen; } static String _toColorTag(String? tag) { if (tag == null) { return ""; } _TerminalColor? color = tagColors[tag]; if (color == null) { color = tagCandidateColor[RandomUtils.nextInt(tagCandidateColor.length)]; tagColors[tag] = color; } return color(" #$tag#"); // "${AnsiColor.ansiEsc}33;1m$name${AnsiColor.ansiDefault}"; 加粗 // "${AnsiColor.ansiEsc}33;3m$name${AnsiColor.ansiDefault}"; 斜体 // "${AnsiColor.ansiEsc}33;4m$name${AnsiColor.ansiDefault}"; 下滑线 return "${AnsiColor.ansiEsc}33;1m$tag${AnsiColor.ansiDefault}"; } static String _toColorName(String name) { if (!kIsWeb && Platform.isAndroid) { // "${AnsiColor.ansiEsc}33;1m$name${AnsiColor.ansiDefault}"; 加粗 // "${AnsiColor.ansiEsc}33;3m$name${AnsiColor.ansiDefault}"; 斜体 // "${AnsiColor.ansiEsc}33;4m$name${AnsiColor.ansiDefault}"; 下滑线 return "${AnsiColor.ansiEsc}33;1m$name${AnsiColor.ansiDefault}"; } return name; } static String _toDateTime(DateTime dateTime) { final humanDt = "${NumberUtils.twoDigits(dateTime.hour)}:${NumberUtils.twoDigits(dateTime.minute)}:${NumberUtils.twoDigits(dateTime.second)}.${NumberUtils.threeDigits(dateTime.millisecondsSinceEpoch % 1000)}"; if (!kIsWeb && Platform.isAndroid) { return "${AnsiColor.ansiEsc}36;3m<$humanDt>${AnsiColor.ansiDefault}"; } return "<$humanDt>"; } static void _log(int level, String msgData, {String? tag, dynamic error, StackTrace? stackTrace, bool syncFirebase = false, bool persistent = false}) { final now = DateTime.now(); final msg = msgData.substring(0, min(4096, msgData.length)); recordCount++; // latestLogRecords.addFirst(MapEntry("[$recordCount] ${DateTimeUtils.humanTime}", msg)); final logRecord = LogRecord(recordCount, now, LogLevel.levels[level], tag, "$msg${error != null ? '!![$error]!!' : ''}"); latestLogRecords.addFirst(logRecord); if (latestLogRecords.length > 2000) { latestLogRecords.removeLast(); } if (stackTrace != null) { _logger.log(LogLevel.levels[level], "[$_appName] ${tag == null ? "" : "#$tag#"} $msg", error: error, stackTrace: stackTrace); } else { final color = levelFgColors[level]; String output; if (error != null) { output = "${color(msg)}[${errorFgColor(error.toString())}]"; } else { output = color(msg); } if (isDebug) { // ignore: avoid_print print( "${_toColorName("[$_appName]")} ${_toDateTime(now)} ${levelTags[level]}${_toColorTag(tag)} $output"); } } if (!kIsWeb && (level >= _persistentLevel || persistent)) { PersistentLog.log( logName: persistentLogName, message: "$msg ${error != null ? error.toString() : ''}"); } if (syncFirebase) { // Analytics.instance.logFirebase("$msg ${error != null ? error.toString() : ''}"); // if (error != null) { // if (syncCrashlytics) { // Analytics.instance // .logException(M2BlocksException(error, cause: error), stacktrace: stackTrace); // } // } else { // Analytics.instance.logFirebase(msg); // } } if (listening) { _logStream.add(logRecord); } } static void v(String msg, {String? tag, dynamic error, StackTrace? stackTrace, bool syncFirebase = false, bool syncCrashlytics = false, bool persistent = false}) { _log(LogLevel.verbose, msg, tag: tag, error: error, stackTrace: stackTrace, syncFirebase: syncFirebase, persistent: persistent); } static void d(String msg, {String? tag, dynamic error, StackTrace? stackTrace, bool syncFirebase = false, bool syncCrashlytics = false, bool persistent = false}) { _log(LogLevel.debug, msg, tag: tag, error: error, stackTrace: stackTrace, syncFirebase: syncFirebase, persistent: persistent); } static void i(String msg, {String? tag, dynamic error, StackTrace? stackTrace, bool syncFirebase = false, bool syncCrashlytics = false, bool persistent = false}) { _log(LogLevel.info, msg, tag: tag, error: error, stackTrace: stackTrace, syncFirebase: syncFirebase, persistent: persistent); } static void w(String msg, {String? tag, dynamic error, StackTrace? stackTrace, bool syncFirebase = false, bool syncCrashlytics = false, bool persistent = false}) { _log(LogLevel.warning, msg, tag: tag, error: error, stackTrace: stackTrace, syncFirebase: syncFirebase, persistent: persistent); } static void e(String msg, {String? tag, dynamic error, StackTrace? stackTrace, bool syncFirebase = false, bool syncCrashlytics = false, bool persistent = false}) { _log(LogLevel.error, msg, tag: tag, error: error, stackTrace: stackTrace, syncFirebase: syncFirebase, persistent: persistent); } }