feat: use sqlite for logging (#20414)

* feat: use drift for logging

* fix: tests

* feat: use the truncate limit from constants.ts as default

* chore: move setupAll to top level and restructure

* chore: code review changes

* fix: inherits

* feat: raise log line limit to 2000

* limit getAll to 250 lines

* delete DLog and make LogRepository not a singleton

* fix: drift build settings and `make migration`

* fix: tests

* remove sensitive log

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Brandon Wees 2025-08-06 10:49:29 -05:00 committed by GitHub
parent f2067221c5
commit 3cd7f5ab90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 155 additions and 241 deletions

View file

@ -19,6 +19,7 @@ targets:
- lib/infrastructure/entities/*.dart - lib/infrastructure/entities/*.dart
- lib/infrastructure/entities/*.drift - lib/infrastructure/entities/*.drift
- lib/infrastructure/repositories/db.repository.dart - lib/infrastructure/repositories/db.repository.dart
- lib/infrastructure/repositories/logger_db.repository.dart
drift_dev:modular: drift_dev:modular:
enabled: true enabled: true
options: *drift_options options: *drift_options

View file

@ -3,7 +3,7 @@ const double downloadCompleted = -1;
const double downloadFailed = -2; const double downloadFailed = -2;
// Number of log entries to retain on app start // Number of log entries to retain on app start
const int kLogTruncateLimit = 250; const int kLogTruncateLimit = 2000;
// Sync // Sync
const int kSyncEventBatchSize = 5000; const int kSyncEventBatchSize = 5000;

View file

@ -6,7 +6,6 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
class HashService { class HashService {
@ -46,7 +45,6 @@ class HashService {
stopwatch.stop(); stopwatch.stop();
_log.info("Hashing took - ${stopwatch.elapsedMilliseconds}ms"); _log.info("Hashing took - ${stopwatch.elapsedMilliseconds}ms");
DLog.log("Hashing took - ${stopwatch.elapsedMilliseconds}ms");
} }
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB /// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
@ -101,7 +99,6 @@ class HashService {
} }
_log.fine("Hashed ${hashed.length}/${toHash.length} assets"); _log.fine("Hashed ${hashed.length}/${toHash.length} assets");
DLog.log("Hashed ${hashed.length}/${toHash.length} assets");
await _localAssetRepository.updateHashes(hashed); await _localAssetRepository.updateHashes(hashed);
await _storageRepository.clearCache(); await _storageRepository.clearCache();

View file

@ -6,7 +6,6 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
@ -30,19 +29,17 @@ class LocalSyncService {
try { try {
if (full || await _nativeSyncApi.shouldFullSync()) { if (full || await _nativeSyncApi.shouldFullSync()) {
_log.fine("Full sync request from ${full ? "user" : "native"}"); _log.fine("Full sync request from ${full ? "user" : "native"}");
DLog.log("Full sync request from ${full ? "user" : "native"}");
return await fullSync(); return await fullSync();
} }
final delta = await _nativeSyncApi.getMediaChanges(); final delta = await _nativeSyncApi.getMediaChanges();
if (!delta.hasChanges) { if (!delta.hasChanges) {
_log.fine("No media changes detected. Skipping sync"); _log.fine("No media changes detected. Skipping sync");
DLog.log("No media changes detected. Skipping sync");
return; return;
} }
DLog.log("Delta updated: ${delta.updates.length}"); _log.fine("Delta updated: ${delta.updates.length}");
DLog.log("Delta deleted: ${delta.deletes.length}"); _log.fine("Delta deleted: ${delta.deletes.length}");
final deviceAlbums = await _nativeSyncApi.getAlbums(); final deviceAlbums = await _nativeSyncApi.getAlbums();
await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums()); await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums());
@ -83,7 +80,6 @@ class LocalSyncService {
} finally { } finally {
stopwatch.stop(); stopwatch.stop();
_log.info("Device sync took - ${stopwatch.elapsedMilliseconds}ms"); _log.info("Device sync took - ${stopwatch.elapsedMilliseconds}ms");
DLog.log("Device sync took - ${stopwatch.elapsedMilliseconds}ms");
} }
} }
@ -106,7 +102,6 @@ class LocalSyncService {
await _nativeSyncApi.checkpointSync(); await _nativeSyncApi.checkpointSync();
stopwatch.stop(); stopwatch.stop();
_log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); _log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
DLog.log("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
} catch (e, s) { } catch (e, s) {
_log.severe("Error performing full device sync", e, s); _log.severe("Error performing full device sync", e, s);
} }
@ -150,7 +145,6 @@ class LocalSyncService {
// Faster path - only new assets added // Faster path - only new assets added
if (await checkAddition(dbAlbum, deviceAlbum)) { if (await checkAddition(dbAlbum, deviceAlbum)) {
_log.fine("Fast synced device album ${dbAlbum.name}"); _log.fine("Fast synced device album ${dbAlbum.name}");
DLog.log("Fast synced device album ${dbAlbum.name}");
return true; return true;
} }

View file

@ -14,7 +14,7 @@ import 'package:logging/logging.dart';
/// writes them to a persistent [ILogRepository], and manages log levels /// writes them to a persistent [ILogRepository], and manages log levels
/// via [IStoreRepository] /// via [IStoreRepository]
class LogService { class LogService {
final IsarLogRepository _logRepository; final LogRepository _logRepository;
final IsarStoreRepository _storeRepository; final IsarStoreRepository _storeRepository;
final List<LogMessage> _msgBuffer = []; final List<LogMessage> _msgBuffer = [];
@ -37,7 +37,7 @@ class LogService {
} }
static Future<LogService> init({ static Future<LogService> init({
required IsarLogRepository logRepository, required LogRepository logRepository,
required IsarStoreRepository storeRepository, required IsarStoreRepository storeRepository,
bool shouldBuffer = true, bool shouldBuffer = true,
}) async { }) async {
@ -50,7 +50,7 @@ class LogService {
} }
static Future<LogService> create({ static Future<LogService> create({
required IsarLogRepository logRepository, required LogRepository logRepository,
required IsarStoreRepository storeRepository, required IsarStoreRepository storeRepository,
bool shouldBuffer = true, bool shouldBuffer = true,
}) async { }) async {
@ -85,7 +85,7 @@ class LogService {
if (_shouldBuffer) { if (_shouldBuffer) {
_msgBuffer.add(record); _msgBuffer.add(record);
_flushTimer ??= Timer(const Duration(seconds: 5), () => unawaited(flushBuffer())); _flushTimer ??= Timer(const Duration(seconds: 5), () => unawaited(_flushBuffer()));
} else { } else {
unawaited(_logRepository.insert(record)); unawaited(_logRepository.insert(record));
} }
@ -108,20 +108,18 @@ class LogService {
await _logRepository.deleteAll(); await _logRepository.deleteAll();
} }
void flush() { Future<void> flush() {
_flushTimer?.cancel(); _flushTimer?.cancel();
// TODO: Rename enable this after moving to sqlite - #16504 return _flushBuffer();
// await _flushBufferToDatabase();
} }
Future<void> dispose() { Future<void> dispose() {
_flushTimer?.cancel(); _flushTimer?.cancel();
_logSubscription.cancel(); _logSubscription.cancel();
return flushBuffer(); return _flushBuffer();
} }
// TOOD: Move this to private once Isar is removed Future<void> _flushBuffer() async {
Future<void> flushBuffer() async {
_flushTimer = null; _flushTimer = null;
final buffer = [..._msgBuffer]; final buffer = [..._msgBuffer];
_msgBuffer.clear(); _msgBuffer.clear();

View file

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -26,7 +25,6 @@ class SyncStreamService {
Future<void> sync() { Future<void> sync() {
_logger.info("Remote sync request for user"); _logger.info("Remote sync request for user");
DLog.log("Remote sync request for user");
// Start the sync stream and handle events // Start the sync stream and handle events
return _syncApiRepository.streamChanges(_handleEvents); return _syncApiRepository.streamChanges(_handleEvents);
} }

View file

@ -1,47 +1,29 @@
import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:drift/drift.dart';
import 'package:isar/isar.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart';
import 'package:immich_mobile/domain/models/log.model.dart' as domain;
part 'log.entity.g.dart'; class LogMessageEntity extends Table {
const LogMessageEntity();
@Collection(inheritance: false) @override
class LoggerMessage { String get tableName => 'logger_messages';
final Id id = Isar.autoIncrement;
final String message;
final String? details;
@Enumerated(EnumType.ordinal)
final LogLevel level;
final DateTime createdAt;
final String? context1;
final String? context2;
const LoggerMessage({ IntColumn get id => integer().autoIncrement()();
required this.message, TextColumn get message => text()();
required this.details, TextColumn get details => text().nullable()();
this.level = LogLevel.info, IntColumn get level => intEnum<domain.LogLevel>()();
required this.createdAt, DateTimeColumn get createdAt => dateTime()();
required this.context1, TextColumn get logger => text().nullable()();
required this.context2, TextColumn get stack => text().nullable()();
}); }
LogMessage toDto() { extension LogMessageEntityDataDomainEx on LogMessageEntityData {
return LogMessage( domain.LogMessage toDto() => domain.LogMessage(
message: message, message: message,
level: level, level: level,
createdAt: createdAt, createdAt: createdAt,
logger: context1, logger: logger,
error: details, error: details,
stack: context2, stack: stack,
); );
} }
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,
);
}
}

Binary file not shown.

View file

@ -1,27 +1,43 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart';
import 'package:isar/isar.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
class IsarLogRepository extends IsarDatabaseRepository { class LogRepository {
final Isar _db; final DriftLogger _db;
const IsarLogRepository(super.db) : _db = db; const LogRepository(this._db);
Future<bool> deleteAll() async { Future<bool> deleteAll() async {
await transaction(() async => await _db.loggerMessages.clear()); await _db.logMessageEntity.deleteAll();
return true; return true;
} }
Future<List<LogMessage>> getAll() async { Future<List<LogMessage>> getAll({int limit = 250}) async {
final logs = await _db.loggerMessages.where().sortByCreatedAtDesc().findAll(); final query = _db.logMessageEntity.select()
return logs.map((l) => l.toDto()).toList(); ..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(limit);
return query.map((log) => log.toDto()).get();
}
LogMessageEntityCompanion _toEntityCompanion(LogMessage log) {
return LogMessageEntityCompanion.insert(
message: log.message,
level: log.level,
createdAt: log.createdAt,
logger: Value(log.logger),
details: Value(log.error),
stack: Value(log.stack),
);
} }
Future<bool> insert(LogMessage log) async { Future<bool> insert(LogMessage log) async {
final logEntity = LoggerMessage.fromDto(log); final logEntity = _toEntityCompanion(log);
try { try {
await transaction(() => _db.loggerMessages.put(logEntity)); await _db.logMessageEntity.insertOne(logEntity);
} catch (e) { } catch (e) {
return false; return false;
} }
@ -30,19 +46,30 @@ class IsarLogRepository extends IsarDatabaseRepository {
} }
Future<bool> insertAll(Iterable<LogMessage> logs) async { Future<bool> insertAll(Iterable<LogMessage> logs) async {
await transaction(() async { final logEntities = logs.map(_toEntityCompanion).toList();
final logEntities = logs.map((log) => LoggerMessage.fromDto(log)).toList(); await _db.logMessageEntity.insertAll(logEntities);
await _db.loggerMessages.putAll(logEntities);
});
return true; return true;
} }
Future<void> truncate({int limit = 250}) async { Future<void> deleteByLogger(String logger) async {
await transaction(() async { await _db.logMessageEntity.deleteWhere((row) => row.logger.equals(logger));
final count = await _db.loggerMessages.count(); }
if (count <= limit) return;
final toRemove = count - limit; Stream<List<LogMessage>> watchMessages(String logger) {
await _db.loggerMessages.where().limit(toRemove).deleteAll(); final query = _db.logMessageEntity.select()
}); ..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..where((row) => row.logger.equals(logger));
return query.watch().map((rows) => rows.map((row) => row.toDto()).toList());
}
Future<void> truncate({int limit = kLogTruncateLimit}) async {
final totalCount = await _db.managers.logMessageEntity.count();
if (totalCount > limit) {
final rowsToDelete = totalCount - limit;
await _db.managers.logMessageEntity.orderBy((o) => o.createdAt.asc()).limit(rowsToDelete).delete();
}
} }
} }

View file

@ -0,0 +1,27 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'logger_db.repository.drift.dart';
@DriftDatabase(tables: [LogMessageEntity])
class DriftLogger extends $DriftLogger implements IDatabaseRepository {
DriftLogger([QueryExecutor? executor])
: super(
executor ?? driftDatabase(name: 'immich_logs', native: const DriftNativeOptions(shareAcrossIsolates: true)),
);
@override
int get schemaVersion => 1;
@override
MigrationStrategy get migration => MigrationStrategy(
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
await customStatement('PRAGMA synchronous = NORMAL');
await customStatement('PRAGMA journal_mode = WAL');
await customStatement('PRAGMA busy_timeout = 500');
},
);
}

View file

@ -4,7 +4,6 @@ import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -107,7 +106,6 @@ class SyncApiRepository {
} }
stopwatch.stop(); stopwatch.stop();
_logger.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); _logger.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
DLog.log("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
} }
List<SyncEvent> _parseLines(List<String> lines) { List<SyncEvent> _parseLines(List<String> lines) {

View file

@ -65,7 +65,7 @@ class AppLogDetailPage extends HookConsumerWidget {
); );
} }
buildLogContext1(String context1) { buildLogContext(String logger) {
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
@ -86,7 +86,7 @@ class AppLogDetailPage extends HookConsumerWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: SelectableText( child: SelectableText(
context1.toString(), logger.toString(),
style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "Inconsolata"), style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "Inconsolata"),
), ),
), ),
@ -103,7 +103,7 @@ class AppLogDetailPage extends HookConsumerWidget {
children: [ children: [
buildTextWithCopyButton("MESSAGE", logMessage.message), buildTextWithCopyButton("MESSAGE", logMessage.message),
if (logMessage.error != null) buildTextWithCopyButton("DETAILS", logMessage.error.toString()), if (logMessage.error != null) buildTextWithCopyButton("DETAILS", logMessage.error.toString()),
if (logMessage.logger != null) buildLogContext1(logMessage.logger.toString()), if (logMessage.logger != null) buildLogContext(logMessage.logger.toString()),
if (logMessage.stack != null) buildTextWithCopyButton("STACK TRACE", logMessage.stack.toString()), if (logMessage.stack != null) buildTextWithCopyButton("STACK TRACE", logMessage.stack.toString()),
], ],
), ),

View file

@ -49,7 +49,6 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
final wsProvider = ref.read(websocketProvider.notifier); final wsProvider = ref.read(websocketProvider.notifier);
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then( ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
(a) { (a) {
log.info('Successfully updated auth info with access token: $accessToken');
try { try {
wsProvider.connect(); wsProvider.connect();
infoProvider.getServerInfo(); infoProvider.getServerInfo();

View file

@ -1,68 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.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/log.repository.dart';
// ignore: import_rule_isar
import 'package:isar/isar.dart';
const kDevLoggerTag = 'DEV';
abstract final class DLog {
const DLog();
static Stream<List<LogMessage>> watchLog() {
final db = Isar.getInstance();
if (db == null) {
return const Stream.empty();
}
return db.loggerMessages
.filter()
.context1EqualTo(kDevLoggerTag)
.sortByCreatedAtDesc()
.watch(fireImmediately: true)
.map((logs) => logs.map((log) => log.toDto()).toList());
}
static void clearLog() {
final db = Isar.getInstance();
if (db == null) {
return;
}
db.writeTxnSync(() {
db.loggerMessages.filter().context1EqualTo(kDevLoggerTag).deleteAllSync();
});
}
static void log(String message, [Object? error, StackTrace? stackTrace]) {
if (!Platform.environment.containsKey('FLUTTER_TEST')) {
debugPrint('[$kDevLoggerTag] [${DateTime.now()}] $message');
}
if (error != null) {
debugPrint('Error: $error');
}
if (stackTrace != null) {
debugPrint('StackTrace: $stackTrace');
}
final isar = Isar.getInstance();
if (isar == null) {
return;
}
final record = LogMessage(
message: message,
level: LogLevel.info,
createdAt: DateTime.now(),
logger: kDevLoggerTag,
error: error?.toString(),
stack: stackTrace?.toString(),
);
unawaited(IsarLogRepository(isar).insert(record));
}
}

View file

@ -2,19 +2,16 @@ import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:logging/logging.dart';
final _features = [ final _features = [
_Feature( _Feature(
@ -37,7 +34,7 @@ final _features = [
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: assets.toSet()), DriftAssetSelectionTimelineRoute(lockedSelectionAssets: assets.toSet()),
); );
DLog.log("Selected ${selectedAssets?.length ?? 0} assets"); Logger("FeaturesInDevelopment").fine("Selected ${selectedAssets?.length ?? 0} assets");
return Future.value(); return Future.value();
}, },
@ -159,7 +156,6 @@ class FeatInDevPage extends StatelessWidget {
), ),
), ),
const Divider(height: 0), const Divider(height: 0),
const Flexible(child: _DevLogs()),
], ],
), ),
); );
@ -174,57 +170,3 @@ class _Feature {
final TextStyle? style; final TextStyle? style;
final Future<void> Function(BuildContext, WidgetRef _) onTap; final Future<void> Function(BuildContext, WidgetRef _) onTap;
} }
class _DevLogs extends StatelessWidget {
const _DevLogs();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
actions: [
IconButton(
onPressed: DLog.clearLog,
icon: Icon(
Icons.delete_outline_rounded,
size: 20.0,
color: context.primaryColor,
semanticLabel: "Clear logs",
),
),
],
centerTitle: true,
),
body: StreamBuilder(
initialData: [],
stream: DLog.watchLog(),
builder: (_, logMessages) {
return ListView.separated(
itemBuilder: (ctx, index) {
final logMessage = logMessages.data![index];
return ListTile(
title: Text(
logMessage.message,
style: TextStyle(color: ctx.colorScheme.onSurface, fontSize: 14.0, overflow: TextOverflow.ellipsis),
),
subtitle: Text(
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}",
style: TextStyle(color: ctx.colorScheme.onSurfaceSecondary, fontSize: 12.0),
),
dense: true,
visualDensity: VisualDensity.compact,
tileColor: Colors.transparent,
minLeadingWidth: 10,
);
},
separatorBuilder: (_, index) {
return const Divider(height: 0);
},
itemCount: logMessages.data?.length ?? 0,
);
},
),
);
}
}

View file

@ -12,10 +12,10 @@ import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.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/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -36,7 +36,6 @@ abstract final class Bootstrap {
UserSchema, UserSchema,
BackupAlbumSchema, BackupAlbumSchema,
DuplicatedAssetSchema, DuplicatedAssetSchema,
LoggerMessageSchema,
ETagSchema, ETagSchema,
if (Platform.isAndroid) AndroidDeviceAssetSchema, if (Platform.isAndroid) AndroidDeviceAssetSchema,
if (Platform.isIOS) IOSDeviceAssetSchema, if (Platform.isIOS) IOSDeviceAssetSchema,
@ -49,9 +48,13 @@ abstract final class Bootstrap {
} }
static Future<void> initDomain(Isar db, {bool shouldBufferLogs = true}) async { static Future<void> initDomain(Isar db, {bool shouldBufferLogs = true}) async {
// load drift dbs
final loggerDb = DriftLogger();
await StoreService.init(storeRepository: IsarStoreRepository(db)); await StoreService.init(storeRepository: IsarStoreRepository(db));
await LogService.init( await LogService.init(
logRepository: IsarLogRepository(db), logRepository: LogRepository(loggerDb),
storeRepository: IsarStoreRepository(db), storeRepository: IsarStoreRepository(db),
shouldBuffer: shouldBufferLogs, shouldBuffer: shouldBufferLogs,
); );

View file

@ -56,7 +56,7 @@ Cancelable<T?> runInIsolateGentle<T>({
log.severe("Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", error, stack); log.severe("Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", error, stack);
} finally { } finally {
try { try {
await LogService.I.flushBuffer(); await LogService.I.flush();
await ref.read(driftProvider).close(); await ref.read(driftProvider).close();
// Close Isar safely // Close Isar safely

View file

@ -28,7 +28,7 @@ final _kWarnLog = LogMessage(
void main() { void main() {
late LogService sut; late LogService sut;
late IsarLogRepository mockLogRepo; late LogRepository mockLogRepo;
late IsarStoreRepository mockStoreRepo; late IsarStoreRepository mockStoreRepo;
setUp(() async { setUp(() async {

View file

@ -12,7 +12,7 @@ import 'package:mocktail/mocktail.dart';
class MockStoreRepository extends Mock implements IsarStoreRepository {} class MockStoreRepository extends Mock implements IsarStoreRepository {}
class MockLogRepository extends Mock implements IsarLogRepository {} class MockLogRepository extends Mock implements LogRepository {}
class MockIsarUserRepository extends Mock implements IsarUserRepository {} class MockIsarUserRepository extends Mock implements IsarUserRepository {}

View file

@ -1,3 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
@ -9,9 +11,10 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/sync.service.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -49,6 +52,28 @@ void main() {
); );
} }
final owner = UserDto(
id: "1",
updatedAt: DateTime.now(),
email: "a@b.c",
name: "first last",
isAdmin: false,
profileChangedAt: DateTime.now(),
);
setUpAll(() async {
final loggerDb = DriftLogger(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
final LogRepository logRepository = LogRepository(loggerDb);
WidgetsFlutterBinding.ensureInitialized();
final db = await TestUtils.initIsar();
db.writeTxnSync(() => db.clearSync());
await StoreService.init(storeRepository: IsarStoreRepository(db));
await Store.put(StoreKey.currentUser, owner);
await LogService.init(logRepository: logRepository, storeRepository: IsarStoreRepository(db));
});
group('Test SyncService grouped', () { group('Test SyncService grouped', () {
final MockHashService hs = MockHashService(); final MockHashService hs = MockHashService();
final MockEntityService entityService = MockEntityService(); final MockEntityService entityService = MockEntityService();
@ -74,16 +99,9 @@ void main() {
isAdmin: false, isAdmin: false,
profileChangedAt: DateTime(2021), profileChangedAt: DateTime(2021),
); );
late SyncService s;
setUpAll(() async {
WidgetsFlutterBinding.ensureInitialized();
final db = await TestUtils.initIsar();
db.writeTxnSync(() => db.clearSync()); late SyncService s;
await StoreService.init(storeRepository: IsarStoreRepository(db));
await Store.put(StoreKey.currentUser, owner);
await LogService.init(logRepository: IsarLogRepository(db), storeRepository: IsarStoreRepository(db));
});
final List<Asset> initialAssets = [ final List<Asset> initialAssets = [
makeAsset(checksum: "a", remoteId: "0-1"), makeAsset(checksum: "a", remoteId: "0-1"),
makeAsset(checksum: "b", remoteId: "2-1"), makeAsset(checksum: "b", remoteId: "2-1"),

View file

@ -14,7 +14,6 @@ import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.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/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
@ -48,7 +47,6 @@ abstract final class TestUtils {
UserSchema, UserSchema,
BackupAlbumSchema, BackupAlbumSchema,
DuplicatedAssetSchema, DuplicatedAssetSchema,
LoggerMessageSchema,
ETagSchema, ETagSchema,
AndroidDeviceAssetSchema, AndroidDeviceAssetSchema,
IOSDeviceAssetSchema, IOSDeviceAssetSchema,