浏览代码

feature(mobile): configurable log level (#2248)

* feature(mobile): configurable log level

* increase maxLogEntries to 500

---------

Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com>
Fynn Petersen-Frey 2 年之前
父节点
当前提交
d500ef77cf

+ 3 - 6
mobile/lib/modules/settings/services/app_settings.service.dart

@@ -43,17 +43,14 @@ enum AppSettingsEnum<T> {
     "selectedAlbumSortOrder",
     0,
   ),
-  advancedTroubleshooting<bool>(
-    StoreKey.advancedTroubleshooting,
-    "advancedTroubleshooting",
-    false,
-  ),
+  advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
+  logLevel<int>(StoreKey.logLevel, null, 5) // Level.INFO = 5
   ;
 
   const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
 
   final StoreKey<T> storeKey;
-  final String hiveKey;
+  final String? hiveKey;
   final T defaultValue;
 }
 

+ 31 - 0
mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart

@@ -5,6 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
+import 'package:immich_mobile/shared/services/immich_logger.service.dart';
+import 'package:logging/logging.dart';
 
 class AdvancedSettings extends HookConsumerWidget {
   const AdvancedSettings({super.key});
@@ -13,16 +15,21 @@ class AdvancedSettings extends HookConsumerWidget {
     final appSettingService = ref.watch(appSettingsServiceProvider);
     final isEnabled =
         useState(AppSettingsEnum.advancedTroubleshooting.defaultValue);
+    final levelId = useState(AppSettingsEnum.logLevel.defaultValue);
 
     useEffect(
       () {
         isEnabled.value = appSettingService.getSetting<bool>(
           AppSettingsEnum.advancedTroubleshooting,
         );
+        levelId.value = appSettingService.getSetting(AppSettingsEnum.logLevel);
         return null;
       },
       [],
     );
+
+    final logLevel = Level.LEVELS[levelId.value].name;
+
     return ExpansionTile(
       textColor: Theme.of(context).primaryColor,
       title: const Text(
@@ -46,6 +53,30 @@ class AdvancedSettings extends HookConsumerWidget {
           title: "advanced_settings_troubleshooting_title".tr(),
           subtitle: "advanced_settings_troubleshooting_subtitle".tr(),
         ),
+        ListTile(
+          dense: true,
+          title: Text(
+            // Not translated because the levels are only English
+            "Log level: $logLevel",
+            style: const TextStyle(fontWeight: FontWeight.bold),
+          ),
+          subtitle: Slider(
+            value: levelId.value.toDouble(),
+            onChanged: (double v) => levelId.value = v.toInt(),
+            onChangeEnd: (double v) {
+              appSettingService.setSetting(
+                AppSettingsEnum.logLevel,
+                v.toInt(),
+              );
+              ImmichLogger().level = Level.LEVELS[v.toInt()];
+            },
+            max: 8,
+            min: 1.0,
+            divisions: 7,
+            label: logLevel,
+            activeColor: Theme.of(context).primaryColor,
+          ),
+        ),
       ],
     );
   }

+ 1 - 0
mobile/lib/shared/models/store.dart

@@ -168,6 +168,7 @@ enum StoreKey<T> {
   albumThumbnailCacheSize<int>(112, type: int),
   selectedAlbumSortOrder<int>(113, type: int),
   advancedTroubleshooting<bool>(114, type: bool),
+  logLevel<int>(115, type: int),
   ;
 
   const StoreKey(

+ 6 - 2
mobile/lib/shared/services/immich_logger.service.dart

@@ -3,6 +3,7 @@ import 'dart:io';
 
 import 'package:flutter/widgets.dart';
 import 'package:immich_mobile/shared/models/logger_message.model.dart';
+import 'package:immich_mobile/shared/models/store.dart';
 import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:path_provider/path_provider.dart';
@@ -18,7 +19,7 @@ import 'package:share_plus/share_plus.dart';
 /// and generate a csv file.
 class ImmichLogger {
   static final ImmichLogger _instance = ImmichLogger._internal();
-  final maxLogEntries = 200;
+  final maxLogEntries = 500;
   final Isar _db = Isar.getInstance()!;
   List<LoggerMessage> _msgBuffer = [];
   Timer? _timer;
@@ -27,10 +28,13 @@ class ImmichLogger {
 
   ImmichLogger._internal() {
     _removeOverflowMessages();
-    Logger.root.level = Level.INFO;
+    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();

+ 29 - 8
mobile/lib/shared/services/sync.service.dart

@@ -389,7 +389,13 @@ class SyncService {
           _addAlbumFromDevice(ape, existing, excludedAssets),
       onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates),
     );
+    _log.fine(
+      "Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete",
+    );
     final pair = _handleAssetRemoval(deleteCandidates, existing, remote: false);
+    _log.fine(
+      "${pair.first.length} assets to delete, ${pair.second.length} to update",
+    );
     if (pair.first.isNotEmpty || pair.second.isNotEmpty) {
       await _db.writeTxn(() async {
         await _db.assets.deleteAll(pair.first);
@@ -415,6 +421,7 @@ class SyncService {
     bool forceRefresh = false,
   ]) async {
     if (!forceRefresh && !await _hasAssetPathEntityChanged(ape, album)) {
+      _log.fine("Local album ${ape.name} has not changed. Skipping sync.");
       return false;
     }
     if (!forceRefresh &&
@@ -441,9 +448,18 @@ class SyncService {
         album.name == ape.name &&
         album.modifiedAt == ape.lastModified) {
       // changes only affeted excluded albums
+      _log.fine(
+        "Only excluded assets in local album ${ape.name} changed. Stopping sync.",
+      );
       return false;
     }
+    _log.fine(
+      "Syncing local album ${ape.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
+    );
     final result = await _linkWithExistingFromDb(toAdd);
+    _log.fine(
+      "Linking assets to add with existing from db. ${result.first.length} existing, ${result.second.length} to update",
+    );
     deleteCandidates.addAll(toDelete);
     existing.addAll(result.first);
     album.name = ape.name;
@@ -462,9 +478,9 @@ class SyncService {
         album.thumbnail.value ??= await album.assets.filter().findFirst();
         await album.thumbnail.save();
       });
-      _log.info("Synced changes of local album $ape to DB");
+      _log.info("Synced changes of local album ${ape.name} to DB");
     } on IsarError catch (e) {
-      _log.severe("Failed to update synced album $ape in DB: $e");
+      _log.severe("Failed to update synced album ${ape.name} in DB: $e");
     }
 
     return true;
@@ -499,9 +515,9 @@ class SyncService {
         await album.assets.update(link: result.first + result.second);
         await _db.albums.put(album);
       });
-      _log.info("Fast synced local album $ape to DB");
+      _log.info("Fast synced local album ${ape.name} to DB");
     } on IsarError catch (e) {
-      _log.severe("Failed to fast sync local album $ape to DB: $e");
+      _log.severe("Failed to fast sync local album ${ape.name} to DB: $e");
       return false;
     }
 
@@ -515,7 +531,7 @@ class SyncService {
     List<Asset> existing, [
     Set<String>? excludedAssets,
   ]) async {
-    _log.info("Syncing a new local album to DB: $ape");
+    _log.info("Syncing a new local album to DB: ${ape.name}");
     final Album a = Album.local(ape);
     final result = await _linkWithExistingFromDb(
       await ape.getAssets(excludedAssets: excludedAssets),
@@ -531,9 +547,9 @@ class SyncService {
     a.thumbnail.value = thumb;
     try {
       await _db.writeTxn(() => _db.albums.store(a));
-      _log.info("Added a new local album to DB: $ape");
+      _log.info("Added a new local album to DB: ${ape.name}");
     } on IsarError catch (e) {
-      _log.severe("Failed to add new local album $ape to DB: $e");
+      _log.severe("Failed to add new local album ${ape.name} to DB: $e");
     }
   }
 
@@ -574,7 +590,11 @@ class SyncService {
           return true;
         }
       },
-      onlyFirst: (Asset a) => {},
+      onlyFirst: (Asset a) => _log.finer(
+        "_linkWithExistingFromDb encountered asset only in DB: $a",
+        null,
+        StackTrace.current,
+      ),
       onlySecond: (Asset b) => toUpsert.add(b),
     );
     return Pair(existing, toUpsert);
@@ -663,6 +683,7 @@ Pair<List<int>, List<Asset>> _handleAssetRemoval(
     compare: Asset.compareById,
     remote: remote,
   );
+  assert(triple.first.isEmpty, "toAdd should be empty in _handleAssetRemoval");
   return Pair(triple.third.map((e) => e.id).toList(), triple.second);
 }
 

+ 3 - 1
mobile/lib/utils/migration.dart

@@ -112,7 +112,9 @@ FutureOr<void> _migrateDuplicatedAssetsBox(Box<HiveDuplicatedAssets> box) {
 
 Future<void> _migrateAppSettingsBox(Box box) async {
   for (AppSettingsEnum s in AppSettingsEnum.values) {
-    await _migrateKey(box, s.hiveKey, s.storeKey);
+    if (s.hiveKey != null) {
+      await _migrateKey(box, s.hiveKey!, s.storeKey);
+    }
   }
 }