2023-03-22 18:36:44 -07:00
|
|
|
import 'dart:async';
|
2022-11-27 13:34:19 -07:00
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
import 'package:flutter/widgets.dart';
|
2023-03-22 18:36:44 -07:00
|
|
|
import 'package:immich_mobile/shared/models/logger_message.model.dart';
|
2023-04-14 06:50:46 -07:00
|
|
|
import 'package:immich_mobile/shared/models/store.dart';
|
2023-03-22 18:36:44 -07:00
|
|
|
import 'package:isar/isar.dart';
|
2022-11-27 13:34:19 -07:00
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import 'package:share_plus/share_plus.dart';
|
|
|
|
|
|
|
|
/// [ImmichLogger] is a custom logger that is built on top of the [logging] package.
|
2023-03-22 18:36:44 -07:00
|
|
|
/// The logs are written to the database and onto console, using `debugPrint` method.
|
2022-11-27 13:34:19 -07:00
|
|
|
///
|
2024-02-23 20:38:57 -07:00
|
|
|
/// The logs are deleted when exceeding the `maxLogEntries` (default 500) property
|
2022-11-27 13:34:19 -07:00
|
|
|
/// in the class.
|
|
|
|
///
|
|
|
|
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
|
|
|
|
/// and generate a csv file.
|
|
|
|
class ImmichLogger {
|
2023-03-22 18:36:44 -07:00
|
|
|
static final ImmichLogger _instance = ImmichLogger._internal();
|
2023-04-14 06:50:46 -07:00
|
|
|
final maxLogEntries = 500;
|
2023-03-22 18:36:44 -07:00
|
|
|
final Isar _db = Isar.getInstance()!;
|
2023-03-26 19:35:52 -07:00
|
|
|
List<LoggerMessage> _msgBuffer = [];
|
2023-03-22 18:36:44 -07:00
|
|
|
Timer? _timer;
|
2022-11-27 13:34:19 -07:00
|
|
|
|
2023-03-22 18:36:44 -07:00
|
|
|
factory ImmichLogger() => _instance;
|
2022-11-27 13:34:19 -07:00
|
|
|
|
2023-03-22 18:36:44 -07:00
|
|
|
ImmichLogger._internal() {
|
2022-11-27 13:34:19 -07:00
|
|
|
_removeOverflowMessages();
|
2023-04-14 06:50:46 -07:00
|
|
|
final int levelId = Store.get(StoreKey.logLevel, 5); // 5 is INFO
|
|
|
|
Logger.root.level = Level.LEVELS[levelId];
|
2023-03-22 18:36:44 -07:00
|
|
|
Logger.root.onRecord.listen(_writeLogToDatabase);
|
2022-11-27 13:34:19 -07:00
|
|
|
}
|
|
|
|
|
2023-04-14 06:50:46 -07:00
|
|
|
set level(Level level) => Logger.root.level = level;
|
|
|
|
|
2023-03-22 18:36:44 -07:00
|
|
|
List<LoggerMessage> get messages {
|
|
|
|
final inDb =
|
|
|
|
_db.loggerMessages.where(sort: Sort.desc).anyId().findAllSync();
|
|
|
|
return _msgBuffer.isEmpty ? inDb : _msgBuffer.reversed.toList() + inDb;
|
2022-11-27 13:34:19 -07:00
|
|
|
}
|
|
|
|
|
2023-03-22 18:36:44 -07:00
|
|
|
void _removeOverflowMessages() {
|
|
|
|
final msgCount = _db.loggerMessages.countSync();
|
|
|
|
if (msgCount > maxLogEntries) {
|
|
|
|
final numberOfEntryToBeDeleted = msgCount - maxLogEntries;
|
2023-03-26 19:35:52 -07:00
|
|
|
_db.writeTxn(
|
|
|
|
() => _db.loggerMessages
|
|
|
|
.where()
|
|
|
|
.limit(numberOfEntryToBeDeleted)
|
|
|
|
.deleteAll(),
|
|
|
|
);
|
2022-11-27 13:34:19 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-22 18:36:44 -07:00
|
|
|
void _writeLogToDatabase(LogRecord record) {
|
2022-11-27 13:34:19 -07:00
|
|
|
debugPrint('[${record.level.name}] [${record.time}] ${record.message}');
|
2023-03-22 18:36:44 -07:00
|
|
|
final lm = LoggerMessage(
|
|
|
|
message: record.message,
|
2024-02-23 20:38:57 -07:00
|
|
|
details: record.error?.toString(),
|
2023-03-22 18:36:44 -07:00
|
|
|
level: record.level.toLogLevel(),
|
|
|
|
createdAt: record.time,
|
|
|
|
context1: record.loggerName,
|
|
|
|
context2: record.stackTrace?.toString(),
|
2022-11-27 13:34:19 -07:00
|
|
|
);
|
2023-03-22 18:36:44 -07:00
|
|
|
_msgBuffer.add(lm);
|
|
|
|
|
|
|
|
// delayed batch writing to database: increases performance when logging
|
|
|
|
// messages in quick succession and reduces NAND wear
|
|
|
|
_timer ??= Timer(const Duration(seconds: 5), _flushBufferToDatabase);
|
|
|
|
}
|
|
|
|
|
|
|
|
void _flushBufferToDatabase() {
|
|
|
|
_timer = null;
|
2023-03-26 19:35:52 -07:00
|
|
|
final buffer = _msgBuffer;
|
|
|
|
_msgBuffer = [];
|
|
|
|
_db.writeTxn(() => _db.loggerMessages.putAll(buffer));
|
2022-11-27 13:34:19 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
void clearLogs() {
|
2023-03-22 18:36:44 -07:00
|
|
|
_timer?.cancel();
|
|
|
|
_timer = null;
|
|
|
|
_msgBuffer.clear();
|
|
|
|
_db.writeTxn(() => _db.loggerMessages.clear());
|
2022-11-27 13:34:19 -07:00
|
|
|
}
|
|
|
|
|
2022-11-28 09:17:27 -07:00
|
|
|
Future<void> shareLogs() async {
|
|
|
|
final tempDir = await getTemporaryDirectory();
|
|
|
|
final dateTime = DateTime.now().toIso8601String();
|
|
|
|
final filePath = '${tempDir.path}/Immich_log_$dateTime.csv';
|
|
|
|
final logFile = await File(filePath).create();
|
|
|
|
final io = logFile.openWrite();
|
|
|
|
try {
|
|
|
|
// Write header
|
|
|
|
io.write("created_at,level,context,message,stacktrace\n");
|
2022-11-27 13:34:19 -07:00
|
|
|
|
2022-11-28 09:17:27 -07:00
|
|
|
// Write messages
|
|
|
|
for (final m in messages) {
|
|
|
|
io.write(
|
|
|
|
'${m.createdAt},${m.level},"${m.context1 ?? ""}","${m.message}","${m.context2 ?? ""}"\n',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
await io.flush();
|
|
|
|
await io.close();
|
2022-11-27 13:34:19 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Share file
|
2023-04-03 14:43:46 -07:00
|
|
|
await Share.shareXFiles(
|
|
|
|
[XFile(filePath)],
|
2022-11-28 09:17:27 -07:00
|
|
|
subject: "Immich logs $dateTime",
|
2022-11-27 13:34:19 -07:00
|
|
|
sharePositionOrigin: Rect.zero,
|
2023-04-03 14:43:46 -07:00
|
|
|
).then(
|
|
|
|
(value) => logFile.delete(),
|
2022-11-27 13:34:19 -07:00
|
|
|
);
|
|
|
|
}
|
2023-03-22 18:36:44 -07:00
|
|
|
|
|
|
|
/// Flush pending log messages to persistent storage
|
|
|
|
void flush() {
|
|
|
|
if (_timer != null) {
|
|
|
|
_timer!.cancel();
|
2023-03-26 19:35:52 -07:00
|
|
|
_db.writeTxnSync(() => _db.loggerMessages.putAllSync(_msgBuffer));
|
2023-03-22 18:36:44 -07:00
|
|
|
}
|
|
|
|
}
|
2022-11-27 13:34:19 -07:00
|
|
|
}
|