Skip to content

Commit 3f29b8e

Browse files
committed
🎉 Initial setup for the new improved logger. BoltLogger!
1 parent 61f6e2d commit 3f29b8e

14 files changed

+538
-0
lines changed

lib/logger/bolt_logger.dart

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import 'dart:async';
2+
3+
import 'package:dcc_toolkit/logger/charges/bolt_charge.dart';
4+
import 'package:dcc_toolkit/logger/charges/debug_console_charge.dart';
5+
import 'package:dcc_toolkit/logger/charges/file_charge.dart';
6+
import 'package:dcc_toolkit/logger/charges/memory_charge.dart';
7+
import 'package:dcc_toolkit/logger/zap_event.dart';
8+
import 'package:logging/logging.dart';
9+
10+
/// {@template bolt_logger}
11+
/// Super charge your logging and zap logs like Zeus with the BoltLogger.
12+
/// {@endtemplate}
13+
class BoltLogger {
14+
/// {@macro bolt_logger}
15+
factory BoltLogger() => _instance;
16+
17+
BoltLogger._();
18+
19+
static final _instance = BoltLogger._();
20+
21+
StreamSubscription<LogRecord>? _subscription;
22+
23+
final Map<String, BoltCharge> _outputs = {};
24+
25+
/// Add [BoltCharge] charges to the logger. This will start the logger if it is not already running.
26+
/// Without any charges, the logger will not output any logs.
27+
///
28+
/// Available charges:
29+
/// - [DebugConsoleCharge]
30+
/// - [MemoryCharge]
31+
/// - [FileCharge]
32+
///
33+
/// For example to add a [DebugConsoleCharge] charge, use the following code snippet:
34+
/// ```dart
35+
/// BoltLogger.charge([DebugConsoleCharge()]);
36+
/// ```
37+
static void charge(List<BoltCharge> charges) {
38+
for (final charge in charges) {
39+
_instance._outputs.putIfAbsent(charge.name, () => charge);
40+
}
41+
_instance._subscribeIfNeeded();
42+
}
43+
44+
/// Look up registered charge by name.
45+
static BoltCharge? getCharge(String name) {
46+
return _instance._outputs[name];
47+
}
48+
49+
void _subscribeIfNeeded() {
50+
Logger.root.level = Level.ALL;
51+
_subscription ??= Logger.root.onRecord.listen((record) {
52+
for (final output in _outputs.values) {
53+
output.logOutput(ZapEvent.fromRecord(record));
54+
}
55+
});
56+
}
57+
58+
/// Discharge the logger and all charges.
59+
void discharge() {
60+
_subscription?.cancel();
61+
_subscription = null;
62+
for (final output in _outputs.values) {
63+
output.discharge();
64+
}
65+
_outputs.clear();
66+
}
67+
68+
/// {@template zap}
69+
/// Zap a log message with optional [tag] and [level].
70+
///
71+
/// For example to zap a message:
72+
/// ```dart
73+
/// BoltLogger.zap('Electricity is in the air!');
74+
/// ```
75+
///
76+
/// The [message] can also be an [Exception]/[Error], [StackTrace] or a [List] to zap all 3 types ([Object]?, [Exception]/[Error], [StackTrace]). These will be passed to the logger.
77+
///
78+
/// ```dart
79+
/// BoltLogger.zap(['Electricity is in the air!', myException, stackTrace]);
80+
/// ```
81+
///
82+
/// {@endtemplate}
83+
static void zap(
84+
Object? message, {
85+
String? tag,
86+
Level level = Level.INFO,
87+
}) {
88+
Object? msg;
89+
Object? error;
90+
StackTrace? stacktrace;
91+
92+
void zapMap(Object? value) {
93+
if (value is Exception || value is Error && error == null) {
94+
error = value;
95+
} else if (value is StackTrace && stacktrace == null) {
96+
stacktrace = value;
97+
} else if (msg == null) {
98+
msg = value;
99+
} else {
100+
final errorMessage =
101+
'When zapping a list it can only contain one of each Exception/Error, StackTrace or Object?: $message';
102+
assert(false, errorMessage);
103+
error = AssertionError([errorMessage]);
104+
msg = '';
105+
stacktrace = StackTrace.empty;
106+
}
107+
}
108+
109+
if (message is List) {
110+
for (final Object? value in message) {
111+
zapMap(value);
112+
}
113+
}else {
114+
zapMap(message);
115+
}
116+
117+
Logger(tag ?? 'BoltLogger').log(level, msg, error, stacktrace);
118+
}
119+
120+
/// {@template shock}
121+
/// Shock is a zap intensified! It zaps a log message default [level] of [Level.SEVERE].
122+
///
123+
/// {@macro zap}
124+
///
125+
/// {@endtemplate}
126+
static void shock(
127+
Object? message, {
128+
String? tag,
129+
Level level = Level.SEVERE,
130+
}) {
131+
zap(message, tag: tag, level: level);
132+
}
133+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'package:dcc_toolkit/logger/bolt_logger.dart';
2+
import 'package:dcc_toolkit/logger/zap_event.dart';
3+
4+
/// Interface for creating [BoltCharge]s to power the [BoltLogger]
5+
abstract interface class BoltCharge {
6+
7+
/// The name of the charge.
8+
abstract final String name;
9+
/// Log the output of the [ZapEvent].
10+
void logOutput(ZapEvent event);
11+
12+
/// Discharge the charge.
13+
void discharge();
14+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import 'package:dcc_toolkit/logger/charges/bolt_charge.dart';
2+
import 'package:dcc_toolkit/logger/util/ansi_support.dart'
3+
if (dart.library.io) 'package:dcc_toolkit/logger/util/ansi_support_io.dart'
4+
if (dart.library.html) 'package:dcc_toolkit/logger/util/ansi_support_web.dart';
5+
import 'package:dcc_toolkit/logger/zap_event.dart';
6+
import 'package:flutter/foundation.dart';
7+
import 'package:logging/logging.dart';
8+
9+
/// {@template debug_console_charge}
10+
/// A [BoltCharge] that logs output to the console with ANSI color support.
11+
/// {@endtemplate}
12+
class DebugConsoleCharge implements BoltCharge {
13+
/// {@macro debug_console_charge}
14+
const DebugConsoleCharge();
15+
@override
16+
String get name => 'DebugConsoleCharge';
17+
18+
@override
19+
void logOutput(ZapEvent event) {
20+
if (!kDebugMode) return;
21+
_paintLines(event).forEach(debugPrint);
22+
}
23+
24+
List<String> _paintLines(ZapEvent event) {
25+
final shouldPaint = supportsAnsiEscapes &&
26+
(event.origin.level.value >= Level.SEVERE.value ||
27+
event.origin.stackTrace != null ||
28+
event.origin.error != null);
29+
30+
return shouldPaint ? event.lines.map((line) => '$_red$line$_reset').toList() : event.lines;
31+
}
32+
33+
@override
34+
void discharge() {}
35+
}
36+
37+
const _esc = '\x1B[';
38+
const _reset = '${_esc}0m';
39+
const _red = '${_esc}38;5;1m';
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import 'dart:async';
2+
import 'dart:io';
3+
4+
import 'package:dcc_toolkit/logger/charges/bolt_charge.dart';
5+
import 'package:dcc_toolkit/logger/zap_event.dart';
6+
import 'package:intl/intl.dart';
7+
8+
/// {@template file_charge}
9+
/// A [BoltCharge] that logs output to a file.
10+
///
11+
/// The [FileCharge] will write logs to a file in the specified [path].
12+
/// Logs are written to the file in batches, this is done when it reaches the [bufferSize] or every [writeDelay].
13+
///
14+
/// {@endtemplate}
15+
class FileCharge implements BoltCharge {
16+
17+
/// {@macro file_charge}
18+
FileCharge(this.path, {this.bufferSize = 1000, this.writeDelay = const Duration(seconds: 5)}) {
19+
final fileName = '${DateFormat('yyyy-MM-dd').format(DateTime.now())}.log';
20+
_file = File('$path/$fileName');
21+
22+
Timer.periodic(writeDelay, (_) => _flush());
23+
}
24+
@override
25+
String get name => 'FileCharge';
26+
27+
/// The size of the buffer (in lines) before writing to the file.
28+
final int bufferSize;
29+
/// The path to the directory where the log files will be written.
30+
final String path;
31+
/// The delay between writing to the file.
32+
final Duration writeDelay;
33+
File? _file;
34+
final List<ZapEvent> _buffer = [];
35+
IOSink? _sink;
36+
Timer? _timer;
37+
38+
@override
39+
void logOutput(ZapEvent event) {
40+
_buffer.add(event);
41+
42+
if (_buffer.length >= bufferSize) {
43+
_flush();
44+
}
45+
}
46+
47+
void _flush() {
48+
if (_buffer.isEmpty) return;
49+
50+
_sink ??= _file?.openWrite(mode: FileMode.append);
51+
52+
for (final event in _buffer) {
53+
_sink?.writeAll(event.lines, '\n');
54+
_sink?.writeln();
55+
}
56+
_buffer.clear();
57+
}
58+
59+
@override
60+
void discharge() {
61+
_timer?.cancel();
62+
_timer = null;
63+
_flush();
64+
_sink?.close();
65+
_sink = null;
66+
}
67+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import 'dart:async';
2+
3+
import 'package:dcc_toolkit/logger/charges/bolt_charge.dart';
4+
import 'package:dcc_toolkit/logger/ui/bolt_logger_view.dart';
5+
import 'package:dcc_toolkit/logger/zap_event.dart';
6+
7+
/// {@template memory_charge}
8+
/// A [BoltCharge] that stores logs in memory.
9+
/// This will be used for viewing logs in the app. With the [BoltLoggerView] widget.
10+
///
11+
/// By default, the [maxItems] is set to 1000. This is the amount of logs that will be stored in memory.
12+
///
13+
/// {@endtemplate}
14+
class MemoryCharge implements BoltCharge {
15+
/// {@macro memory_charge}
16+
MemoryCharge({this.maxItems = 1000});
17+
18+
@override
19+
String get name => 'MemoryCharge';
20+
21+
/// The maximum amount of logs to store in memory.
22+
final int maxItems;
23+
final List<ZapEvent> _items = [];
24+
25+
final StreamController<ZapEvent> _controller = StreamController.broadcast();
26+
27+
/// The stream of [ZapEvent]s.
28+
Stream<ZapEvent> get stream => _controller.stream;
29+
30+
/// The list of [ZapEvent]s currently stored in memory.
31+
List<ZapEvent> get items => _items.toList(growable: false);
32+
33+
@override
34+
void logOutput(ZapEvent event) {
35+
if (_items.length >= maxItems) {
36+
_items.removeAt(0);
37+
}
38+
_items.add(event);
39+
_controller.add(event);
40+
}
41+
42+
@override
43+
void discharge() {
44+
_controller.close();
45+
_items.clear();
46+
}
47+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// StackTrace extension methods.
2+
extension ZapStackTraceExtension on StackTrace {
3+
4+
/// Strike the [StackTrace] which will format it so that it's easier to read.
5+
String get strike => toString()
6+
.split('\n')
7+
.map((e) => e.isNotEmpty && e.startsWith('#') ? e.replaceAll(RegExp(r'\s\s+'), ' ') : null)
8+
.whereType<String>()
9+
.join('\n');
10+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import 'package:dcc_toolkit/logger/bolt_logger.dart';
2+
import 'package:logging/logging.dart';
3+
4+
/// Extension to zap messages.
5+
extension ZapExtension on Object {
6+
/// {@macro zap}
7+
void zap(
8+
Object? message, {
9+
String? tag,
10+
Level level = Level.INFO,
11+
}) {
12+
//ignore: no_runtimeType_toString
13+
BoltLogger.zap(message, tag: tag ?? runtimeType.toString(), level: level);
14+
}
15+
16+
/// {@macro shock}
17+
void shock(
18+
Object? message, {
19+
String? tag,
20+
Level level = Level.SEVERE,
21+
}) {
22+
//ignore: no_runtimeType_toString
23+
BoltLogger.shock(message, tag: tag ?? runtimeType.toString(), level: level);
24+
}
25+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import 'package:dcc_toolkit/logger/extensions/stacktrace.dart';
2+
import 'package:intl/intl.dart';
3+
import 'package:logging/logging.dart';
4+
5+
/// {@template log_record_formatter}
6+
/// A formatter that formats a [LogRecord] into a list of strings.
7+
/// {@endtemplate}
8+
class LogRecordFormatter {
9+
/// {@macro log_record_formatter}
10+
const LogRecordFormatter();
11+
12+
static final DateFormat _formatter = DateFormat(DateFormat.HOUR24_MINUTE);
13+
14+
/// Format the [record] into a list of strings.
15+
List<String> format(LogRecord record) {
16+
final lines = <String>[];
17+
final prefix = _prefix(record);
18+
19+
lines
20+
..addAll(_formatMessage(record))
21+
..addAll(_formatErrors(record))
22+
..addAll(_formatStacktrace(record));
23+
24+
return lines.map((e) => '$prefix$e').toList();
25+
}
26+
27+
List<String> _formatMessage(LogRecord record) {
28+
if (record.message == 'null'|| record.message.trim().isEmpty) return [];
29+
30+
return record.message.split('\n');
31+
}
32+
33+
List<String> _formatErrors(LogRecord record) {
34+
if (record.error == null) return [];
35+
36+
return record.error.toString().split('\n').map((e) => e.trim()).toList();
37+
}
38+
39+
List<String> _formatStacktrace(LogRecord record) => record.stackTrace?.strike.split('\n') ?? [];
40+
41+
String _prefix(LogRecord record) {
42+
final tag = record.loggerName;
43+
final level = record.level.name.substring(0, 1);
44+
final time = _formatter.format(record.time);
45+
return '⚡[$time] $level/$tag: ';
46+
}
47+
}

0 commit comments

Comments
 (0)