forked from immich-app/immich
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(mobile): log service (immich-app#16383)
refactor: log service Co-authored-by: shenlong-tanwen <[email protected]>
- Loading branch information
1 parent
fbd85a8
commit 28c664c
Showing
24 changed files
with
654 additions
and
199 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
const int noDbId = -9223372036854775808; // from Isar | ||
const double downloadCompleted = -1; | ||
const double downloadFailed = -2; | ||
|
||
// Number of log entries to retain on app start | ||
const int kLogTruncateLimit = 250; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import 'dart:async'; | ||
|
||
import 'package:immich_mobile/domain/models/log.model.dart'; | ||
|
||
abstract interface class ILogRepository { | ||
Future<bool> insert(LogMessage log); | ||
|
||
Future<bool> insertAll(Iterable<LogMessage> logs); | ||
|
||
Future<List<LogMessage>> getAll(); | ||
|
||
Future<bool> deleteAll(); | ||
|
||
/// Truncates the logs to the most recent [limit]. Defaults to recent 250 logs | ||
Future<void> truncate({int limit = 250}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// ignore_for_file: constant_identifier_names | ||
|
||
import 'package:logging/logging.dart'; | ||
|
||
/// Log levels according to dart logging [Level] | ||
enum LogLevel { | ||
ALL, | ||
FINEST, | ||
FINER, | ||
FINE, | ||
CONFIG, | ||
INFO, | ||
WARNING, | ||
SEVERE, | ||
SHOUT, | ||
OFF, | ||
} | ||
|
||
class LogMessage { | ||
final String message; | ||
final LogLevel level; | ||
final DateTime createdAt; | ||
final String? logger; | ||
final String? error; | ||
final String? stack; | ||
|
||
const LogMessage({ | ||
required this.message, | ||
required this.level, | ||
required this.createdAt, | ||
this.logger, | ||
this.error, | ||
this.stack, | ||
}); | ||
|
||
@override | ||
bool operator ==(covariant LogMessage other) { | ||
if (identical(this, other)) return true; | ||
|
||
return other.message == message && | ||
other.level == level && | ||
other.createdAt == createdAt && | ||
other.logger == logger && | ||
other.error == error && | ||
other.stack == stack; | ||
} | ||
|
||
@override | ||
int get hashCode { | ||
return message.hashCode ^ | ||
level.hashCode ^ | ||
createdAt.hashCode ^ | ||
logger.hashCode ^ | ||
error.hashCode ^ | ||
stack.hashCode; | ||
} | ||
|
||
@override | ||
String toString() { | ||
return '''LogMessage: { | ||
message: $message, | ||
level: $level, | ||
createdAt: $createdAt, | ||
logger: ${logger ?? '<NA>'}, | ||
error: ${error ?? '<NA>'}, | ||
stack: ${stack ?? '<NA>'}, | ||
}'''; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import 'dart:async'; | ||
|
||
import 'package:immich_mobile/constants/constants.dart'; | ||
import 'package:immich_mobile/domain/interfaces/log.interface.dart'; | ||
import 'package:immich_mobile/domain/interfaces/store.interface.dart'; | ||
import 'package:immich_mobile/domain/models/log.model.dart'; | ||
import 'package:immich_mobile/domain/models/store.model.dart'; | ||
import 'package:logging/logging.dart'; | ||
|
||
class LogService { | ||
final ILogRepository _logRepository; | ||
final IStoreRepository _storeRepository; | ||
|
||
final List<LogMessage> _msgBuffer = []; | ||
|
||
/// 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. | ||
final bool _shouldBuffer; | ||
Timer? _flushTimer; | ||
|
||
late final StreamSubscription<LogRecord> _logSubscription; | ||
|
||
LogService._( | ||
this._logRepository, | ||
this._storeRepository, | ||
this._shouldBuffer, | ||
) { | ||
// Listen to log messages and write them to the database | ||
_logSubscription = Logger.root.onRecord.listen(_writeLogToDatabase); | ||
} | ||
|
||
static LogService? _instance; | ||
static LogService get I { | ||
if (_instance == null) { | ||
throw const LoggerUnInitializedException(); | ||
} | ||
return _instance!; | ||
} | ||
|
||
static Future<LogService> init({ | ||
required ILogRepository logRepo, | ||
required IStoreRepository storeRepo, | ||
bool shouldBuffer = true, | ||
}) async { | ||
if (_instance != null) { | ||
return _instance!; | ||
} | ||
_instance = await create( | ||
logRepo: logRepo, | ||
storeRepo: storeRepo, | ||
shouldBuffer: shouldBuffer, | ||
); | ||
return _instance!; | ||
} | ||
|
||
static Future<LogService> create({ | ||
required ILogRepository logRepo, | ||
required IStoreRepository storeRepo, | ||
bool shouldBuffer = true, | ||
}) async { | ||
final instance = LogService._(logRepo, storeRepo, shouldBuffer); | ||
// Truncate logs to 250 | ||
await logRepo.truncate(limit: kLogTruncateLimit); | ||
// Get log level from store | ||
final level = await instance._storeRepository.tryGet(StoreKey.logLevel); | ||
if (level != null) { | ||
Logger.root.level = Level.LEVELS.elementAtOrNull(level) ?? Level.INFO; | ||
} | ||
return instance; | ||
} | ||
|
||
Future<void> setlogLevel(LogLevel level) async { | ||
await _storeRepository.insert(StoreKey.logLevel, level.index); | ||
Logger.root.level = level.toLevel(); | ||
} | ||
|
||
Future<List<LogMessage>> getMessages() async { | ||
final logsFromDb = await _logRepository.getAll(); | ||
if (_msgBuffer.isNotEmpty) { | ||
return [..._msgBuffer.reversed, ...logsFromDb]; | ||
} | ||
return logsFromDb; | ||
} | ||
|
||
Future<void> clearLogs() async { | ||
_flushTimer?.cancel(); | ||
_flushTimer = null; | ||
_msgBuffer.clear(); | ||
await _logRepository.deleteAll(); | ||
} | ||
|
||
/// Flush pending log messages to persistent storage | ||
Future<void> flush() async { | ||
if (_flushTimer == null) { | ||
return; | ||
} | ||
_flushTimer!.cancel(); | ||
await _flushBufferToDatabase(); | ||
} | ||
|
||
Future<void> dispose() { | ||
_flushTimer?.cancel(); | ||
_logSubscription.cancel(); | ||
return _flushBufferToDatabase(); | ||
} | ||
|
||
void _writeLogToDatabase(LogRecord r) { | ||
final record = LogMessage( | ||
message: r.message, | ||
level: r.level.toLogLevel(), | ||
createdAt: r.time, | ||
logger: r.loggerName, | ||
error: r.error?.toString(), | ||
stack: r.stackTrace?.toString(), | ||
); | ||
|
||
if (_shouldBuffer) { | ||
_msgBuffer.add(record); | ||
_flushTimer ??= Timer( | ||
const Duration(seconds: 5), | ||
() => unawaited(_flushBufferToDatabase()), | ||
); | ||
} else { | ||
unawaited(_logRepository.insert(record)); | ||
} | ||
} | ||
|
||
Future<void> _flushBufferToDatabase() async { | ||
_flushTimer = null; | ||
final buffer = [..._msgBuffer]; | ||
_msgBuffer.clear(); | ||
await _logRepository.insertAll(buffer); | ||
} | ||
} | ||
|
||
class LoggerUnInitializedException implements Exception { | ||
const LoggerUnInitializedException(); | ||
|
||
@override | ||
String toString() => 'Logger is not initialized. Call init()'; | ||
} | ||
|
||
/// Log levels according to dart logging [Level] | ||
extension LevelDomainToInfraExtension on Level { | ||
LogLevel toLogLevel() => | ||
LogLevel.values.elementAtOrNull(Level.LEVELS.indexOf(this)) ?? | ||
LogLevel.INFO; | ||
} | ||
|
||
extension on LogLevel { | ||
Level toLevel() => Level.LEVELS.elementAtOrNull(index) ?? Level.INFO; | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import 'package:immich_mobile/domain/models/log.model.dart'; | ||
import 'package:isar/isar.dart'; | ||
|
||
part 'log.entity.g.dart'; | ||
|
||
@Collection(inheritance: false) | ||
class LoggerMessage { | ||
Id id = Isar.autoIncrement; | ||
String message; | ||
String? details; | ||
@Enumerated(EnumType.ordinal) | ||
LogLevel level = LogLevel.INFO; | ||
DateTime createdAt; | ||
String? context1; | ||
String? context2; | ||
|
||
LoggerMessage({ | ||
required this.message, | ||
required this.details, | ||
required this.level, | ||
required this.createdAt, | ||
required this.context1, | ||
required this.context2, | ||
}); | ||
|
||
@override | ||
String toString() { | ||
return 'LoggerMessage(message: $message, level: $level, createdAt: $createdAt)'; | ||
} | ||
|
||
LogMessage toDto() { | ||
return LogMessage( | ||
message: message, | ||
level: level, | ||
createdAt: createdAt, | ||
logger: context1, | ||
error: details, | ||
stack: context2, | ||
); | ||
} | ||
|
||
static LoggerMessage fromDto(LogMessage log) { | ||
return LoggerMessage( | ||
message: log.message, | ||
details: log.error, | ||
level: log.level, | ||
createdAt: log.createdAt, | ||
context1: log.logger, | ||
context2: log.stack, | ||
); | ||
} | ||
} |
2 changes: 1 addition & 1 deletion
2
...lib/entities/logger_message.entity.g.dart → ...infrastructure/entities/log.entity.g.dart
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.