Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions example/caller_logger/main.dart
Original file line number Diff line number Diff line change
@@ -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<Exception, dynamic Function()?>? 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();
}
}
4 changes: 4 additions & 0 deletions lib/logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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';
Expand Down
40 changes: 40 additions & 0 deletions lib/src/filters/type_filter.dart
Original file line number Diff line number Diff line change
@@ -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<Type> 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);
}
}
66 changes: 66 additions & 0 deletions lib/src/loggers/caller_logger.dart
Original file line number Diff line number Diff line change
@@ -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<String> ignoreCallers,
}) : _ignoreCallers = ignoreCallers,
super(filter: filter, printer: printer, output: output, level: level);

factory CallerLogger({
LogFilter? filter,
LogPrinter? printer,
LogOutput? output,
Level? level,
Set<String>? 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<String> _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);
}
}
159 changes: 159 additions & 0 deletions lib/src/printers/caller_printer.dart
Original file line number Diff line number Diff line change
@@ -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<String> 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 = <String>[];
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:');
}
}