From 82cae234b1642147255fa3c663143d2f03ff82d9 Mon Sep 17 00:00:00 2001 From: "susa.keyber" Date: Sat, 30 Apr 2022 08:16:03 +0900 Subject: [PATCH] #1 implement roggle --- .run/main.run.xml | 6 + CHANGELOG.md | 4 +- README.md | 16 +- analysis_options.yaml | 10 +- example/main.dart | 22 ++ lib/roggle.dart | 9 +- lib/src/printers/single_pretty_printer.dart | 302 ++++++++++++++++++ lib/src/roggle.dart | 110 +++++++ pubspec.yaml | 53 +-- test/printers/single_pretty_printer_test.dart | 1 + test/roggle_test.dart | 171 +++++++++- 11 files changed, 628 insertions(+), 76 deletions(-) create mode 100644 .run/main.run.xml create mode 100644 example/main.dart create mode 100644 lib/src/printers/single_pretty_printer.dart create mode 100644 lib/src/roggle.dart create mode 100644 test/printers/single_pretty_printer_test.dart diff --git a/.run/main.run.xml b/.run/main.run.xml new file mode 100644 index 0000000..d72fd86 --- /dev/null +++ b/.run/main.run.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cc7d8..2960966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ -## 0.0.1 +## 0.1.0 -* TODO: Describe initial release. +- First version \ No newline at end of file diff --git a/README.md b/README.md index 8b55e73..b353cb3 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,6 @@ - - -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. +Simple, colorful and easy to expand logger for dart. ## Features diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1..e4add5b 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,8 @@ -include: package:flutter_lints/flutter.yaml +# https://pub.dev/packages/pedantic_mono +include: package:pedantic_mono/analysis_options.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +linter: + rules: + avoid_classes_with_only_static_members: false + constant_identifier_names: true + prefer_relative_imports: true diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 0000000..26d5a93 --- /dev/null +++ b/example/main.dart @@ -0,0 +1,22 @@ +import 'package:roggle/roggle.dart'; + +final logger = Roggle( + printer: SinglePrettyPrinter( + loggerName: '[APP]', + stackTraceLevel: Level.error, + ), +); + +void main() { + print( + 'Run with either `dart example/main.dart` or `dart --enable-asserts example/main.dart`.'); + demo(); +} + +void demo() { + logger.d('Log message with 2 methods'); + + logger.e('Error! Something bad happened', 'Test Error'); + + logger.i('Log message'); +} diff --git a/lib/roggle.dart b/lib/roggle.dart index 2df860b..e0391d9 100644 --- a/lib/roggle.dart +++ b/lib/roggle.dart @@ -1,7 +1,6 @@ library roggle; -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; -} +export 'package:logger/logger.dart' hide Logger, LogCallback, OutputCallback; + +export 'src/printers/single_pretty_printer.dart'; +export 'src/roggle.dart'; diff --git a/lib/src/printers/single_pretty_printer.dart b/lib/src/printers/single_pretty_printer.dart new file mode 100644 index 0000000..27fb9d6 --- /dev/null +++ b/lib/src/printers/single_pretty_printer.dart @@ -0,0 +1,302 @@ +import 'package:logger/logger.dart'; +import 'package:stack_trace/stack_trace.dart'; + +/// Default implementation of [LogPrinter]. +/// +/// Output looks like this: +/// ``` +/// 💡 [INFO] 06:46:15.354 demo (file:///your/file/path/roggle/example/main.dart:16:10): Log message +/// ``` +class SinglePrettyPrinter extends LogPrinter { + SinglePrettyPrinter({ + this.loggerName, + this.colors = true, + this.printCaller = true, + this.printEmoji = true, + this.printLevel = true, + this.printTime = true, + this.stackTraceLevel = Level.nothing, + this.stackTraceMethodCount = 20, + this.stackTraceFilters = const [], + this.stackTracePrefix = _defaultStackTracePrefix, + Map? levelColors, + this.levelEmojis = _defaultLevelEmojis, + this.levelLabels = _defaultLevelLabels, + }) : _levelColors = levelColors ?? _defaultLevelColors; + + /// If specified, it will be output at the beginning of the log. + final String? loggerName; + + /// If set to true, the log will be colorful. + final bool colors; + + /// If set to true, caller will be output to the log. + final bool printCaller; + + /// If set to true, the emoji will be output to the log. + final bool printEmoji; + + /// If set to true, the log level string will be output to the log. + final bool printLevel; + + /// If set to true, the time stamp will be output to the log. + final bool printTime; + + /// The current logging level to display stack trace. + /// + /// All stack traces with levels below this level will be omitted. + final Level stackTraceLevel; + + /// Number of stack trace methods to display. + final int stackTraceMethodCount; + + /// No stack trace that matches the regular expression is output. + final List stackTraceFilters; + + /// Stack trace prefix. + final String stackTracePrefix; + + /// Color for each log level. + final Map _levelColors; + + /// Emoji for each log level. + final Map levelEmojis; + + /// String for each log level. + final Map levelLabels; + + /// Path to this file. + static final _selfPath = _getSelfPath(); + + /// Stack trace prefix default. + static const _defaultStackTracePrefix = '│'; + + /// Color default for each log level. + static final _defaultLevelColors = { + 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), + }; + + /// Emoji default for each log level. + static const _defaultLevelEmojis = { + Level.verbose: '🐱', + Level.debug: '🐛', + Level.info: '💡', + Level.warning: '⚠️', + Level.error: '⛔', + Level.wtf: '👾', + }; + + /// String default for each log level. + static const _defaultLevelLabels = { + Level.verbose: '[VERBOSE]', + Level.debug: '[DEBUG] ', + Level.info: '[INFO] ', + Level.warning: '[WARNING]', + Level.error: '[ERROR] ', + Level.wtf: '[WTF] ', + }; + + /// 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]+\/)'); + + /// Returns the path to this file. + static String _getSelfPath() { + final match = RegExp(r'^(.+.dart)').firstMatch(Frame.caller(0).toString()); + if (match == null) { + return ''; + } + return match.group(1)!; + } + + @override + List log(LogEvent event) { + List? stackTraceLines; + if (event.stackTrace != null) { + // If stackTrace is not null, it will be displayed with priority. + stackTraceLines = _getStackTrace(stackTrace: event.stackTrace); + } else if (event.level.index >= stackTraceLevel.index) { + stackTraceLines = _getStackTrace(); + } + + return _formatMessage( + level: event.level, + message: _stringifyMessage(event.message), + error: event.error?.toString(), + stackTrace: stackTraceLines, + ); + } + + String? _getCaller() { + final lines = StackTrace.current.toString().split('\n'); + for (final line in lines) { + if (_discardDeviceStackTraceLine(line) || + _discardWebStackTraceLine(line) || + _discardUserStacktraceLine(line) || + line.isEmpty) { + continue; + } + + // Remove unnecessary parts. + if (_deviceStackTraceRegex.matchAsPrefix(line) != null) { + return line + .replaceFirst(RegExp(r'#\d+\s+'), '') + .replaceFirst(RegExp(r'package:[a-z0-9_]+\/'), '/'); + } + if (_webStackTraceRegex.matchAsPrefix(line) != null) { + return line.replaceFirst(RegExp(r'^packages\/[a-z0-9_]+\/'), '/'); + } + } + return null; + } + + List _getStackTrace({ + StackTrace? stackTrace, + }) { + final lines = (stackTrace ?? StackTrace.current).toString().split('\n'); + final formatted = []; + var count = 0; + for (final line in lines) { + if (_discardDeviceStackTraceLine(line) || + _discardWebStackTraceLine(line) || + _discardUserStacktraceLine(line) || + line.isEmpty) { + continue; + } + final replaced = line.replaceFirst(RegExp(r'#\d+\s+'), ''); + formatted.add('$stackTracePrefix #$count $replaced'); + if (++count == stackTraceMethodCount) { + break; + } + } + return formatted; + } + + bool _discardDeviceStackTraceLine(String line) { + final match = _deviceStackTraceRegex.matchAsPrefix(line); + if (match == null) { + return false; + } + return match.group(2)!.startsWith('package:roggle') || + line.contains(_selfPath); + } + + bool _discardWebStackTraceLine(String line) { + final match = _webStackTraceRegex.matchAsPrefix(line); + if (match == null) { + return false; + } + return match.group(1)!.startsWith('packages/roggle') || + match.group(1)!.startsWith('dart-sdk/lib') || + line.startsWith(_selfPath); + } + + bool _discardUserStacktraceLine(String line) => + stackTraceFilters.any((element) => element.hasMatch(line)); + + String _getCurrentTime() { + String _threeDigits(int n) { + if (n >= 100) { + return '$n'; + } + if (n >= 10) { + return '0$n'; + } + return '00$n'; + } + + String _twoDigits(int n) { + if (n >= 10) { + return '$n'; + } + return '0$n'; + } + + final now = DateTime.now(); + final h = _twoDigits(now.hour); + final min = _twoDigits(now.minute); + final sec = _twoDigits(now.second); + final ms = _threeDigits(now.millisecond); + return '$h:$min:$sec.$ms'; + } + + String _stringifyMessage(dynamic message) { + if (message is dynamic Function()) { + return message().toString(); + } else if (message is String) { + return message; + } + return message.toString(); + } + + AnsiColor _getLevelColor(Level level) { + if (colors) { + return _levelColors[level]!; + } else { + return AnsiColor.none(); + } + } + + List _formatMessage({ + required Level level, + required String message, + String? error, + List? stackTrace, + }) { + final color = _getLevelColor(level); + final fixed = _formatFixed(level: level); + final logs = [ + color('$fixed$message'), + ]; + + if (error != null) { + logs.add(color('$fixed$stackTracePrefix $error')); + } + + if (stackTrace != null && stackTrace.isNotEmpty) { + for (final line in stackTrace) { + logs.add(color('$fixed$line')); + } + } + return logs; + } + + String _formatFixed({ + required Level level, + }) { + final buffer = []; + + if (printEmoji) { + buffer.add(levelEmojis[level]!); + } + if (loggerName != null) { + buffer.add(loggerName!); + } + if (printLevel) { + buffer.add(levelLabels[level]!); + } + if (printTime) { + buffer.add(_getCurrentTime()); + } + if (printCaller) { + final caller = _getCaller(); + if (caller != null) { + buffer.add(caller); + } + } + return buffer.isNotEmpty ? '${buffer.join(' ')}: ' : ''; + } +} diff --git a/lib/src/roggle.dart b/lib/src/roggle.dart new file mode 100644 index 0000000..961c5de --- /dev/null +++ b/lib/src/roggle.dart @@ -0,0 +1,110 @@ +import 'package:logger/logger.dart'; + +import 'printers/single_pretty_printer.dart'; + +/// Use instances of roggle to send log messages to the [LogPrinter]. +class Roggle { + /// Create a new instance of Roggle. + /// + /// You can provide a custom [printer], [filter] and [output]. Otherwise the + /// defaults: [SinglePrettyPrinter], [DevelopmentFilter] and [ConsoleOutput] + /// will be used. + Roggle({ + LogFilter? filter, + LogPrinter? printer, + LogOutput? output, + Level? level, + }) : _filter = filter ?? DevelopmentFilter(), + _printer = printer ?? SinglePrettyPrinter(), + _output = output ?? ConsoleOutput() { + _filter.init(); + // ignore: cascade_invocations + _filter.level = level ?? Logger.level; + _printer.init(); + _output.init(); + } + + /// The current logging level of the app. + /// + /// All logs with levels below this level will be omitted. + static Level level = Level.verbose; + + final LogFilter _filter; + LogFilter get filter => _filter; + + final LogPrinter _printer; + LogPrinter get printer => _printer; + + final LogOutput _output; + LogOutput get output => _output; + + bool _active = true; + bool get active => _active; + + /// Log a message at level [Level.verbose]. + List v(Object message, [Object? error, StackTrace? stackTrace]) { + return log(Level.verbose, message, error, stackTrace); + } + + /// Log a message at level [Level.debug]. + List d(Object message, [Object? error, StackTrace? stackTrace]) { + return log(Level.debug, message, error, stackTrace); + } + + /// Log a message at level [Level.info]. + List i(Object message, [Object? error, StackTrace? stackTrace]) { + return log(Level.info, message, error, stackTrace); + } + + /// Log a message at level [Level.warning]. + List w(Object message, [Object? error, StackTrace? stackTrace]) { + return log(Level.warning, message, error, stackTrace); + } + + /// Log a message at level [Level.error]. + List e(Object message, [Object? error, StackTrace? stackTrace]) { + return log(Level.error, message, error, stackTrace); + } + + /// Log a message at level [Level.wtf]. + List wtf(Object message, [Object? error, StackTrace? stackTrace]) { + return log(Level.wtf, message, error, stackTrace); + } + + /// Log a message with [level]. + List log( + Level level, + Object message, [ + Object? error, + StackTrace? stackTrace, + ]) { + if (!_active) { + throw ArgumentError('Logger has already been closed.'); + } else if (error != null && error is StackTrace) { + throw ArgumentError('Error parameter cannot take a StackTrace!'); + } else if (level == Level.nothing) { + throw ArgumentError('Log events cannot have Level.nothing'); + } + var output = []; + final logEvent = LogEvent(level, message, error, stackTrace); + if (_filter.shouldLog(logEvent)) { + output = _printer.log(logEvent); + + if (output.isNotEmpty) { + final outputEvent = OutputEvent(level, output); + // I didn't try to catch it because I wanted + // to stop the app on purpose. + _output.output(outputEvent); + } + } + return output; + } + + /// Closes the logger and releases all resources. + void close() { + _active = false; + _filter.destroy(); + _printer.destroy(); + _output.destroy(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index cca7b3b..91cf690 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,54 +1,15 @@ name: roggle -description: A new Flutter package project. -version: 0.0.1 -homepage: +description: Simple, colorful and easy to expand logger for dart. +version: 0.1.0 +homepage: https://github.com/keyber-inc/roggle environment: sdk: ">=2.16.2 <3.0.0" - flutter: ">=1.17.0" dependencies: - flutter: - sdk: flutter + logger: ^1.1.0 + stack_trace: ^1.10.0 dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^1.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. -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 + pedantic_mono: ^1.18.0 + test: ^1.21.1 diff --git a/test/printers/single_pretty_printer_test.dart b/test/printers/single_pretty_printer_test.dart new file mode 100644 index 0000000..ab73b3a --- /dev/null +++ b/test/printers/single_pretty_printer_test.dart @@ -0,0 +1 @@ +void main() {} diff --git a/test/roggle_test.dart b/test/roggle_test.dart index 91b1e32..7fe5786 100644 --- a/test/roggle_test.dart +++ b/test/roggle_test.dart @@ -1,12 +1,171 @@ -import 'package:flutter_test/flutter_test.dart'; +import 'dart:math'; import 'package:roggle/roggle.dart'; +import 'package:test/test.dart'; + +typedef PrinterCallback = List Function( + Level level, + dynamic message, + dynamic error, + StackTrace? stackTrace, +); + +class _AlwaysFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) => true; +} + +class _NeverFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) => false; +} + +class _CallbackPrinter extends LogPrinter { + _CallbackPrinter(this.callback); + + final PrinterCallback callback; + + @override + List log(LogEvent event) { + return callback( + event.level, + event.message, + event.error, + event.stackTrace, + ); + } +} void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); + Level? printedLevel; + dynamic printedMessage; + dynamic printedError; + StackTrace? printedStackTrace; + final callbackPrinter = _CallbackPrinter((l, dynamic m, dynamic e, s) { + printedLevel = l; + printedMessage = m; + printedError = e; + printedStackTrace = s; + return []; + }); + + setUp(() { + printedLevel = null; + printedMessage = null; + printedError = null; + printedStackTrace = null; + }); + + test('Roggle.log', () { + var logger = Roggle(filter: _NeverFilter(), printer: callbackPrinter) + ..log(Level.debug, 'Some message'); + + expect(printedMessage, null); + + logger = Roggle(filter: _AlwaysFilter(), printer: callbackPrinter); + + final levels = Level.values.take(6); + for (final level in levels) { + var message = Random().nextInt(999999999).toString(); + logger.log(level, message); + expect(printedLevel, level); + expect(printedMessage, message); + expect(printedError, null); + expect(printedStackTrace, null); + + message = Random().nextInt(999999999).toString(); + logger.log(level, message, 'MyError'); + expect(printedLevel, level); + expect(printedMessage, message); + expect(printedError, 'MyError'); + expect(printedStackTrace, null); + + message = Random().nextInt(999999999).toString(); + final stackTrace = StackTrace.current; + logger.log(level, message, 'MyError', stackTrace); + expect(printedLevel, level); + expect(printedMessage, message); + expect(printedError, 'MyError'); + expect(printedStackTrace, stackTrace); + } + + expect( + () => logger.log(Level.verbose, 'Test', StackTrace.current), + throwsArgumentError, + ); + expect(() => logger.log(Level.nothing, 'Test'), throwsArgumentError); + }); + + test('Roggle.v', () { + final logger = Roggle(filter: _AlwaysFilter(), printer: callbackPrinter); + final stackTrace = StackTrace.current; + logger.v('Test', 'Error', stackTrace); + expect(printedLevel, Level.verbose); + expect(printedMessage, 'Test'); + expect(printedError, 'Error'); + expect(printedStackTrace, stackTrace); + }); + + test('Roggle.d', () { + final logger = Roggle(filter: _AlwaysFilter(), printer: callbackPrinter); + final stackTrace = StackTrace.current; + logger.d('Test', 'Error', stackTrace); + expect(printedLevel, Level.debug); + expect(printedMessage, 'Test'); + expect(printedError, 'Error'); + expect(printedStackTrace, stackTrace); + }); + + test('Roggle.i', () { + final logger = Roggle(filter: _AlwaysFilter(), printer: callbackPrinter); + final stackTrace = StackTrace.current; + logger.i('Test', 'Error', stackTrace); + expect(printedLevel, Level.info); + expect(printedMessage, 'Test'); + expect(printedError, 'Error'); + expect(printedStackTrace, stackTrace); + }); + + test('Roggle.w', () { + final logger = Roggle(filter: _AlwaysFilter(), printer: callbackPrinter); + final stackTrace = StackTrace.current; + logger.w('Test', 'Error', stackTrace); + expect(printedLevel, Level.warning); + expect(printedMessage, 'Test'); + expect(printedError, 'Error'); + expect(printedStackTrace, stackTrace); + }); + + test('Roggle.e', () { + final logger = Roggle(filter: _AlwaysFilter(), printer: callbackPrinter); + final stackTrace = StackTrace.current; + logger.e('Test', 'Error', stackTrace); + expect(printedLevel, Level.error); + expect(printedMessage, 'Test'); + expect(printedError, 'Error'); + expect(printedStackTrace, stackTrace); + }); + + test('Roggle.wtf', () { + final logger = Roggle(filter: _AlwaysFilter(), printer: callbackPrinter); + final stackTrace = StackTrace.current; + logger.wtf('Test', 'Error', stackTrace); + expect(printedLevel, Level.wtf); + expect(printedMessage, 'Test'); + expect(printedError, 'Error'); + expect(printedStackTrace, stackTrace); + }); + + test('setting log level above log level of message', () { + printedMessage = null; + final logger = Roggle( + filter: ProductionFilter(), + printer: callbackPrinter, + level: Level.warning, + )..d('This isn\'t logged'); + expect(printedMessage, isNull); + + logger.w('This is'); + expect(printedMessage, 'This is'); }); }