mirror of
https://github.com/immich-app/immich.git
synced 2025-04-21 15:36:26 +02:00
refactor(mobile): log service (#16383)
refactor: log service Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
fbd85a89e0
commit
28c664c769
24 changed files with 656 additions and 201 deletions
mobile
analysis_options.yamlpubspec.lockpubspec.yaml
lib
constants
domain
entities
infrastructure
main.dartpages/common
providers
routing
services
utils
widgets/settings
test
|
@ -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
|
||||
|
|
|
@ -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
mobile/lib/domain/interfaces/log.interface.dart
Normal file
16
mobile/lib/domain/interfaces/log.interface.dart
Normal file
|
@ -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
mobile/lib/domain/models/log.model.dart
Normal file
69
mobile/lib/domain/models/log.model.dart
Normal file
|
@ -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
mobile/lib/domain/services/log.service.dart
Normal file
153
mobile/lib/domain/services/log.service.dart
Normal file
|
@ -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;
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
part 'logger_message.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 'InAppLoggerMessage(message: $message, level: $level, createdAt: $createdAt)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Log levels according to dart logging [Level]
|
||||
enum LogLevel {
|
||||
ALL,
|
||||
FINEST,
|
||||
FINER,
|
||||
FINE,
|
||||
CONFIG,
|
||||
INFO,
|
||||
WARNING,
|
||||
SEVERE,
|
||||
SHOUT,
|
||||
OFF,
|
||||
}
|
||||
|
||||
extension LevelExtension on Level {
|
||||
LogLevel toLogLevel() => LogLevel.values[Level.LEVELS.indexOf(this)];
|
||||
}
|
52
mobile/lib/infrastructure/entities/log.entity.dart
Normal file
52
mobile/lib/infrastructure/entities/log.entity.dart
Normal file
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'logger_message.entity.dart';
|
||||
part of 'log.entity.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
53
mobile/lib/infrastructure/repositories/log.repository.dart
Normal file
53
mobile/lib/infrastructure/repositories/log.repository.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class IsarLogRepository extends IsarDatabaseRepository
|
||||
implements ILogRepository {
|
||||
final Isar _db;
|
||||
const IsarLogRepository(super.db) : _db = db;
|
||||
|
||||
@override
|
||||
Future<bool> deleteAll() async {
|
||||
await transaction(() async => await _db.loggerMessages.clear());
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<LogMessage>> getAll() async {
|
||||
final logs =
|
||||
await _db.loggerMessages.where().sortByCreatedAtDesc().findAll();
|
||||
return logs.map((l) => l.toDto()).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> insert(LogMessage log) async {
|
||||
final logEntity = LoggerMessage.fromDto(log);
|
||||
await transaction(() async {
|
||||
await _db.loggerMessages.put(logEntity);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> insertAll(Iterable<LogMessage> logs) async {
|
||||
await transaction(() async {
|
||||
final logEntities =
|
||||
logs.map((log) => LoggerMessage.fromDto(log)).toList();
|
||||
await _db.loggerMessages.putAll(logEntities);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> truncate({int limit = 250}) async {
|
||||
await transaction(() async {
|
||||
final count = await _db.loggerMessages.count();
|
||||
if (count <= limit) return;
|
||||
final toRemove = count - limit;
|
||||
await _db.loggerMessages.where().limit(toRemove).deleteAll();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ import 'package:immich_mobile/providers/theme.provider.dart';
|
|||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
||||
import 'package:immich_mobile/theme/theme_data.dart';
|
||||
|
@ -67,9 +66,6 @@ Future<void> initApp() async {
|
|||
|
||||
await DynamicTheme.fetchSystemPalette();
|
||||
|
||||
// Initialize Immich Logger Service
|
||||
ImmichLogger();
|
||||
|
||||
final log = Logger("ImmichErrorLogger");
|
||||
|
||||
FlutterError.onError = (details) {
|
||||
|
|
|
@ -2,10 +2,11 @@ import 'package:auto_route/auto_route.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
|
@ -17,8 +18,11 @@ class AppLogPage extends HookConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final immichLogger = ImmichLogger();
|
||||
final logMessages = useState(immichLogger.messages);
|
||||
final immichLogger = LogService.I;
|
||||
final shouldReload = useState(false);
|
||||
final logMessages = useFuture(
|
||||
useMemoized(() => immichLogger.getMessages(), [shouldReload.value]),
|
||||
);
|
||||
|
||||
Widget colorStatusIndicator(Color color) {
|
||||
return Column(
|
||||
|
@ -71,7 +75,7 @@ class AppLogPage extends HookConsumerWidget {
|
|||
),
|
||||
onPressed: () {
|
||||
immichLogger.clearLogs();
|
||||
logMessages.value = [];
|
||||
shouldReload.value = !shouldReload.value;
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
|
@ -84,7 +88,7 @@ class AppLogPage extends HookConsumerWidget {
|
|||
size: 20.0,
|
||||
),
|
||||
onPressed: () {
|
||||
immichLogger.shareLogs(iconContext);
|
||||
ImmichLogger.shareLogs(iconContext);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -105,9 +109,9 @@ class AppLogPage extends HookConsumerWidget {
|
|||
separatorBuilder: (context, index) {
|
||||
return const Divider(height: 0);
|
||||
},
|
||||
itemCount: logMessages.value.length,
|
||||
itemCount: logMessages.data?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
var logMessage = logMessages.value[index];
|
||||
var logMessage = logMessages.data![index];
|
||||
return ListTile(
|
||||
onTap: () => context.pushRoute(
|
||||
AppLogDetailRoute(
|
||||
|
@ -128,7 +132,7 @@ class AppLogPage extends HookConsumerWidget {
|
|||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}",
|
||||
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.logger}",
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AppLogDetailPage extends HookConsumerWidget {
|
||||
const AppLogDetailPage({super.key, required this.logMessage});
|
||||
|
||||
final LoggerMessage logMessage;
|
||||
final LogMessage logMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
@ -126,14 +126,14 @@ class AppLogDetailPage extends HookConsumerWidget {
|
|||
child: ListView(
|
||||
children: [
|
||||
buildTextWithCopyButton("MESSAGE", logMessage.message),
|
||||
if (logMessage.details != null)
|
||||
buildTextWithCopyButton("DETAILS", logMessage.details.toString()),
|
||||
if (logMessage.context1 != null)
|
||||
buildLogContext1(logMessage.context1.toString()),
|
||||
if (logMessage.context2 != null)
|
||||
if (logMessage.error != null)
|
||||
buildTextWithCopyButton("DETAILS", logMessage.error.toString()),
|
||||
if (logMessage.logger != null)
|
||||
buildLogContext1(logMessage.logger.toString()),
|
||||
if (logMessage.stack != null)
|
||||
buildTextWithCopyButton(
|
||||
"STACK TRACE",
|
||||
logMessage.context2.toString(),
|
||||
logMessage.stack.toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/providers/notification_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
enum AppLifeCycleEnum {
|
||||
|
@ -112,7 +114,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||
_ref.read(websocketProvider.notifier).disconnect();
|
||||
}
|
||||
|
||||
ImmichLogger().flush();
|
||||
unawaited(LogService.I.flush());
|
||||
}
|
||||
|
||||
void handleAppDetached() {
|
||||
|
|
|
@ -1,44 +1,48 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
||||
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
||||
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
||||
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
||||
import 'package:immich_mobile/pages/library/places/places_collection.page.dart';
|
||||
import 'package:immich_mobile/pages/library/library.page.dart';
|
||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||
import 'package:immich_mobile/pages/album/album_additional_shared_user_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/album/album_asset_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/album/album_options.page.dart';
|
||||
import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/album/album_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||
import 'package:immich_mobile/pages/common/app_log.page.dart';
|
||||
import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
|
||||
import 'package:immich_mobile/pages/common/create_album.page.dart';
|
||||
import 'package:immich_mobile/pages/common/gallery_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/common/headers_settings.page.dart';
|
||||
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/common/settings.page.dart';
|
||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
|
||||
import 'package:immich_mobile/pages/editing/edit.page.dart';
|
||||
import 'package:immich_mobile/pages/editing/crop.page.dart';
|
||||
import 'package:immich_mobile/pages/editing/edit.page.dart';
|
||||
import 'package:immich_mobile/pages/editing/filter.page.dart';
|
||||
import 'package:immich_mobile/pages/library/archive.page.dart';
|
||||
import 'package:immich_mobile/pages/library/favorite.page.dart';
|
||||
import 'package:immich_mobile/pages/library/library.page.dart';
|
||||
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
||||
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
||||
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
||||
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
||||
import 'package:immich_mobile/pages/library/places/places_collection.page.dart';
|
||||
import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
|
||||
import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart';
|
||||
import 'package:immich_mobile/pages/library/trash.page.dart';
|
||||
import 'package:immich_mobile/pages/login/change_password.page.dart';
|
||||
import 'package:immich_mobile/pages/login/login.page.dart';
|
||||
|
@ -54,10 +58,6 @@ import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart';
|
|||
import 'package:immich_mobile/pages/search/person_result.page.dart';
|
||||
import 'package:immich_mobile/pages/search/recently_added.page.dart';
|
||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
||||
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
||||
import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
|
||||
import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart';
|
||||
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
|
|
|
@ -386,7 +386,7 @@ class AllVideosRoute extends PageRouteInfo<void> {
|
|||
class AppLogDetailRoute extends PageRouteInfo<AppLogDetailRouteArgs> {
|
||||
AppLogDetailRoute({
|
||||
Key? key,
|
||||
required LoggerMessage logMessage,
|
||||
required LogMessage logMessage,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AppLogDetailRoute.name,
|
||||
|
@ -419,7 +419,7 @@ class AppLogDetailRouteArgs {
|
|||
|
||||
final Key? key;
|
||||
|
||||
final LoggerMessage logMessage;
|
||||
final LogMessage logMessage;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
|
|
@ -2,11 +2,7 @@ import 'dart:async';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
|
@ -18,75 +14,10 @@ import 'package:share_plus/share_plus.dart';
|
|||
///
|
||||
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
|
||||
/// and generate a csv file.
|
||||
class ImmichLogger {
|
||||
static final ImmichLogger _instance = ImmichLogger._internal();
|
||||
final maxLogEntries = 500;
|
||||
final Isar _db = Isar.getInstance()!;
|
||||
List<LoggerMessage> _msgBuffer = [];
|
||||
Timer? _timer;
|
||||
abstract final class ImmichLogger {
|
||||
const ImmichLogger();
|
||||
|
||||
factory ImmichLogger() => _instance;
|
||||
|
||||
ImmichLogger._internal() {
|
||||
_removeOverflowMessages();
|
||||
final int levelId = Store.get(StoreKey.logLevel, 5); // 5 is INFO
|
||||
Logger.root.level = Level.LEVELS[levelId];
|
||||
Logger.root.onRecord.listen(_writeLogToDatabase);
|
||||
}
|
||||
|
||||
set level(Level level) => Logger.root.level = level;
|
||||
|
||||
List<LoggerMessage> get messages {
|
||||
final inDb =
|
||||
_db.loggerMessages.where(sort: Sort.desc).anyId().findAllSync();
|
||||
return _msgBuffer.isEmpty ? inDb : _msgBuffer.reversed.toList() + inDb;
|
||||
}
|
||||
|
||||
void _removeOverflowMessages() {
|
||||
final msgCount = _db.loggerMessages.countSync();
|
||||
if (msgCount > maxLogEntries) {
|
||||
final numberOfEntryToBeDeleted = msgCount - maxLogEntries;
|
||||
_db.writeTxn(
|
||||
() => _db.loggerMessages
|
||||
.where()
|
||||
.limit(numberOfEntryToBeDeleted)
|
||||
.deleteAll(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _writeLogToDatabase(LogRecord record) {
|
||||
debugPrint('[${record.level.name}] [${record.time}] ${record.message}');
|
||||
final lm = LoggerMessage(
|
||||
message: record.message,
|
||||
details: record.error?.toString(),
|
||||
level: record.level.toLogLevel(),
|
||||
createdAt: record.time,
|
||||
context1: record.loggerName,
|
||||
context2: record.stackTrace?.toString(),
|
||||
);
|
||||
_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;
|
||||
final buffer = _msgBuffer;
|
||||
_msgBuffer = [];
|
||||
_db.writeTxn(() => _db.loggerMessages.putAll(buffer));
|
||||
}
|
||||
|
||||
void clearLogs() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_msgBuffer.clear();
|
||||
_db.writeTxn(() => _db.loggerMessages.clear());
|
||||
}
|
||||
|
||||
Future<void> shareLogs(BuildContext context) async {
|
||||
static Future<void> shareLogs(BuildContext context) async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final dateTime = DateTime.now().toIso8601String();
|
||||
final filePath = '${tempDir.path}/Immich_log_$dateTime.log';
|
||||
|
@ -94,13 +25,13 @@ class ImmichLogger {
|
|||
final io = logFile.openWrite();
|
||||
try {
|
||||
// Write messages
|
||||
for (final m in messages) {
|
||||
for (final m in await LogService.I.getMessages()) {
|
||||
final created = m.createdAt;
|
||||
final level = m.level.name.padRight(8);
|
||||
final logger = (m.context1 ?? "<UNKNOWN_LOGGER>").padRight(20);
|
||||
final logger = (m.logger ?? "<UNKNOWN_LOGGER>").padRight(20);
|
||||
final message = m.message;
|
||||
final error = m.details != null ? " ${m.details} |" : "";
|
||||
final stack = m.context2 != null ? "\n${m.context2!}" : "";
|
||||
final error = m.error == null ? "" : " ${m.error} |";
|
||||
final stack = m.stack == null ? "" : "\n${m.stack!}";
|
||||
io.write('$created | $level | $logger | $message |$error$stack\n');
|
||||
}
|
||||
} finally {
|
||||
|
@ -115,16 +46,6 @@ class ImmichLogger {
|
|||
[XFile(filePath)],
|
||||
subject: "Immich logs $dateTime",
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
).then(
|
||||
(value) => logFile.delete(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Flush pending log messages to persistent storage
|
||||
void flush() {
|
||||
if (_timer != null) {
|
||||
_timer!.cancel();
|
||||
_db.writeTxnSync(() => _db.loggerMessages.putAllSync(_msgBuffer));
|
||||
}
|
||||
).then((value) => logFile.delete());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
||||
|
@ -10,9 +11,10 @@ import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
|||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
@ -46,5 +48,9 @@ abstract final class Bootstrap {
|
|||
|
||||
static Future<void> initDomain(Isar db) async {
|
||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
||||
await LogService.init(
|
||||
logRepo: IsarLogRepository(db),
|
||||
storeRepo: IsarStoreRepository(db),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/local_storage_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
import 'package:immich_mobile/widgets/settings/ssl_client_cert_settings.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
|
@ -33,7 +34,8 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||
|
||||
useValueChanged(
|
||||
levelId.value,
|
||||
(_, __) => ImmichLogger().level = Level.LEVELS[levelId.value],
|
||||
(_, __) =>
|
||||
LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()),
|
||||
);
|
||||
|
||||
final advancedSettings = [
|
||||
|
|
|
@ -407,7 +407,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.0.2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
|
|
|
@ -113,6 +113,7 @@ dev_dependencies:
|
|||
mocktail: ^1.0.3
|
||||
immich_mobile_immich_lint:
|
||||
path: './immich_lint'
|
||||
fake_async: ^1.3.1
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
|
186
mobile/test/domain/services/log_service_test.dart
Normal file
186
mobile/test/domain/services/log_service_test.dart
Normal file
|
@ -0,0 +1,186 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
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:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../infrastructure/repository.mock.dart';
|
||||
import '../../test_utils.dart';
|
||||
|
||||
final _kInfoLog = LogMessage(
|
||||
message: '#Info Message',
|
||||
level: LogLevel.INFO,
|
||||
createdAt: DateTime(2025, 2, 26),
|
||||
logger: 'Info Logger',
|
||||
);
|
||||
|
||||
final _kWarnLog = LogMessage(
|
||||
message: '#Warn Message',
|
||||
level: LogLevel.WARNING,
|
||||
createdAt: DateTime(2025, 2, 27),
|
||||
logger: 'Warn Logger',
|
||||
);
|
||||
|
||||
void main() {
|
||||
late LogService sut;
|
||||
late ILogRepository mockLogRepo;
|
||||
late IStoreRepository mockStoreRepo;
|
||||
|
||||
setUp(() async {
|
||||
mockLogRepo = MockLogRepository();
|
||||
mockStoreRepo = MockStoreRepository();
|
||||
|
||||
registerFallbackValue(_kInfoLog);
|
||||
|
||||
when(() => mockLogRepo.truncate(limit: any(named: 'limit')))
|
||||
.thenAnswer((_) async => {});
|
||||
when(() => mockStoreRepo.tryGet<int>(StoreKey.logLevel))
|
||||
.thenAnswer((_) async => LogLevel.FINE.index);
|
||||
when(() => mockLogRepo.getAll()).thenAnswer((_) async => []);
|
||||
when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true);
|
||||
when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true);
|
||||
|
||||
sut =
|
||||
await LogService.create(logRepo: mockLogRepo, storeRepo: mockStoreRepo);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await sut.dispose();
|
||||
});
|
||||
|
||||
group("Log Service Init:", () {
|
||||
test('Truncates the existing logs on init', () {
|
||||
final limit =
|
||||
verify(() => mockLogRepo.truncate(limit: captureAny(named: 'limit')))
|
||||
.captured
|
||||
.firstOrNull as int?;
|
||||
expect(limit, kLogTruncateLimit);
|
||||
});
|
||||
|
||||
test('Sets log level based on the store setting', () {
|
||||
verify(() => mockStoreRepo.tryGet<int>(StoreKey.logLevel)).called(1);
|
||||
expect(Logger.root.level, Level.FINE);
|
||||
});
|
||||
});
|
||||
|
||||
group("Log Service Set Level:", () {
|
||||
setUp(() async {
|
||||
when(() => mockStoreRepo.insert<int>(StoreKey.logLevel, any()))
|
||||
.thenAnswer((_) async => true);
|
||||
await sut.setlogLevel(LogLevel.SHOUT);
|
||||
});
|
||||
|
||||
test('Updates the log level in store', () {
|
||||
final index = verify(
|
||||
() => mockStoreRepo.insert<int>(StoreKey.logLevel, captureAny()),
|
||||
).captured.firstOrNull;
|
||||
expect(index, LogLevel.SHOUT.index);
|
||||
});
|
||||
|
||||
test('Sets log level on logger', () {
|
||||
expect(Logger.root.level, Level.SHOUT);
|
||||
});
|
||||
});
|
||||
|
||||
group("Log Service Buffer:", () {
|
||||
test('Buffers logs until timer elapses', () {
|
||||
TestUtils.fakeAsync((time) async {
|
||||
sut = await LogService.create(
|
||||
logRepo: mockLogRepo,
|
||||
storeRepo: mockStoreRepo,
|
||||
shouldBuffer: true,
|
||||
);
|
||||
|
||||
final logger = Logger(_kInfoLog.logger!);
|
||||
logger.info(_kInfoLog.message);
|
||||
expect(await sut.getMessages(), hasLength(1));
|
||||
logger.warning(_kWarnLog.message);
|
||||
expect(await sut.getMessages(), hasLength(2));
|
||||
time.elapse(const Duration(seconds: 6));
|
||||
expect(await sut.getMessages(), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
test('Batch inserts all logs on timer', () {
|
||||
TestUtils.fakeAsync((time) async {
|
||||
sut = await LogService.create(
|
||||
logRepo: mockLogRepo,
|
||||
storeRepo: mockStoreRepo,
|
||||
shouldBuffer: true,
|
||||
);
|
||||
|
||||
final logger = Logger(_kInfoLog.logger!);
|
||||
logger.info(_kInfoLog.message);
|
||||
time.elapse(const Duration(seconds: 6));
|
||||
final insert = verify(() => mockLogRepo.insertAll(captureAny()));
|
||||
insert.called(1);
|
||||
// ignore: prefer-correct-json-casts
|
||||
final captured = insert.captured.firstOrNull as List<LogMessage>;
|
||||
expect(captured.firstOrNull?.message, _kInfoLog.message);
|
||||
expect(captured.firstOrNull?.logger, _kInfoLog.logger);
|
||||
|
||||
verifyNever(() => mockLogRepo.insert(captureAny()));
|
||||
});
|
||||
});
|
||||
|
||||
test('Does not buffer when off', () {
|
||||
TestUtils.fakeAsync((time) async {
|
||||
sut = await LogService.create(
|
||||
logRepo: mockLogRepo,
|
||||
storeRepo: mockStoreRepo,
|
||||
shouldBuffer: false,
|
||||
);
|
||||
|
||||
final logger = Logger(_kInfoLog.logger!);
|
||||
logger.info(_kInfoLog.message);
|
||||
// Ensure nothing gets buffer. This works because we mock log repo getAll to return nothing
|
||||
expect(await sut.getMessages(), isEmpty);
|
||||
|
||||
final insert = verify(() => mockLogRepo.insert(captureAny()));
|
||||
insert.called(1);
|
||||
final captured = insert.captured.firstOrNull as LogMessage;
|
||||
expect(captured.message, _kInfoLog.message);
|
||||
expect(captured.logger, _kInfoLog.logger);
|
||||
|
||||
verifyNever(() => mockLogRepo.insertAll(captureAny()));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group("Log Service Get messages:", () {
|
||||
setUp(() {
|
||||
when(() => mockLogRepo.getAll()).thenAnswer((_) async => [_kInfoLog]);
|
||||
});
|
||||
|
||||
test('Fetches result from DB', () async {
|
||||
expect(await sut.getMessages(), hasLength(1));
|
||||
verify(() => mockLogRepo.getAll()).called(1);
|
||||
});
|
||||
|
||||
test('Combines result from both DB + Buffer', () {
|
||||
TestUtils.fakeAsync((time) async {
|
||||
sut = await LogService.create(
|
||||
logRepo: mockLogRepo,
|
||||
storeRepo: mockStoreRepo,
|
||||
shouldBuffer: true,
|
||||
);
|
||||
|
||||
final logger = Logger(_kWarnLog.logger!);
|
||||
logger.warning(_kWarnLog.message);
|
||||
expect(await sut.getMessages(), hasLength(2)); // 1 - DB, 1 - Buff
|
||||
|
||||
final messages = await sut.getMessages();
|
||||
// Logged time is assigned in the service for messages in the buffer, so compare manually
|
||||
expect(messages.firstOrNull?.message, _kWarnLog.message);
|
||||
expect(messages.firstOrNull?.logger, _kWarnLog.logger);
|
||||
|
||||
expect(messages.elementAtOrNull(1), _kInfoLog);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockStoreRepository extends Mock implements IStoreRepository {}
|
||||
|
||||
class MockLogRepository extends Mock implements ILogRepository {}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
|
@ -70,7 +71,10 @@ void main() {
|
|||
db.writeTxnSync(() => db.clearSync());
|
||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
||||
await Store.put(StoreKey.currentUser, owner);
|
||||
ImmichLogger();
|
||||
await LogService.init(
|
||||
logRepo: IsarLogRepository(db),
|
||||
storeRepo: IsarStoreRepository(db),
|
||||
);
|
||||
});
|
||||
final List<Asset> initialAssets = [
|
||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fake_async/fake_async.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
|
@ -11,8 +13,8 @@ import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
|||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
@ -88,4 +90,36 @@ abstract final class TestUtils {
|
|||
WidgetController.hitTestWarningShouldBeFatal = true;
|
||||
HttpOverrides.global = MockHttpOverrides();
|
||||
}
|
||||
|
||||
// Workaround till the following issue is resolved
|
||||
// https://github.com/dart-lang/test/issues/2307
|
||||
static T fakeAsync<T>(
|
||||
Future<T> Function(FakeAsync _) callback, {
|
||||
DateTime? initialTime,
|
||||
}) {
|
||||
late final T result;
|
||||
Object? error;
|
||||
StackTrace? stack;
|
||||
FakeAsync(initialTime: initialTime).run((FakeAsync async) {
|
||||
bool shouldPump = true;
|
||||
unawaited(
|
||||
callback(async).then<void>(
|
||||
(value) => result = value,
|
||||
onError: (e, s) {
|
||||
error = e;
|
||||
stack = s;
|
||||
},
|
||||
).whenComplete(() => shouldPump = false),
|
||||
);
|
||||
|
||||
while (shouldPump) {
|
||||
async.flushMicrotasks();
|
||||
}
|
||||
});
|
||||
|
||||
if (error != null) {
|
||||
Error.throwWithStackTrace(error!, stack!);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue