import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/interfaces/log.interface.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/service_locator.dart'; import 'package:logging/logging.dart' as logging; /// [LogManager] is a custom logger that is built on top of the [logging] package. /// The logs are written to the database and onto console, using `debugPrint` method. /// /// The logs are deleted when exceeding the `maxLogEntries` (default 500) property /// in the class. class LogManager { LogManager._(); static final LogManager _instance = LogManager._(); static final Map _loggers = {}; // ignore: match-getter-setter-field-names static LogManager get I => _instance; List _msgBuffer = []; Timer? _timer; /// Whether to buffer logs in memory before writing to the database. /// This is useful when logging in quick succession, as it increases performance /// and reduces NAND wear. However, it may cause the logs to be lost in case of a crash / in isolates. bool _shouldBuffer = true; late final StreamSubscription _subscription; void _onLogRecord(logging.LogRecord record) { // Only print in development assert(() { debugPrint('[${record.level.name}] [${record.time}] ${record.message}'); if (record.error != null && record.stackTrace != null) { debugPrint('${record.error}'); debugPrint('${record.stackTrace}'); } return true; }()); final lm = LogMessage( logger: record.loggerName, content: record.message, level: record.level.toLogLevel(), createdAt: record.time, error: record.error?.toString(), stack: record.stackTrace?.toString(), ); if (_shouldBuffer) { _msgBuffer.add(lm); _timer ??= Timer(const Duration(seconds: 5), () => _flushBufferToDatabase()); } else { di().create(lm); } } void _flushBufferToDatabase() { _timer = null; final buffer = _msgBuffer; _msgBuffer = []; di().createAll(buffer); } void init({bool? shouldBuffer}) { _shouldBuffer = shouldBuffer ?? _shouldBuffer; _subscription = logging.Logger.root.onRecord.listen(_onLogRecord); } Logger get(String? loggerName) => _loggers.putIfAbsent( loggerName ?? 'main', () => Logger(loggerName ?? 'main'), ); void updateLevel(LogLevel level) { logging.Logger.root.level = logging.Level.LEVELS.elementAtOrNull(level.index); } void dispose() { _subscription.cancel(); } void clearLogs() { _timer?.cancel(); _timer = null; _msgBuffer.clear(); di().deleteAll(); } static void setGlobalErrorCallbacks() { FlutterError.onError = (details) { LogManager.I.get("FlutterError").wtf( 'Unknown framework error occured in library ${details.library ?? ""} at node ${details.context ?? ""}', details.exception, details.stack, ); FlutterError.presentError(details); }; PlatformDispatcher.instance.onError = (error, stack) { LogManager.I .get("PlatformDispatcher") .wtf('Unknown error occured in root isolate', error, stack); return true; }; } } class Logger { final String _loggerName; const Logger(this._loggerName); logging.Logger get _logger => logging.Logger(_loggerName); /// Finest / Verbose logs. Useful for highly detailed messages void v(String message) => _logger.finest(message); /// Fine / Debug logs. Useful for troubleshooting void d(String message) => _logger.fine(message); /// Info logs. Useful for general logging void i(String message) => _logger.info(message); /// Warning logs. Useful to identify potential issues void w(String message, [Object? error, StackTrace? stack]) => _logger.warning(message, error, stack); /// Error logs. Useful for identifying issues void e(String message, [Object? error, StackTrace? stack]) => _logger.severe(message, error, stack); /// Crash / Serious failure logs. Shouldn't happen void wtf(String message, [Object? error, StackTrace? stack]) => _logger.shout(message, error, stack); }