Line data Source code
1 : import 'dart:async';
2 :
3 : import 'package:drift/drift.dart';
4 : import 'package:flutter/foundation.dart';
5 : import 'package:flutter/widgets.dart';
6 : import 'package:logging/logging.dart';
7 : import 'package:the_logger/src/abstract_logger.dart';
8 : import 'package:the_logger/src/db/logger_database.dart';
9 : import 'package:the_logger/src/log_export.dart';
10 : import 'package:the_logger/src/models/models.dart';
11 :
12 : /// Database logger
13 : class DbLogger extends AbstractLogger {
14 : late LoggerDatabase _database;
15 : late final Map<Level, int> _retainStrategy;
16 : int _sessionId = -1;
17 : final List<Future<void>> _pendingOperations = [];
18 :
19 2 : @override
20 : Future<void> init(
21 : Map<Level, int> retainStrategy, [
22 : @visibleForTesting LoggerDatabase? database,
23 : ]) async {
24 2 : WidgetsFlutterBinding.ensureInitialized();
25 :
26 2 : _retainStrategy = retainStrategy;
27 :
28 2 : _database = database ?? LoggerDatabase();
29 : }
30 :
31 2 : @override
32 : Future<String?> sessionStart() async {
33 4 : _sessionId = await _database
34 6 : .into(_database.sessions)
35 4 : .insert(SessionsCompanion.insert());
36 :
37 2 : final cleanupFuture = Future.delayed(
38 : const Duration(milliseconds: 200),
39 2 : _cleanup,
40 : );
41 4 : _pendingOperations.add(cleanupFuture);
42 2 : unawaited(
43 2 : cleanupFuture.whenComplete(
44 6 : () => _pendingOperations.remove(cleanupFuture),
45 : ),
46 : );
47 :
48 4 : return 'new session id: $_sessionId';
49 : }
50 :
51 2 : @override
52 : void write(MaskedLogRecord record) {
53 2 : unawaited(
54 2 : _database
55 6 : .into(_database.records)
56 2 : .insert(
57 2 : RecordsCompanion.insert(
58 4 : sessionId: Value(_sessionId),
59 6 : level: Value(record.level.value),
60 2 : message: Value(
61 5 : shouldMask ? record.maskedMessage : record.message,
62 : ),
63 4 : loggerName: Value(record.loggerName),
64 2 : error: Value(
65 5 : shouldMask ? record.maskedError : record.error?.toString(),
66 : ),
67 2 : stackTrace: Value(
68 2 : shouldMask
69 2 : ? record.maskedStackTrace
70 2 : : record.stackTrace.toString(),
71 : ),
72 6 : time: Value(record.time.microsecondsSinceEpoch),
73 : ),
74 : ),
75 : );
76 : }
77 :
78 : /// Get all logs as strings
79 1 : Future<String> getAllLogsAsString() async {
80 : final list =
81 6 : await (_database.select(_database.records)..orderBy([
82 3 : (t) => OrderingTerm.asc(t.recordTimestamp),
83 : ]))
84 1 : .get();
85 :
86 2 : return list.map((element) => '$element').join('\n');
87 : }
88 :
89 : /// Get all logs as [LogRecord]s (for debug purposes only)
90 0 : Future<List<LogRecord>> getAllLogs() async {
91 0 : final list = await _database
92 0 : .customSelect(
93 : 'SELECT * FROM records ORDER BY record_timestamp ASC',
94 : )
95 0 : .get();
96 :
97 : return list
98 0 : .map((row) => WritableLogRecord.fromMap(row.data.cast()))
99 0 : .toList();
100 : }
101 :
102 : /// Get all logs as maps (for debug purposes only)
103 2 : Future<List<Map<String, Object?>>> getAllLogsAsMaps() async {
104 2 : final list = await _database
105 2 : .customSelect(
106 : 'SELECT * FROM records ORDER BY record_timestamp ASC',
107 : )
108 2 : .get();
109 :
110 10 : return list.map((row) => row.data.cast<String, Object?>()).toList();
111 : }
112 :
113 : /// Write logs to archived JSON, return file path
114 1 : Future<String> writeAllLogsToJson(String filename) async {
115 : if (kIsWeb) {
116 0 : throw UnsupportedError('Log export is not supported on web');
117 : }
118 2 : return writeLogsToFile(_database, filename);
119 : }
120 :
121 2 : Future<void> _cleanup() async {
122 8 : if (_sessionId < 0 || _retainStrategy.isEmpty) return;
123 :
124 6 : final levelList = _retainStrategy.entries.toList()
125 6 : ..sort((a, b) => b.key.compareTo(a.key));
126 :
127 : Level? nextLevel;
128 2 : var retain = _sessionId;
129 : final leveListWithBouds = levelList
130 2 : .map(
131 2 : (e) {
132 2 : final ret = _LevelBound(
133 2 : level: e.key,
134 : nextLevel: nextLevel,
135 2 : sessionId: e.value,
136 : );
137 2 : nextLevel = e.key;
138 :
139 : return ret;
140 : },
141 : )
142 2 : .toList()
143 2 : .reversed
144 4 : .map((e) {
145 4 : retain -= e.sessionId;
146 :
147 2 : return e.copyWith(sessionId: retain);
148 : })
149 2 : .toList();
150 :
151 4 : final where = leveListWithBouds.fold('', (previousValue, element) {
152 3 : final previous = previousValue.isNotEmpty ? '$previousValue OR ' : '';
153 2 : final next = element.nextLevel == null
154 : ? ''
155 3 : : 'AND level < ${element.nextLevel!.value}';
156 :
157 : return '''
158 6 : $previous(session_id < ${element.sessionId} AND level >= ${element.level.value} $next)
159 2 : ''';
160 : });
161 :
162 : final query =
163 : '''
164 : DELETE FROM records WHERE $where;
165 2 : ''';
166 :
167 4 : await _database.customStatement(query);
168 : }
169 :
170 : /// Clear logs (for debug purposes only)
171 2 : Future<void> clearAllLogs() async {
172 10 : await _database.delete(_database.records).go();
173 : }
174 :
175 : /// Whether to mask logs
176 : bool shouldMask = true;
177 :
178 2 : @override
179 : Future<void> dispose() async {
180 4 : await Future.wait(_pendingOperations);
181 4 : _pendingOperations.clear();
182 4 : await _database.close();
183 : }
184 : }
185 :
186 : class _LevelBound {
187 2 : _LevelBound({
188 : required this.level,
189 : required this.nextLevel,
190 : required this.sessionId,
191 : });
192 : final Level level;
193 : final Level? nextLevel;
194 : final int sessionId;
195 :
196 4 : _LevelBound copyWith({int? sessionId}) => _LevelBound(
197 2 : level: level,
198 2 : nextLevel: nextLevel,
199 0 : sessionId: sessionId ?? this.sessionId,
200 : );
201 : }
|