Line data Source code
1 : import 'dart:async';
2 :
3 : import 'package:collection/collection.dart';
4 : import 'package:flutter/foundation.dart';
5 : import 'package:logging/logging.dart';
6 : import 'package:the_logger/src/abstract_logger.dart';
7 : import 'package:the_logger/src/console_logger.dart';
8 : import 'package:the_logger/src/db/logger_database.dart';
9 : import 'package:the_logger/src/db_logger.dart';
10 : import 'package:the_logger/src/models/models.dart';
11 :
12 : export 'abstract_logger.dart';
13 : export 'models/console_colors.dart';
14 : export 'models/masked_log_record.dart';
15 : export 'models/masking_string.dart';
16 :
17 : /// The Logger: modularity, extensibility, testability
18 : class TheLogger {
19 : /// Get logger instance
20 5 : factory TheLogger.i() {
21 5 : _instance ??= TheLogger._();
22 : return _instance!;
23 : }
24 5 : TheLogger._();
25 :
26 : static TheLogger? _instance;
27 :
28 : final _log = Logger('TheLogger');
29 0 : late List<AbstractLogger> _loggers = [];
30 : Level _minLevel = Level.ALL;
31 : String? _sessionStartExtra;
32 : late Level _sessionStartLevel;
33 :
34 : final MaskingStrings _maskingStrings = {};
35 :
36 : bool _initialized = false;
37 :
38 : final StreamController<MaskedLogRecord> _streamController =
39 : StreamController<MaskedLogRecord>.broadcast();
40 :
41 : /// A broadcast stream of masked log records.
42 : ///
43 : /// Use this to monitor logs in real-time. Supports multiple listeners.
44 : /// Can be filtered with standard [Stream.where]:
45 : /// ```dart
46 : /// TheLogger.i().stream
47 : /// .where((r) => r.level >= Level.WARNING)
48 : /// .listen((record) => print(record.message));
49 : /// ```
50 3 : Stream<MaskedLogRecord> get stream => _streamController.stream;
51 :
52 : /// Init app logger
53 : ///
54 : /// [retainStrategy] processing algorythm:
55 : /// * sort all records by level (ALL->OFF)
56 : /// * record with minimum level will be used as global filter
57 : /// (for storing and printing)
58 : /// * each integer for a level means how many sessions the records with this
59 : /// level will be retained
60 : /// * each next entry will add this number
61 : /// * if [retainStrategy] is empty => {Level.ALL: 10}
62 : /// So, examples:
63 : ///
64 : /// {
65 : /// Level.ALL: 200, // ALL records will be deleted after 200 sessions
66 : /// Level.INFO: 100, // records with INFO and higher level retained for 300 sessions
67 : /// Level.SEVERE: 50, // records with SEVERE and higher level retained for 350 sessions
68 : /// }
69 : ///
70 : /// {
71 : /// Level.CONFIG: 200, // records with CONFIG and higher level retained for 200 sessions
72 : /// // lower level records (FINE, FINER and FINEST) will not
73 : /// // be printed nor stored because lowest level in the map
74 : /// // is CONFIG
75 : /// Level.INFO: 100, // records with INFO and higher level retained for 300 sessions
76 : /// Level.SEVERE: 50, // records with SEVERE and higher level retained for 350 sessions
77 : /// }
78 : ///
79 : /// {
80 : /// Level.OFF: 0, // disable logging
81 : /// }
82 : ///
83 : /// {
84 : /// Level.ALL: 1, // all level records will be retained for 1 session
85 : /// // (i.e. you will be able to retrieve the logs from the
86 : /// // previous run)
87 : /// }
88 : /// [startNewSession] - if true, new session will be started
89 : /// [consoleLogger] - if true, console logger will be used
90 : /// [dbLogger] - if true, db logger will be used
91 : /// [maskDbLogger] - mask sensitive data in db logger
92 : /// [consoleLoggerCallback] - callback for console logger
93 : /// [consoleColors] - console colors
94 : /// [consoleFormatJson] - format JSON in console logger
95 : /// [sessionStartExtra] - extra info for session start, will be added to all
96 : /// session start records
97 : /// [customLoggers] - custom loggers
98 : /// [sessionStartLevel] - session start log level
99 5 : Future<void> init({
100 : Map<Level, int> retainStrategy = const {},
101 : bool startNewSession = true,
102 : bool consoleLogger = true,
103 : bool dbLogger = true,
104 : bool maskDbLogger = true,
105 : ConsoleLoggerCallback? consoleLoggerCallback,
106 : ConsoleColors consoleColors = const ConsoleColors(),
107 : bool consoleFormatJson = true,
108 : String? sessionStartExtra,
109 : List<AbstractLogger>? customLoggers,
110 : Level sessionStartLevel = Level.INFO,
111 : @visibleForTesting LoggerDatabase? database,
112 : }) async {
113 5 : if (_initialized) {
114 0 : _log.warning('TheLogger is already initialized!');
115 : return;
116 : }
117 :
118 5 : _initialized = true;
119 :
120 5 : _sessionStartExtra = sessionStartExtra;
121 5 : _sessionStartLevel = sessionStartLevel;
122 :
123 : // If there are no explicit instructions on how to retain logs
124 5 : final retainStrategyNotEmpty = retainStrategy.isEmpty
125 5 : ? _defaultRetainStrategy()
126 : : retainStrategy;
127 :
128 15 : _minLevel = retainStrategyNotEmpty.keys.reduce(
129 3 : (value, element) => element.compareTo(value) < 0 ? element : value,
130 : );
131 :
132 15 : Logger.root.level = _minLevel;
133 10 : _loggers = <AbstractLogger>[
134 : if (consoleLogger)
135 4 : ConsoleLogger(
136 : loggerCallback: consoleLoggerCallback,
137 : colors: consoleColors,
138 : formatJson: consoleFormatJson,
139 : ),
140 2 : if (dbLogger) DbLogger(),
141 9 : ...customLoggers ?? [],
142 : ];
143 :
144 10 : for (final logger in _loggers) {
145 5 : if (logger is DbLogger) {
146 2 : await logger.init(retainStrategyNotEmpty, database);
147 2 : logger.shouldMask = maskDbLogger;
148 : } else {
149 5 : await logger.init(retainStrategyNotEmpty);
150 : }
151 : }
152 :
153 10 : Logger.root.clearListeners();
154 20 : Logger.root.onRecord.listen(_writeRecord);
155 :
156 1 : if (startNewSession) await startSession();
157 : }
158 :
159 : /// Dispose logger
160 5 : Future<void> dispose() async {
161 5 : _assureInitialized();
162 :
163 10 : Logger.root.clearListeners();
164 10 : await _streamController.close();
165 10 : for (final logger in _loggers) {
166 5 : await logger.dispose();
167 : }
168 : _instance = null;
169 : }
170 :
171 : /// Get computed minimal level
172 0 : Level get minLevel => _minLevel;
173 :
174 5 : void _writeRecord(LogRecord record) {
175 5 : final maskedRecord = MaskedLogRecord.fromLogRecord(
176 : record,
177 5 : maskingStrings: _maskingStrings,
178 : );
179 10 : for (final logger in _loggers) {
180 5 : logger.write(maskedRecord);
181 : }
182 10 : if (!_streamController.isClosed) {
183 10 : _streamController.add(maskedRecord);
184 : }
185 : }
186 :
187 : /// Increment session id
188 4 : Future<void> startSession() async {
189 4 : _assureInitialized();
190 :
191 4 : final logStrings = <String>[];
192 8 : for (final logger in _loggers) {
193 4 : final logString = await logger.sessionStart();
194 : if (logString != null) {
195 3 : logStrings.add(logString);
196 : }
197 : }
198 :
199 4 : final logStringsReduced = logStrings.fold(
200 : '',
201 6 : (previousValue, element) => '$previousValue$element',
202 : );
203 :
204 4 : final extraString = _sessionStartExtra == null
205 : ? ''
206 4 : : ' $_sessionStartExtra';
207 8 : _log.log(
208 4 : _sessionStartLevel,
209 4 : 'Session start $logStringsReduced$extraString',
210 : );
211 : }
212 :
213 : /// Get all logs as strings (for debug purposes only)
214 1 : Future<String> getAllLogsAsString() async {
215 1 : _assureInitialized();
216 :
217 2 : return _dbLogger.getAllLogsAsString();
218 : }
219 :
220 : /// Get all logs as [LogRecord]s (for debug purposes only)
221 0 : @visibleForTesting
222 : Future<List<LogRecord>> getAllLogs() async {
223 0 : _assureInitialized();
224 :
225 0 : return _dbLogger.getAllLogs();
226 : }
227 :
228 : /// Get all logs as maps (for debug purposes only)
229 2 : @visibleForTesting
230 : Future<List<Map<String, Object?>>> getAllLogsAsMaps() async {
231 2 : _assureInitialized();
232 :
233 4 : return _dbLogger.getAllLogsAsMaps();
234 : }
235 :
236 : /// Write logs to archived JSON, return file path
237 1 : Future<String> writeAllLogsToJson([String filename = 'logs.json']) async {
238 1 : _assureInitialized();
239 :
240 2 : return _dbLogger.writeAllLogsToJson(filename);
241 : }
242 :
243 : /// Clear logs (for debug purposes only)
244 2 : Future<void> clearAllLogs() {
245 2 : _assureInitialized();
246 :
247 4 : return _dbLogger.clearAllLogs();
248 : }
249 :
250 : /// Add masking string
251 3 : void addMaskingString(MaskingString maskingString) {
252 3 : _assureInitialized();
253 :
254 6 : addMaskingStrings({maskingString});
255 : }
256 :
257 : /// Add masking strings
258 3 : void addMaskingStrings(MaskingStrings maskingStrings) {
259 3 : _assureInitialized();
260 :
261 6 : _maskingStrings.addAll(maskingStrings);
262 : }
263 :
264 : /// Remove masking string
265 1 : void removeMaskingString(MaskingString maskingString) {
266 1 : _assureInitialized();
267 :
268 2 : removeMaskingStrings({maskingString});
269 : }
270 :
271 : /// Remove masking strings
272 1 : void removeMaskingStrings(MaskingStrings maskingStrings) {
273 1 : _assureInitialized();
274 :
275 2 : _maskingStrings.removeAll(maskingStrings);
276 : }
277 :
278 : /// Clear masking strings
279 1 : void clearMaskingStrings() {
280 1 : _assureInitialized();
281 :
282 2 : _maskingStrings.clear();
283 : }
284 :
285 2 : DbLogger get _dbLogger {
286 : final dbLogger =
287 8 : _loggers.firstWhereOrNull((logger) => logger is DbLogger) as DbLogger?;
288 : if (dbLogger == null) {
289 0 : throw Exception('DbLogger is not initialized!');
290 : }
291 :
292 : return dbLogger;
293 : }
294 :
295 : /// Default retain strategy
296 10 : Map<Level, int> _defaultRetainStrategy() => {Level.ALL: 10};
297 :
298 5 : void _assureInitialized() {
299 5 : if (!_initialized) {
300 0 : throw Exception('TheLogger is not initialized!');
301 : }
302 : }
303 : }
|