Skip to content

Commit

Permalink
feat: error reporting with logs (#45)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Dr-Blank authored Oct 3, 2024
1 parent 7b0c2c4 commit 35a2d7c
Show file tree
Hide file tree
Showing 44 changed files with 861 additions and 176 deletions.
Binary file added assets/images/vaani_logo_foreground.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions lib/api/api_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/db/cache_manager.dart';
import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/shared/extensions/obfuscation.dart';

part 'api_provider.g.dart';

Expand Down Expand Up @@ -80,7 +81,7 @@ FutureOr<ServerStatusResponse?> serverStatus(
Uri baseUrl, [
ResponseErrorHandler? responseErrorHandler,
]) async {
_logger.fine('fetching server status: $baseUrl');
_logger.fine('fetching server status: ${baseUrl.obfuscate()}');
final api = ref.watch(audiobookshelfApiProvider(baseUrl));
final res =
await api.server.status(responseErrorHandler: responseErrorHandler);
Expand Down Expand Up @@ -145,7 +146,6 @@ class PersonalizedView extends _$PersonalizedView {
_logger.warning('failed to fetch personalized view');
yield [];
}

}

// method to force refresh the view and ignore the cache
Expand Down
2 changes: 1 addition & 1 deletion lib/api/api_provider.g.dart

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

10 changes: 6 additions & 4 deletions lib/api/authenticated_user_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import 'package:vaani/api/server_provider.dart'
import 'package:vaani/db/storage.dart';
import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/audiobookshelf_server.dart';
import 'package:vaani/settings/models/authenticated_user.dart'
as model;
import 'package:vaani/settings/models/authenticated_user.dart' as model;
import 'package:vaani/shared/extensions/obfuscation.dart';

part 'authenticated_user_provider.g.dart';

Expand Down Expand Up @@ -35,7 +35,9 @@ class AuthenticatedUser extends _$AuthenticatedUser {
Set<model.AuthenticatedUser> readFromBoxOrCreate() {
if (_box.isNotEmpty) {
final foundData = _box.getRange(0, _box.length);
_logger.fine('found users in box: $foundData');
_logger.fine(
'found users in box: ${foundData.obfuscate()}',
);
return foundData.toSet();
} else {
_logger.fine('no settings found in box');
Expand All @@ -49,7 +51,7 @@ class AuthenticatedUser extends _$AuthenticatedUser {
return;
}
_box.addAll(state);
_logger.fine('writing state to box: $state');
_logger.fine('writing state to box: ${state.obfuscate()}');
}

void addUser(model.AuthenticatedUser user, {bool setActive = false}) {
Expand Down
2 changes: 1 addition & 1 deletion lib/api/authenticated_user_provider.g.dart

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

18 changes: 10 additions & 8 deletions lib/api/server_provider.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/api/authenticated_user_provider.dart';
import 'package:vaani/db/storage.dart';
import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/audiobookshelf_server.dart'
as model;
import 'package:vaani/settings/models/audiobookshelf_server.dart' as model;
import 'package:vaani/shared/extensions/obfuscation.dart';

part 'server_provider.g.dart';

final _box = AvailableHiveBoxes.serverBox;

final _logger = Logger('AudiobookShelfServerProvider');

class ServerAlreadyExistsException implements Exception {
final model.AudiobookShelfServer server;

Expand Down Expand Up @@ -47,10 +49,10 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
Set<model.AudiobookShelfServer> readFromBoxOrCreate() {
if (_box.isNotEmpty) {
final foundServers = _box.getRange(0, _box.length);
debugPrint('found servers in box: $foundServers');
_logger.info('found servers in box: ${foundServers.obfuscate()}');
return foundServers.whereNotNull().toSet();
} else {
debugPrint('no settings found in box');
_logger.info('no settings found in box');
return {};
}
}
Expand All @@ -61,7 +63,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
return;
}
_box.addAll(state);
debugPrint('writing state to box: $state');
_logger.info('writing state to box: ${state.obfuscate()}');
}

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

void removeServer(model.AudiobookShelfServer server,
{
void removeServer(
model.AudiobookShelfServer server, {
bool removeUsers = false,
}) {
state = state.where((s) => s != server).toSet();
Expand Down
2 changes: 1 addition & 1 deletion lib/api/server_provider.g.dart

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

12 changes: 6 additions & 6 deletions lib/db/init.dart
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
// does the initial setup of the storage

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:vaani/main.dart';
import 'package:vaani/settings/constants.dart';

import 'register_models.dart';

// does the initial setup of the storage
Future initStorage() async {
final dir = await getApplicationDocumentsDirectory();

// use vaani as the directory for hive
final storageDir = Directory(p.join(
dir.path,
final storageDir = Directory(
p.join(
dir.path,
AppMetadata.appNameLowerCase,
),
);
await storageDir.create(recursive: true);

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

await registerModels();
}
5 changes: 4 additions & 1 deletion lib/features/downloads/core/download_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/extensions/obfuscation.dart';

final _logger = Logger('AudiobookDownloadManager');
final tq = MemoryTaskQueue();
Expand Down Expand Up @@ -35,7 +36,9 @@ class AudiobookDownloadManager {

FileDownloader().addTaskQueue(tq);

_logger.fine('initialized with baseUrl: $baseUrl, token: $token');
_logger.fine(
'initialized with baseUrl: ${Uri.parse(baseUrl).obfuscate()} and token: ${token.obfuscate()}',
);
_logger.fine(
'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause',
);
Expand Down
4 changes: 4 additions & 0 deletions lib/features/downloads/providers/download_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ class SimpleDownloadManager extends _$SimpleDownloadManager {
core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup;

ref.onDispose(() {
_logger.info('disposing download manager');
manager.dispose();
});

_logger.config('initialized download manager');
return manager;
}
}
Expand All @@ -52,12 +54,14 @@ class DownloadManager extends _$DownloadManager {
Future<void> queueAudioBookDownload(
LibraryItemExpanded item,
) async {
_logger.fine('queueing download for ${item.id}');
await state.queueAudioBookDownload(
item,
);
}

Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
_logger.fine('deleting downloaded item ${item.id}');
await state.deleteDownloadedItem(item);
ref.notifyListeners();
}
Expand Down
4 changes: 2 additions & 2 deletions lib/features/downloads/providers/download_manager.g.dart

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

10 changes: 5 additions & 5 deletions lib/features/item_viewer/view/library_item_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ Future<void> libraryItemPlayButtonOnPressed({
required shelfsdk.BookExpanded book,
shelfsdk.MediaProgress? userMediaProgress,
}) async {
debugPrint('Pressed play/resume button');
appLogger.info('Pressed play/resume button');
final player = ref.watch(audiobookPlayerProvider);

final isCurrentBookSetInPlayer = player.book == book;
Expand All @@ -527,8 +527,8 @@ Future<void> libraryItemPlayButtonOnPressed({
Future<void>? setSourceFuture;
// set the book to the player if not already set
if (!isCurrentBookSetInPlayer) {
debugPrint('Setting the book ${book.libraryItemId}');
debugPrint('Initial position: ${userMediaProgress?.currentTime}');
appLogger.info('Setting the book ${book.libraryItemId}');
appLogger.info('Initial position: ${userMediaProgress?.currentTime}');
final downloadManager = ref.watch(simpleDownloadManagerProvider);
final libItem =
await ref.read(libraryItemProvider(book.libraryItemId).future);
Expand All @@ -539,9 +539,9 @@ Future<void> libraryItemPlayButtonOnPressed({
downloadedUris: downloadedUris,
);
} else {
debugPrint('Book was already set');
appLogger.info('Book was already set');
if (isPlayingThisBook) {
debugPrint('Pausing the book');
appLogger.info('Pausing the book');
await player.pause();
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ class _BookCover extends HookConsumerWidget {
: themeData,
);
} catch (e) {
appLogger.shout('Error changing theme: $e');
appLogger.severe('Error changing theme: $e');
}
});
}
Expand Down
36 changes: 36 additions & 0 deletions lib/features/logging/core/logger.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:path_provider/path_provider.dart';
import 'package:vaani/shared/extensions/duration_format.dart';

Future<String> getLoggingFilePath() async {
final Directory directory = await getApplicationDocumentsDirectory();
return '${directory.path}/vaani.log';
}

Future<void> initLogging() async {
final formatter = const DefaultLogRecordFormatter();
if (kReleaseMode) {
Logger.root.level = Level.INFO; // is also the default
// Write to a file
RotatingFileAppender(
baseFilePath: await getLoggingFilePath(),
formatter: formatter,
).attachToLogger(Logger.root);
} else {
Logger.root.level = Level.FINE; // Capture all logs
RotatingFileAppender(
baseFilePath: await getLoggingFilePath(),
formatter: formatter,
).attachToLogger(Logger.root);
Logger.root.onRecord.listen((record) {
// Print log records to the console
debugPrint(
'${record.loggerName}: ${record.level.name}: ${record.time.time}: ${record.message}',
);
});
}
}
76 changes: 76 additions & 0 deletions lib/features/logging/providers/logs_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'dart:io';

import 'package:archive/archive_io.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/logging/core/logger.dart';

part 'logs_provider.g.dart';

@riverpod
class Logs extends _$Logs {
@override
Future<List<LogRecord>> build() async {
final path = await getLoggingFilePath();
final file = File(path);
if (!file.existsSync()) {
return [];
}
final lines = await file.readAsLines();
return lines.map(parseLogLine).toList();
}

Future<void> clear() async {
final path = await getLoggingFilePath();
final file = File(path);
await file.writeAsString('');
state = AsyncData([]);
}

Future<String> getZipFilePath() async {
var encoder = ZipFileEncoder();
encoder.create(await generateZipFilePath());
encoder.addFile(File(await getLoggingFilePath()));
encoder.close();
return encoder.zipPath;
}
}

Future<String> generateZipFilePath() async {
Directory appDocDirectory = await getTemporaryDirectory();
return '${appDocDirectory.path}/${generateZipFileName()}';
}

String generateZipFileName() {
return 'vaani-${DateTime.now().toIso8601String()}.zip';
}

Level parseLevel(String level) {
return Level.LEVELS
.firstWhere((l) => l.name == level, orElse: () => Level.ALL);
}

LogRecord parseLogLine(String line) {
// 2024-10-03 00:48:58.012400 INFO GoRouter - getting location for name: "logs"

final RegExp logLineRegExp = RegExp(
r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) (\w+) (\w+) - (.+)',
);

final match = logLineRegExp.firstMatch(line);
if (match == null) {
// return as is
return LogRecord(Level.ALL, line, 'Unknown');
}

final timeString = match.group(1)!;
final levelString = match.group(2)!;
final loggerName = match.group(3)!;
final message = match.group(4)!;

final time = DateTime.parse(timeString);
final level = parseLevel(levelString);

return LogRecord(level, message, loggerName, time);
}
Loading

0 comments on commit 35a2d7c

Please sign in to comment.