diff --git a/example/main.dart b/example/main.dart deleted file mode 100644 index 36cf80e..0000000 --- a/example/main.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter_dev_utils/flutter_dev_utils.dart'; -import 'package:logger/logger.dart'; - -void main() { - print( - 'Run with either `dart example/main.dart` or `dart --enable-asserts example/main.dart`.'); - Demo.run(); -} - -class Demo { - static void run() { - syncTryCatchHandler(tryFunction: () { - CallerLogger().i('It works!'); - return true; - }); - asyncTryCatchHandler(tryFunction: () async { - CallerLogger().i('It works!'); - return true; - }); - syncTryCatchHandler( - tryFunction: () => jsonDecode('notJson'), - ); - asyncTryCatchHandler( - tryFunction: () => jsonDecode('notJson'), - ); - } -} diff --git a/example/main_caller_logger.dart b/example/main_caller_logger.dart new file mode 100644 index 0000000..0308171 --- /dev/null +++ b/example/main_caller_logger.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; + +import 'package:flutter_dev_utils/src/caller_logger/caller_logger.dart'; +import 'package:flutter_dev_utils/src/caller_logger/type_filter.dart'; +import 'package:logger/logger.dart'; + +var logger = CallerLogger( + ignoreCallers: { + 'syncTryCatchHandler', + }, + filter: TypeFilter( + ignoreTypes: { + IgnoredClass, + }, + ignoreLevel: Level.warning, + ), + level: Level.verbose, +); + +void main() { + print( + 'Run with either `dart example/main_caller_logger.dart` or `dart --enable-asserts example/main_caller_logger.dart`.'); + demo(); +} + +void demo() { + /// Settings are such that: + /// + /// 1. All logs in ExampleClass are printed (because level: Level.verbose) + /// 2. Logs in IgnoredClass are not printed (because ignoreTypes: IgnoredClass), + /// except for logs at level of warning and above (because ignoreLevel: Level.warning) + + ExampleClass.run(); + print('========================================'); + IgnoredClass.run(); + print('========================================'); + + /// 3. Printed log for this will show ExampleClass.nestedRun() instead of + /// syncTryCatchHandler (because ignoreCallers: {'syncTryCatchHandler'}) + ExampleClass.nestedRun(); +} + +class ExampleClass { + static void run() { + logger.log(Level.verbose, 'log: verbose'); + + logger.v('verbose'); + logger.d('debug'); + logger.i('info'); + logger.w('warning'); + logger.e('error'); + logger.wtf('wtf'); + } + + static void nestedRun() { + // nested functions will print + syncTryCatchHandler( + tryFunction: () => jsonDecode('thisIsNotJson'), + ); + } +} + +class IgnoredClass { + static void run() { + logger.v('verbose'); + logger.d('debug'); + logger.i('info'); + logger.w('warning'); + logger.e('error'); + logger.wtf('wtf'); + } +} + +/// Synchronous try and catch handler to reduce boilerplate +/// +/// Used as an example to show how nested functions can be ignored in [ignoreCallers] +dynamic syncTryCatchHandler( + {dynamic Function()? tryFunction, + Map? catchKnownExceptions, + dynamic Function()? catchUnknownExceptions}) { + try { + try { + return tryFunction?.call(); + } on Exception catch (e, s) { + if (catchKnownExceptions == null) { + rethrow; + } else if (catchKnownExceptions.containsKey(e)) { + logger.w('handling known exception', e, s); + catchKnownExceptions[e]?.call(); + } else { + rethrow; + } + } + } catch (e, s) { + logger.e('catchUnknown:', e, s); + catchUnknownExceptions?.call(); + } +} diff --git a/example/main_try_catch_handler.dart b/example/main_try_catch_handler.dart new file mode 100644 index 0000000..f8054b9 --- /dev/null +++ b/example/main_try_catch_handler.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +import 'package:flutter_dev_utils/flutter_dev_utils.dart'; +import 'package:flutter_dev_utils/src/caller_logger/caller_logger.dart'; +import 'package:logger/logger.dart'; + +void main() { + print( + 'Run with either `dart example/main_try_catch_handler.dart` or `dart --enable-asserts example/main_try_catch_handler.dart`.'); + Demo.run(); +} + +class Demo { + static void run() { + syncTryCatchHandler(tryFunction: () { + CallerLogger().i('It works!'); // this should print + return true; + }); + + asyncTryCatchHandler(tryFunction: () async { + CallerLogger().i('It works!'); // this should print + return true; + }); + syncTryCatchHandler( + tryFunction: () => + jsonDecode('notJson'), // this should throw an exception + ); + asyncTryCatchHandler( + tryFunction: () => + jsonDecode('notJson'), // this should throw an exception + ); + } +} diff --git a/lib/flutter_dev_utils.dart b/lib/flutter_dev_utils.dart index 49fa675..eb663d7 100644 --- a/lib/flutter_dev_utils.dart +++ b/lib/flutter_dev_utils.dart @@ -1,4 +1,6 @@ library flutter_dev_utils; -export 'src/try_catch_handler/sync_try_catch_handler.dart'; -export 'src/try_catch_handler/async_try_catch_handler.dart'; +export 'package:flutter_dev_utils/src/try_catch_handler/sync_try_catch_handler.dart'; +export 'package:flutter_dev_utils/src/try_catch_handler/async_try_catch_handler.dart'; +export 'package:flutter_dev_utils/src/caller_logger/caller_logger.dart'; +export 'package:flutter_dev_utils/src/caller_logger/type_filter.dart'; diff --git a/lib/src/caller_logger/caller_logger.dart b/lib/src/caller_logger/caller_logger.dart new file mode 100644 index 0000000..8e47cf6 --- /dev/null +++ b/lib/src/caller_logger/caller_logger.dart @@ -0,0 +1,66 @@ +import 'package:flutter_dev_utils/src/caller_logger/caller_printer.dart'; +import 'package:logger/logger.dart'; + +/// Custom Logger which prints the caller of the Logger.log() method. +/// +/// E.g., if ExampleClass1.method() calls logger.d('some debug message'), +/// then the console will show: ExampleClass1.method: some debug message +/// +/// [ignoreCallers] can be specified to show the most relevant caller +class CallerLogger extends Logger { + CallerLogger._({ + LogFilter? filter, + LogPrinter? printer, + LogOutput? output, + Level? level, + required Set ignoreCallers, + }) : _ignoreCallers = ignoreCallers, + super(filter: filter, printer: printer, output: output, level: level); + + factory CallerLogger({ + LogFilter? filter, + LogPrinter? printer, + LogOutput? output, + Level? level, + Set? ignoreCallers, + }) { + /// Skip callers in stack trace so that the more relevant caller is printed, + /// e.g., methods from the loggers or utility functions + final _defaultIgnoreCallers = { + 'CallerLogger.log', + 'Logger.', + }; + if (ignoreCallers == null) { + ignoreCallers = _defaultIgnoreCallers; + } else { + ignoreCallers.addAll(_defaultIgnoreCallers); + } + return CallerLogger._( + filter: filter, + printer: printer ?? CallerPrinter(), + output: output, + level: level, + ignoreCallers: ignoreCallers, + ); + } + + Set _ignoreCallers; + + @override + void log(Level level, message, [error, StackTrace? stackTrace]) { + // get caller + final lines = StackTrace.current.toString().split('\n'); + var caller = 'CallerNotFound'; + for (var line in lines) { + if (_ignoreCallers.any((element) => line.contains(element))) { + continue; + } else { + // ! caller in StackTrace #x seems to be i=6 when split by spaces + // ! this may bug out if formatting of StackTrace changes + caller = line.split(' ')[6].trim(); + break; // exit loop to save first caller which is not from the logger + } + } + super.log(level, caller + ': ' + message, error, stackTrace); + } +} diff --git a/lib/src/caller_logger/caller_printer.dart b/lib/src/caller_logger/caller_printer.dart new file mode 100644 index 0000000..da2a640 --- /dev/null +++ b/lib/src/caller_logger/caller_printer.dart @@ -0,0 +1,159 @@ +import 'dart:convert'; + +import 'package:logger/logger.dart'; + +class CallerPrinter extends LogPrinter { + static final levelPrefixes = { + Level.verbose: '[V]', + Level.debug: '[D]', + Level.info: '[I]', + Level.warning: '[W]', + Level.error: '[E]', + Level.wtf: '[WTF]', + }; + + static final levelColors = { + Level.verbose: AnsiColor.fg(AnsiColor.grey(0.5)), + Level.debug: AnsiColor.none(), + Level.info: AnsiColor.fg(12), + Level.warning: AnsiColor.fg(208), + Level.error: AnsiColor.fg(196), + Level.wtf: AnsiColor.fg(199), + }; + + final bool printTime; + final bool colors; + + /// The index which to begin the stack trace at + /// + /// This can be useful if, for instance, Logger is wrapped in another class and + /// you wish to remove these wrapped calls from stack trace + final int stackTraceBeginIndex; + + // No of methods to show in StackTrace when no error or exception is thrown + final int methodCount; + + /// No of methods to show in StackTrace when error or exception is thrown + final int errorMethodCount; + + CallerPrinter({ + this.printTime = false, + this.colors = false, + // StackTrace args + this.stackTraceBeginIndex = 0, + this.methodCount = 0, + this.errorMethodCount = 10, + }); + @override + List log(LogEvent event) { + var messageStr = _stringifyMessage(event.message); + var errorStr = event.error != null ? '\nERROR: ${event.error}' : ''; + var timeStr = printTime ? 'TIME: ${DateTime.now().toIso8601String()}' : ''; + + var stackTraceStr; + if (event.stackTrace == null) { + if (methodCount > 0) { + stackTraceStr = _formatStackTrace(StackTrace.current, methodCount); + } + } else if (errorMethodCount > 0) { + stackTraceStr = _formatStackTrace(event.stackTrace, errorMethodCount); + } + stackTraceStr = stackTraceStr == null + ? '' + : '\n' + stackTraceStr + '\n========================================'; + + return [ + '${_labelFor(event.level)} $timeStr$messageStr$errorStr$stackTraceStr' + ]; + } + + String _labelFor(Level level) { + var prefix = levelPrefixes[level]!; + var color = levelColors[level]!; + + return colors ? color(prefix) : prefix; + } + + String _stringifyMessage(dynamic message) { + final finalMessage = message is Function ? message() : message; + if (finalMessage is Map || finalMessage is Iterable) { + var encoder = JsonEncoder.withIndent(null); + return encoder.convert(finalMessage); + } else { + return finalMessage.toString(); + } + } + + //! Stack Trace formatting + /// Matches a stacktrace line as generated on Android/iOS devices. + /// For example: + /// #1 Logger.log (package:logger/src/logger.dart:115:29) + static final _deviceStackTraceRegex = + RegExp(r'#[0-9]+[\s]+(.+) \(([^\s]+)\)'); + + /// Matches a stacktrace line as generated by Flutter web. + /// For example: + /// packages/logger/src/printers/pretty_printer.dart 91:37 + static final _webStackTraceRegex = + RegExp(r'^((packages|dart-sdk)\/[^\s]+\/)'); + + /// Matches a stacktrace line as generated by browser Dart. + /// For example: + /// dart:sdk_internal + /// package:logger/src/logger.dart + static final _browserStackTraceRegex = + RegExp(r'^(?:package:)?(dart:[^\s]+|[^\s]+)'); + + String? _formatStackTrace(StackTrace? stackTrace, int methodCount) { + var lines = stackTrace.toString().split('\n'); + if (stackTraceBeginIndex > 0 && stackTraceBeginIndex < lines.length - 1) { + lines = lines.sublist(stackTraceBeginIndex); + } + var formatted = []; + var count = 0; + for (var line in lines) { + if (_discardDeviceStacktraceLine(line) || + _discardWebStacktraceLine(line) || + _discardBrowserStacktraceLine(line) || + line.isEmpty) { + continue; + } + formatted.add('#$count ${line.replaceFirst(RegExp(r'#\d+\s+'), '')}'); + if (++count == methodCount) { + break; + } + } + + if (formatted.isEmpty) { + return null; + } else { + return formatted.join('\n'); + } + } + + bool _discardDeviceStacktraceLine(String line) { + var match = _deviceStackTraceRegex.matchAsPrefix(line); + if (match == null) { + return false; + } + return match.group(2)!.startsWith('package:logger'); + } + + bool _discardWebStacktraceLine(String line) { + var match = _webStackTraceRegex.matchAsPrefix(line); + if (match == null) { + return false; + } + return match.group(1)!.startsWith('packages/logger') || + match.group(1)!.startsWith('dart-sdk/lib'); + } + + bool _discardBrowserStacktraceLine(String line) { + var match = _browserStackTraceRegex.matchAsPrefix(line); + if (match == null) { + return false; + } + return match.group(1)!.startsWith('package:logger') || + match.group(1)!.startsWith('dart:'); + } +} diff --git a/lib/src/caller_logger/type_filter.dart b/lib/src/caller_logger/type_filter.dart new file mode 100644 index 0000000..0d00886 --- /dev/null +++ b/lib/src/caller_logger/type_filter.dart @@ -0,0 +1,40 @@ +import 'package:logger/logger.dart'; + +/// Creates a filter which allows logs to be selectively ignored based on the +/// caller Type so that you can compartmentalise printed logs. To be used in +/// conjunction with a Logger. +/// +/// E.g., if ExampleClass1.method() and ExampleClass2.method() calls +/// logger.d('some debug message'); +/// and you are working in Level.debug, but only need the logs for ExampleClass1, +/// then you can initialise [ignoreTypes] with {ExampleClass2}, and logs from +/// ExampleClass 2 will not be shown. +class TypeFilter extends DevelopmentFilter { + TypeFilter({required this.ignoreTypes, required this.ignoreLevel}); + + /// Sets the caller types where logs should not be printed below a certain ignoreLevel + final Set ignoreTypes; + + /// Sets the level above which logs for callers in ignoreTypes are not printed. + /// + /// This allows you to always print higher level logs, e.g., warnings and above, even + /// if it is in ignoreTypes + final Level ignoreLevel; + + @override + bool shouldLog(LogEvent event) { + // Ignore printing of callers of types included in ignoreClass so that you + // + if (ignoreTypes.any( + (element) => event.message.toString().contains(element.toString()))) { + // Always print if event level is above ignoreLevel + if (event.level.index >= ignoreLevel.index) { + return true; + } else { + return false; + } + } + // If not in a caller type which should be ignored, use default handling + return super.shouldLog(event); + } +} diff --git a/lib/src/try_catch_handler/async_try_catch_handler.dart b/lib/src/try_catch_handler/async_try_catch_handler.dart index 58eb006..4086592 100644 --- a/lib/src/try_catch_handler/async_try_catch_handler.dart +++ b/lib/src/try_catch_handler/async_try_catch_handler.dart @@ -1,4 +1,4 @@ -import 'package:flutter_dev_utils/src/logger.dart'; +import 'package:flutter_dev_utils/src/try_catch_handler/logger.dart'; import 'package:flutter_dev_utils/src/try_catch_handler/catch_utils.dart'; /// Asynchronous try and catch handler to reduce boilerplate diff --git a/lib/src/logger.dart b/lib/src/try_catch_handler/logger.dart similarity index 64% rename from lib/src/logger.dart rename to lib/src/try_catch_handler/logger.dart index ef6f06c..6e26385 100644 --- a/lib/src/logger.dart +++ b/lib/src/try_catch_handler/logger.dart @@ -1,3 +1,5 @@ +import 'package:flutter_dev_utils/src/caller_logger/caller_logger.dart'; +import 'package:flutter_dev_utils/src/caller_logger/type_filter.dart'; import 'package:logger/logger.dart'; var utilsLogger = CallerLogger( diff --git a/lib/src/try_catch_handler/sync_try_catch_handler.dart b/lib/src/try_catch_handler/sync_try_catch_handler.dart index 1df3480..1749e00 100644 --- a/lib/src/try_catch_handler/sync_try_catch_handler.dart +++ b/lib/src/try_catch_handler/sync_try_catch_handler.dart @@ -1,4 +1,4 @@ -import 'package:flutter_dev_utils/src/logger.dart'; +import 'package:flutter_dev_utils/src/try_catch_handler/logger.dart'; import 'package:flutter_dev_utils/src/try_catch_handler/catch_utils.dart'; /// Synchronous try and catch handler to reduce boilerplate diff --git a/pubspec.yaml b/pubspec.yaml index 5271865..5540bce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,62 +1,54 @@ name: flutter_dev_utils -description: A new Flutter package project. -version: 0.0.1 +description: Developer utils to make your life easier. +version: 0.0.0 homepage: https://github.com/Kek-Tech/flutter_dev_utils -publish_to: none +# publish_to: none # Remove this line if you wish to publish to pub.dev environment: - sdk: '>=2.18.0-271.7.beta <3.0.0' + sdk: ">=2.18.0 <3.0.0" flutter: ">=1.17.0" dependencies: flutter: sdk: flutter - logger: - git: - url: https://github.com/Kek-Tech/logger.git - ref: master - - + logger: ^1.1.0 dev_dependencies: + flutter_lints: ^2.0.0 flutter_test: sdk: flutter - flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec - # The following section is specific to Flutter packages. -flutter: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages +flutter: null +# To add assets to your package, add an assets section, like this: +# assets: +# - images/a_dot_burr.jpeg +# - images/a_dot_ham.jpeg +# +# For details regarding assets in packages, see +# https://flutter.dev/assets-and-images/#from-packages +# +# An image asset can refer to one or more resolution-specific "variants", see +# https://flutter.dev/assets-and-images/#resolution-aware +# To add custom fonts to your package, add a fonts section here, +# in this "flutter" section. Each entry in this list should have a +# "family" key with the font family name, and a "fonts" key with a +# list giving the asset and other descriptors for the font. For +# example: +# fonts: +# - family: Schyler +# fonts: +# - asset: fonts/Schyler-Regular.ttf +# - asset: fonts/Schyler-Italic.ttf +# style: italic +# - family: Trajan Pro +# fonts: +# - asset: fonts/TrajanPro.ttf +# - asset: fonts/TrajanPro_Bold.ttf +# weight: 700 +# +# For details regarding fonts in packages, see +# https://flutter.dev/custom-fonts/#from-packages