diff --git a/example/caller_logger/main.dart b/example/caller_logger/main.dart new file mode 100644 index 0000000..53e182b --- /dev/null +++ b/example/caller_logger/main.dart @@ -0,0 +1,99 @@ +import 'dart:convert'; + +import 'package:logger/logger.dart'; +import 'package:logger/src/filters/type_filter.dart'; +import 'package:logger/src/loggers/caller_logger.dart'; +import 'package:logger/src/printers/caller_printer.dart'; + +var logger = CallerLogger( + ignoreCallers: { + 'syncTryCatchHandler', + }, + filter: TypeFilter( + ignoreTypes: { + IgnoredClass, + }, + ignoreLevel: Level.warning, + ), + level: Level.verbose, +); + +void main() { + print( + 'Run with either `dart example/caller_logger/main.dart` or `dart --enable-asserts example/caller_logger/main.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/lib/logger.dart b/lib/logger.dart index c5f3951..bbb774d 100644 --- a/lib/logger.dart +++ b/lib/logger.dart @@ -3,6 +3,7 @@ library logger; export 'src/ansi_color.dart'; +export 'src/filters/type_filter.dart'; export 'src/filters/development_filter.dart'; export 'src/filters/production_filter.dart'; @@ -16,6 +17,9 @@ export 'src/printers/logfmt_printer.dart'; export 'src/printers/simple_printer.dart'; export 'src/printers/hybrid_printer.dart'; export 'src/printers/prefix_printer.dart'; +export 'src/printers/caller_printer.dart'; + +export 'src/loggers/caller_logger.dart'; export 'src/log_output.dart' if (dart.library.io) 'src/outputs/file_output.dart'; diff --git a/lib/src/filters/type_filter.dart b/lib/src/filters/type_filter.dart new file mode 100644 index 0000000..0d00886 --- /dev/null +++ b/lib/src/filters/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/loggers/caller_logger.dart b/lib/src/loggers/caller_logger.dart new file mode 100644 index 0000000..8243356 --- /dev/null +++ b/lib/src/loggers/caller_logger.dart @@ -0,0 +1,66 @@ +import 'package:logger/logger.dart'; +import 'package:logger/src/printers/caller_printer.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/printers/caller_printer.dart b/lib/src/printers/caller_printer.dart new file mode 100644 index 0000000..da2a640 --- /dev/null +++ b/lib/src/printers/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:'); + } +}