Skip to content

Commit

Permalink
refactor(mobile): log service (immich-app#16383)
Browse files Browse the repository at this point in the history
refactor: log service

Co-authored-by: shenlong-tanwen <[email protected]>
  • Loading branch information
shenlong-tanwen and shenlong-tanwen authored Feb 27, 2025
1 parent fbd85a8 commit 28c664c
Show file tree
Hide file tree
Showing 24 changed files with 654 additions and 199 deletions.
2 changes: 1 addition & 1 deletion mobile/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ custom_lint:
- lib/entities/*.entity.dart
- lib/repositories/{album,asset,backup,database,etag,exif_info,user,timeline,partner}.repository.dart
- lib/infrastructure/entities/*.entity.dart
- lib/infrastructure/repositories/{store,db}.repository.dart
- lib/infrastructure/repositories/{store,db,log}.repository.dart
- lib/providers/infrastructure/db.provider.dart
# acceptable exceptions for the time being (until Isar is fully replaced)
- integration_test/test_utils/general_helper.dart
Expand Down
3 changes: 3 additions & 0 deletions mobile/lib/constants/constants.dart
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;
16 changes: 16 additions & 0 deletions mobile/lib/domain/interfaces/log.interface.dart
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});
}
69 changes: 69 additions & 0 deletions mobile/lib/domain/models/log.model.dart
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>'},
}''';
}
}
153 changes: 153 additions & 0 deletions mobile/lib/domain/services/log.service.dart
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;
}
50 changes: 0 additions & 50 deletions mobile/lib/entities/logger_message.entity.dart

This file was deleted.

52 changes: 52 additions & 0 deletions mobile/lib/infrastructure/entities/log.entity.dart
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,
);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 28c664c

Please sign in to comment.