From 0e7f02c4879694e1483838d5c342f4f8d096a677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20Moli=C5=84ski?= <47773413+damian-molinski@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:36:44 +0200 Subject: [PATCH] feat: logging (#787) * feat: LoggingManager implementation * fix: typo * refactor: rename LoggingManager to LoggingService * refactor: make LoggingService global and initialized as first in bootstrap * fix: typo * chore: typos --- .../lib/configs/app_bloc_observer.dart | 11 ++- catalyst_voices/lib/configs/bootstrap.dart | 74 ++++++++++++--- catalyst_voices/lib/routes/app_router.dart | 4 +- .../lib/src/catalyst_voices_shared.dart | 1 + .../lib/src/logging/logging_service.dart | 91 +++++++++++++++++++ .../catalyst_voices_shared/pubspec.yaml | 1 + melos.yaml | 1 + 7 files changed, 163 insertions(+), 20 deletions(-) create mode 100644 catalyst_voices/packages/catalyst_voices_shared/lib/src/logging/logging_service.dart diff --git a/catalyst_voices/lib/configs/app_bloc_observer.dart b/catalyst_voices/lib/configs/app_bloc_observer.dart index 666ae7630d9..4f2d250dbda 100644 --- a/catalyst_voices/lib/configs/app_bloc_observer.dart +++ b/catalyst_voices/lib/configs/app_bloc_observer.dart @@ -1,15 +1,18 @@ -import 'dart:developer'; - +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; final class AppBlocObserver extends BlocObserver { + AppBlocObserver(); + + final _logger = Logger('AppBlocObserver'); + @override void onChange( BlocBase bloc, Change change, ) { super.onChange(bloc, change); - log('onChange(${bloc.runtimeType}, $change)'); + _logger.info('onChange(${bloc.runtimeType}, $change)'); } @override @@ -18,7 +21,7 @@ final class AppBlocObserver extends BlocObserver { Object error, StackTrace stackTrace, ) { - log('onError(${bloc.runtimeType}, $error, $stackTrace)'); + _logger.warning('onError(${bloc.runtimeType})', error, stackTrace); super.onError(bloc, error, stackTrace); } } diff --git a/catalyst_voices/lib/configs/bootstrap.dart b/catalyst_voices/lib/configs/bootstrap.dart index dbf2dbbf1ad..b7bb5350c63 100644 --- a/catalyst_voices/lib/configs/bootstrap.dart +++ b/catalyst_voices/lib/configs/bootstrap.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'package:catalyst_voices/app/app.dart'; import 'package:catalyst_voices/configs/app_bloc_observer.dart'; @@ -7,12 +6,20 @@ import 'package:catalyst_voices/configs/sentry_service.dart'; import 'package:catalyst_voices/dependency/dependencies.dart'; import 'package:catalyst_voices/routes/guards/milestone_guard.dart'; import 'package:catalyst_voices/routes/routes.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:url_strategy/url_strategy.dart'; +final _loggingService = LoggingService(); + +final _bootstrapLogger = Logger('Bootstrap'); +final _flutterLogger = Logger('Flutter'); +final _platformDispatcherLogger = Logger('PlatformDispatcher'); +final _uncaughtZoneLogger = Logger('UncaughtZone'); + typedef BootstrapWidgetBuilder = FutureOr Function(BootstrapArgs args); final class BootstrapArgs { @@ -23,13 +30,25 @@ final class BootstrapArgs { }); } -// TODO(damian-molinski): Add PlatformDispatcher.instance.onError // TODO(damian-molinski): Add Isolate.current.addErrorListener -// TODO(damian-molinski): Add runZonedGuarded -// TODO(damian-molinski): Add Global try-catch Future bootstrap([ - BootstrapWidgetBuilder builder = _appBuilder, + BootstrapWidgetBuilder builder = _defaultBuilder, ]) async { + runZonedGuarded( + () => _safeBootstrap(builder), + _reportUncaughtZoneError, + ); +} + +Future _safeBootstrap(BootstrapWidgetBuilder builder) async { + try { + await _doBootstrap(builder); + } catch (error, stack) { + await _reportBootstrapError(error, stack); + } +} + +Future _doBootstrap(BootstrapWidgetBuilder builder) async { // There's no need to call WidgetsFlutterBinding.ensureInitialized() // since this is already done internally by SentryFlutter.init() // More info here: https://github.com/getsentry/sentry-dart/issues/2063 @@ -37,12 +56,14 @@ Future bootstrap([ WidgetsFlutterBinding.ensureInitialized(); } - FlutterError.onError = (details) { - log( - details.exceptionAsString(), - stackTrace: details.stack, - ); - }; + _loggingService + ..level = kDebugMode ? Level.ALL : Level.OFF + ..printLogs = kDebugMode; + + FlutterError.onError = _reportFlutterError; + PlatformDispatcher.instance.onError = _reportPlatformDispatcherError; + + await Dependencies.instance.init(); GoRouter.optionURLReflectsImperativeAPIs = true; setPathUrlStrategy(); @@ -55,11 +76,10 @@ Future bootstrap([ Bloc.observer = AppBlocObserver(); - await Dependencies.instance.init(); - final args = BootstrapArgs(routerConfig: router); + final app = await builder(args); - await _runApp(await builder(args)); + await _runApp(app); } Future _runApp(Widget app) async { @@ -70,8 +90,32 @@ Future _runApp(Widget app) async { } } -Widget _appBuilder(BootstrapArgs args) { +Widget _defaultBuilder(BootstrapArgs args) { return App( routerConfig: args.routerConfig, ); } + +Future _reportBootstrapError(Object error, StackTrace stack) async { + _bootstrapLogger.severe('Error while bootstrapping', error, stack); +} + +/// Flutter-specific assertion failures and contract violations. +Future _reportFlutterError(FlutterErrorDetails details) async { + _flutterLogger.severe( + details.context?.toStringDeep(), + details.exception, + details.stack, + ); +} + +/// Platform Dispatcher Errors reporting +bool _reportPlatformDispatcherError(Object error, StackTrace stack) { + _platformDispatcherLogger.severe('Platform Error', error, stack); + return true; +} + +/// Uncaught Errors reporting +void _reportUncaughtZoneError(Object error, StackTrace stack) { + _uncaughtZoneLogger.severe('Uncaught Error', error, stack); +} diff --git a/catalyst_voices/lib/routes/app_router.dart b/catalyst_voices/lib/routes/app_router.dart index 9e43ba20b9b..0c38d308adf 100644 --- a/catalyst_voices/lib/routes/app_router.dart +++ b/catalyst_voices/lib/routes/app_router.dart @@ -15,7 +15,6 @@ abstract final class AppRouter { List guards = const [], }) { return GoRouter( - debugLogDiagnostics: true, navigatorKey: _rootNavigatorKey, initialLocation: Routes.initialLocation, redirect: (context, state) => _guard(context, state, guards), @@ -23,6 +22,9 @@ abstract final class AppRouter { SentryNavigatorObserver(), ], routes: Routes.routes, + // always true. We're deciding whether to print + // them or not in LoggingService + debugLogDiagnostics: true, ); } diff --git a/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart b/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart index f80314f6864..b0ad6521971 100644 --- a/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart +++ b/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart @@ -2,6 +2,7 @@ export 'common/build_config.dart'; export 'common/build_environment.dart'; export 'dependency/dependency_provider.dart'; export 'formatter/cryptocurrency_formatter.dart'; +export 'logging/logging_service.dart'; export 'platform/catalyst_platform.dart'; export 'platform_aware_builder/platform_aware_builder.dart'; export 'responsive/responsive_builder.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_shared/lib/src/logging/logging_service.dart b/catalyst_voices/packages/catalyst_voices_shared/lib/src/logging/logging_service.dart new file mode 100644 index 00000000000..f05a9cb974c --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_shared/lib/src/logging/logging_service.dart @@ -0,0 +1,91 @@ +import 'dart:async'; +import 'dart:developer' as developer; + +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; + +export 'package:logging/logging.dart' show Level, Logger; + +abstract interface class LoggingService { + factory LoggingService() { + return _LoggingServiceImpl(root: Logger.root); + } + + set level(Level newValue); + + set printLogs(bool newValue); + + void dispose(); +} + +final class _LoggingServiceImpl implements LoggingService { + final Logger root; + + StreamSubscription? _recordsSub; + + _LoggingServiceImpl({ + required this.root, + }) : _printLogs = false { + hierarchicalLoggingEnabled = true; + + root.level = Level.OFF; + _recordsSub = root.onRecord.listen(_onLogRecord); + } + + @override + set level(Level newValue) { + if (root.level == newValue) { + return; + } + root.level = newValue; + } + + bool get printLogs => _printLogs; + + bool _printLogs; + + @override + set printLogs(bool newValue) { + if (_printLogs == newValue) { + return; + } + _printLogs = newValue; + } + + @override + void dispose() { + _recordsSub?.cancel(); + _recordsSub = null; + } + + void _onLogRecord(LogRecord log) { + if (_printLogs) { + _printLogRecord(log); + } + } + + void _printLogRecord(LogRecord log) { + if (log.level >= Level.SEVERE) { + final error = log.error; + final errorDetails = FlutterErrorDetails( + exception: error is Exception ? error : Exception(error), + stack: log.stackTrace, + library: log.loggerName, + context: ErrorDescription(log.message), + ); + FlutterError.dumpErrorToConsole(errorDetails); + return; + } + + developer.log( + log.message, + time: log.time, + sequenceNumber: log.sequenceNumber, + level: log.level.value, + name: log.loggerName, + zone: log.zone, + error: log.error, + stackTrace: log.stackTrace, + ); + } +} diff --git a/catalyst_voices/packages/catalyst_voices_shared/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_shared/pubspec.yaml index fa0fa9dec3c..452215ebc2c 100644 --- a/catalyst_voices/packages/catalyst_voices_shared/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_shared/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: sdk: flutter get_it: ^7.6.7 intl: ^0.19.0 + logging: ^1.2.0 web: ^0.5.0 dev_dependencies: diff --git a/melos.yaml b/melos.yaml index 8c95322cce0..c0a5ed1d221 100644 --- a/melos.yaml +++ b/melos.yaml @@ -27,6 +27,7 @@ command: flutter_quill: ^10.5.0 flutter_quill_extensions: ^10.5.0 formz: ^0.7.0 + logging: ^1.2.0 meta: ^1.10.0 result_type: ^0.2.0 plugin_platform_interface: ^2.1.7