Skip to content

Commit 35a2d7c

Browse files
authored
feat: error reporting with logs (#45)
* feat: add ability to get logs file from ui * test: add unit test for log line parsing in logs_provider * refactor: update all logs to obfuscate sensitive information * feat: generate dynamic zip file name for logs export * feat: enhance logging in audiobook player and provider for better debugging * refactor: extract user display logic into UserBar widget for offline access of settings and logs * feat: add About section with app metadata and source code link in YouPage
1 parent 7b0c2c4 commit 35a2d7c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+861
-176
lines changed
3.78 KB
Loading

lib/api/api_provider.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
88
import 'package:shelfsdk/audiobookshelf_api.dart';
99
import 'package:vaani/db/cache_manager.dart';
1010
import 'package:vaani/settings/api_settings_provider.dart';
11+
import 'package:vaani/shared/extensions/obfuscation.dart';
1112

1213
part 'api_provider.g.dart';
1314

@@ -80,7 +81,7 @@ FutureOr<ServerStatusResponse?> serverStatus(
8081
Uri baseUrl, [
8182
ResponseErrorHandler? responseErrorHandler,
8283
]) async {
83-
_logger.fine('fetching server status: $baseUrl');
84+
_logger.fine('fetching server status: ${baseUrl.obfuscate()}');
8485
final api = ref.watch(audiobookshelfApiProvider(baseUrl));
8586
final res =
8687
await api.server.status(responseErrorHandler: responseErrorHandler);
@@ -145,7 +146,6 @@ class PersonalizedView extends _$PersonalizedView {
145146
_logger.warning('failed to fetch personalized view');
146147
yield [];
147148
}
148-
149149
}
150150

151151
// method to force refresh the view and ignore the cache

lib/api/api_provider.g.dart

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/api/authenticated_user_provider.dart

+6-4
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import 'package:vaani/api/server_provider.dart'
55
import 'package:vaani/db/storage.dart';
66
import 'package:vaani/settings/api_settings_provider.dart';
77
import 'package:vaani/settings/models/audiobookshelf_server.dart';
8-
import 'package:vaani/settings/models/authenticated_user.dart'
9-
as model;
8+
import 'package:vaani/settings/models/authenticated_user.dart' as model;
9+
import 'package:vaani/shared/extensions/obfuscation.dart';
1010

1111
part 'authenticated_user_provider.g.dart';
1212

@@ -35,7 +35,9 @@ class AuthenticatedUser extends _$AuthenticatedUser {
3535
Set<model.AuthenticatedUser> readFromBoxOrCreate() {
3636
if (_box.isNotEmpty) {
3737
final foundData = _box.getRange(0, _box.length);
38-
_logger.fine('found users in box: $foundData');
38+
_logger.fine(
39+
'found users in box: ${foundData.obfuscate()}',
40+
);
3941
return foundData.toSet();
4042
} else {
4143
_logger.fine('no settings found in box');
@@ -49,7 +51,7 @@ class AuthenticatedUser extends _$AuthenticatedUser {
4951
return;
5052
}
5153
_box.addAll(state);
52-
_logger.fine('writing state to box: $state');
54+
_logger.fine('writing state to box: ${state.obfuscate()}');
5355
}
5456

5557
void addUser(model.AuthenticatedUser user, {bool setActive = false}) {

lib/api/authenticated_user_provider.g.dart

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/api/server_provider.dart

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import 'package:collection/collection.dart';
2-
import 'package:flutter/material.dart';
2+
import 'package:logging/logging.dart';
33
import 'package:riverpod_annotation/riverpod_annotation.dart';
44
import 'package:vaani/api/authenticated_user_provider.dart';
55
import 'package:vaani/db/storage.dart';
66
import 'package:vaani/settings/api_settings_provider.dart';
7-
import 'package:vaani/settings/models/audiobookshelf_server.dart'
8-
as model;
7+
import 'package:vaani/settings/models/audiobookshelf_server.dart' as model;
8+
import 'package:vaani/shared/extensions/obfuscation.dart';
99

1010
part 'server_provider.g.dart';
1111

1212
final _box = AvailableHiveBoxes.serverBox;
1313

14+
final _logger = Logger('AudiobookShelfServerProvider');
15+
1416
class ServerAlreadyExistsException implements Exception {
1517
final model.AudiobookShelfServer server;
1618

@@ -47,10 +49,10 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
4749
Set<model.AudiobookShelfServer> readFromBoxOrCreate() {
4850
if (_box.isNotEmpty) {
4951
final foundServers = _box.getRange(0, _box.length);
50-
debugPrint('found servers in box: $foundServers');
52+
_logger.info('found servers in box: ${foundServers.obfuscate()}');
5153
return foundServers.whereNotNull().toSet();
5254
} else {
53-
debugPrint('no settings found in box');
55+
_logger.info('no settings found in box');
5456
return {};
5557
}
5658
}
@@ -61,7 +63,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
6163
return;
6264
}
6365
_box.addAll(state);
64-
debugPrint('writing state to box: $state');
66+
_logger.info('writing state to box: ${state.obfuscate()}');
6567
}
6668

6769
void addServer(model.AudiobookShelfServer server) {
@@ -71,8 +73,8 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
7173
state = {...state, server};
7274
}
7375

74-
void removeServer(model.AudiobookShelfServer server,
75-
{
76+
void removeServer(
77+
model.AudiobookShelfServer server, {
7678
bool removeUsers = false,
7779
}) {
7880
state = state.where((s) => s != server).toSet();

lib/api/server_provider.g.dart

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/db/init.dart

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
// does the initial setup of the storage
2-
31
import 'dart:io';
42

5-
import 'package:flutter/material.dart';
63
import 'package:hive/hive.dart';
74
import 'package:path/path.dart' as p;
85
import 'package:path_provider/path_provider.dart';
6+
import 'package:vaani/main.dart';
97
import 'package:vaani/settings/constants.dart';
108

119
import 'register_models.dart';
1210

11+
// does the initial setup of the storage
1312
Future initStorage() async {
1413
final dir = await getApplicationDocumentsDirectory();
1514

1615
// use vaani as the directory for hive
17-
final storageDir = Directory(p.join(
18-
dir.path,
16+
final storageDir = Directory(
17+
p.join(
18+
dir.path,
1919
AppMetadata.appNameLowerCase,
2020
),
2121
);
2222
await storageDir.create(recursive: true);
2323

2424
Hive.defaultDirectory = storageDir.path;
25-
debugPrint('Hive storage directory init: ${Hive.defaultDirectory}');
25+
appLogger.config('Hive storage directory init: ${Hive.defaultDirectory}');
2626

2727
await registerModels();
2828
}

lib/features/downloads/core/download_manager.dart

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:logging/logging.dart';
88
import 'package:path_provider/path_provider.dart';
99
import 'package:shelfsdk/audiobookshelf_api.dart';
1010
import 'package:vaani/shared/extensions/model_conversions.dart';
11+
import 'package:vaani/shared/extensions/obfuscation.dart';
1112

1213
final _logger = Logger('AudiobookDownloadManager');
1314
final tq = MemoryTaskQueue();
@@ -35,7 +36,9 @@ class AudiobookDownloadManager {
3536

3637
FileDownloader().addTaskQueue(tq);
3738

38-
_logger.fine('initialized with baseUrl: $baseUrl, token: $token');
39+
_logger.fine(
40+
'initialized with baseUrl: ${Uri.parse(baseUrl).obfuscate()} and token: ${token.obfuscate()}',
41+
);
3942
_logger.fine(
4043
'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause',
4144
);

lib/features/downloads/providers/download_manager.dart

+4
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ class SimpleDownloadManager extends _$SimpleDownloadManager {
3131
core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup;
3232

3333
ref.onDispose(() {
34+
_logger.info('disposing download manager');
3435
manager.dispose();
3536
});
3637

38+
_logger.config('initialized download manager');
3739
return manager;
3840
}
3941
}
@@ -52,12 +54,14 @@ class DownloadManager extends _$DownloadManager {
5254
Future<void> queueAudioBookDownload(
5355
LibraryItemExpanded item,
5456
) async {
57+
_logger.fine('queueing download for ${item.id}');
5558
await state.queueAudioBookDownload(
5659
item,
5760
);
5861
}
5962

6063
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
64+
_logger.fine('deleting downloaded item ${item.id}');
6165
await state.deleteDownloadedItem(item);
6266
ref.notifyListeners();
6367
}

lib/features/downloads/providers/download_manager.g.dart

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/features/item_viewer/view/library_item_actions.dart

+5-5
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ Future<void> libraryItemPlayButtonOnPressed({
518518
required shelfsdk.BookExpanded book,
519519
shelfsdk.MediaProgress? userMediaProgress,
520520
}) async {
521-
debugPrint('Pressed play/resume button');
521+
appLogger.info('Pressed play/resume button');
522522
final player = ref.watch(audiobookPlayerProvider);
523523

524524
final isCurrentBookSetInPlayer = player.book == book;
@@ -527,8 +527,8 @@ Future<void> libraryItemPlayButtonOnPressed({
527527
Future<void>? setSourceFuture;
528528
// set the book to the player if not already set
529529
if (!isCurrentBookSetInPlayer) {
530-
debugPrint('Setting the book ${book.libraryItemId}');
531-
debugPrint('Initial position: ${userMediaProgress?.currentTime}');
530+
appLogger.info('Setting the book ${book.libraryItemId}');
531+
appLogger.info('Initial position: ${userMediaProgress?.currentTime}');
532532
final downloadManager = ref.watch(simpleDownloadManagerProvider);
533533
final libItem =
534534
await ref.read(libraryItemProvider(book.libraryItemId).future);
@@ -539,9 +539,9 @@ Future<void> libraryItemPlayButtonOnPressed({
539539
downloadedUris: downloadedUris,
540540
);
541541
} else {
542-
debugPrint('Book was already set');
542+
appLogger.info('Book was already set');
543543
if (isPlayingThisBook) {
544-
debugPrint('Pausing the book');
544+
appLogger.info('Pausing the book');
545545
await player.pause();
546546
return;
547547
}

lib/features/item_viewer/view/library_item_hero_section.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ class _BookCover extends HookConsumerWidget {
383383
: themeData,
384384
);
385385
} catch (e) {
386-
appLogger.shout('Error changing theme: $e');
386+
appLogger.severe('Error changing theme: $e');
387387
}
388388
});
389389
}

lib/features/logging/core/logger.dart

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import 'dart:io';
2+
3+
import 'package:flutter/foundation.dart';
4+
import 'package:logging/logging.dart';
5+
import 'package:logging_appenders/logging_appenders.dart';
6+
import 'package:path_provider/path_provider.dart';
7+
import 'package:vaani/shared/extensions/duration_format.dart';
8+
9+
Future<String> getLoggingFilePath() async {
10+
final Directory directory = await getApplicationDocumentsDirectory();
11+
return '${directory.path}/vaani.log';
12+
}
13+
14+
Future<void> initLogging() async {
15+
final formatter = const DefaultLogRecordFormatter();
16+
if (kReleaseMode) {
17+
Logger.root.level = Level.INFO; // is also the default
18+
// Write to a file
19+
RotatingFileAppender(
20+
baseFilePath: await getLoggingFilePath(),
21+
formatter: formatter,
22+
).attachToLogger(Logger.root);
23+
} else {
24+
Logger.root.level = Level.FINE; // Capture all logs
25+
RotatingFileAppender(
26+
baseFilePath: await getLoggingFilePath(),
27+
formatter: formatter,
28+
).attachToLogger(Logger.root);
29+
Logger.root.onRecord.listen((record) {
30+
// Print log records to the console
31+
debugPrint(
32+
'${record.loggerName}: ${record.level.name}: ${record.time.time}: ${record.message}',
33+
);
34+
});
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import 'dart:io';
2+
3+
import 'package:archive/archive_io.dart';
4+
import 'package:logging/logging.dart';
5+
import 'package:path_provider/path_provider.dart';
6+
import 'package:riverpod_annotation/riverpod_annotation.dart';
7+
import 'package:vaani/features/logging/core/logger.dart';
8+
9+
part 'logs_provider.g.dart';
10+
11+
@riverpod
12+
class Logs extends _$Logs {
13+
@override
14+
Future<List<LogRecord>> build() async {
15+
final path = await getLoggingFilePath();
16+
final file = File(path);
17+
if (!file.existsSync()) {
18+
return [];
19+
}
20+
final lines = await file.readAsLines();
21+
return lines.map(parseLogLine).toList();
22+
}
23+
24+
Future<void> clear() async {
25+
final path = await getLoggingFilePath();
26+
final file = File(path);
27+
await file.writeAsString('');
28+
state = AsyncData([]);
29+
}
30+
31+
Future<String> getZipFilePath() async {
32+
var encoder = ZipFileEncoder();
33+
encoder.create(await generateZipFilePath());
34+
encoder.addFile(File(await getLoggingFilePath()));
35+
encoder.close();
36+
return encoder.zipPath;
37+
}
38+
}
39+
40+
Future<String> generateZipFilePath() async {
41+
Directory appDocDirectory = await getTemporaryDirectory();
42+
return '${appDocDirectory.path}/${generateZipFileName()}';
43+
}
44+
45+
String generateZipFileName() {
46+
return 'vaani-${DateTime.now().toIso8601String()}.zip';
47+
}
48+
49+
Level parseLevel(String level) {
50+
return Level.LEVELS
51+
.firstWhere((l) => l.name == level, orElse: () => Level.ALL);
52+
}
53+
54+
LogRecord parseLogLine(String line) {
55+
// 2024-10-03 00:48:58.012400 INFO GoRouter - getting location for name: "logs"
56+
57+
final RegExp logLineRegExp = RegExp(
58+
r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) (\w+) (\w+) - (.+)',
59+
);
60+
61+
final match = logLineRegExp.firstMatch(line);
62+
if (match == null) {
63+
// return as is
64+
return LogRecord(Level.ALL, line, 'Unknown');
65+
}
66+
67+
final timeString = match.group(1)!;
68+
final levelString = match.group(2)!;
69+
final loggerName = match.group(3)!;
70+
final message = match.group(4)!;
71+
72+
final time = DateTime.parse(timeString);
73+
final level = parseLevel(levelString);
74+
75+
return LogRecord(level, message, loggerName, time);
76+
}

0 commit comments

Comments
 (0)