LCOV - code coverage report
Current view: top level - src - db_logger.dart Coverage Total Hit
Test: filtered_lcov.info Lines: 95.3 % 107 102
Test Date: 2026-03-23 09:40:19 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:async';
       2              : import 'dart:convert';
       3              : import 'dart:io';
       4              : 
       5              : import 'package:flutter/widgets.dart';
       6              : import 'package:logging/logging.dart';
       7              : import 'package:path/path.dart';
       8              : import 'package:path_provider/path_provider.dart';
       9              : import 'package:sqflite/sqflite.dart';
      10              : import 'package:the_logger/src/abstract_logger.dart';
      11              : import 'package:the_logger/src/models/models.dart';
      12              : 
      13              : /// Database logger
      14              : class DbLogger extends AbstractLogger {
      15              :   late final Database _database;
      16              :   late final Map<Level, int> _retainStrategy;
      17              :   int _sessionId = -1;
      18              :   final List<Future<void>> _pendingOperations = [];
      19              : 
      20            2 :   @override
      21              :   Future<void> init(Map<Level, int> retainStrategy) async {
      22            2 :     WidgetsFlutterBinding.ensureInitialized();
      23              : 
      24            2 :     _retainStrategy = retainStrategy;
      25              : 
      26            4 :     _database = await openDatabase(
      27            2 :       join(
      28            2 :         await getDatabasesPath(),
      29              :         'logs.db',
      30              :       ),
      31            2 :       onCreate: _onCreate,
      32              :       version: 1,
      33              :     );
      34              :   }
      35              : 
      36            1 :   Future<void> _onCreate(Database db, int _) async {
      37            1 :     await db.execute(
      38              :       '''
      39              :         CREATE TABLE sessions (
      40              :           id INTEGER PRIMARY KEY AUTOINCREMENT,
      41              :           timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
      42              :         );
      43              :       ''',
      44              :     );
      45            1 :     await db.execute(
      46              :       '''
      47              :         CREATE TABLE records (
      48              :           id INTEGER PRIMARY KEY AUTOINCREMENT,
      49              :           record_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
      50              :           session_id INTEGER,
      51              :           level INTEGER,
      52              :           message TEXT,
      53              :           logger_name TEXT,
      54              :           error TEXT,
      55              :           stack_trace TEXT,
      56              :           time TIMESTAMP
      57              :         );
      58              :       ''',
      59              :     );
      60              :   }
      61              : 
      62            2 :   @override
      63              :   Future<String?> sessionStart() async {
      64            6 :     await _database.transaction((txn) async {
      65            4 :       _sessionId = await txn.rawInsert(
      66              :         '''INSERT INTO sessions DEFAULT VALUES;''',
      67              :       );
      68              :     });
      69              : 
      70            2 :     final cleanupFuture = Future.delayed(
      71              :       const Duration(milliseconds: 200),
      72            2 :       _cleanup,
      73              :     );
      74            4 :     _pendingOperations.add(cleanupFuture);
      75            2 :     unawaited(
      76            2 :       cleanupFuture.whenComplete(
      77            6 :         () => _pendingOperations.remove(cleanupFuture),
      78              :       ),
      79              :     );
      80              : 
      81            4 :     return 'new session id: $_sessionId';
      82              :   }
      83              : 
      84            2 :   @override
      85              :   void write(MaskedLogRecord record) {
      86            4 :     if (!_database.isOpen) return;
      87              : 
      88            2 :     unawaited(
      89            4 :       _database.insert(
      90              :         'records',
      91            2 :         record.toMap(
      92            2 :           sessionId: _sessionId,
      93            2 :           mask: shouldMask,
      94              :         ),
      95              :       ),
      96              :     );
      97              :   }
      98              : 
      99              :   /// Get all logs as strings
     100            1 :   Future<String> getAllLogsAsString() async {
     101            2 :     if (!_database.isOpen) return '';
     102              : 
     103            2 :     final list = await _database.rawQuery(
     104              :       '''
     105              :         SELECT * FROM records ORDER BY record_timestamp ASC
     106              :       ''',
     107              :     );
     108              : 
     109            2 :     return list.map((element) => '$element').join('\n');
     110              :   }
     111              : 
     112              :   /// Get all logs as [LogRecord]s (for debug purposes only)
     113            0 :   Future<List<LogRecord>> getAllLogs() async {
     114            0 :     if (!_database.isOpen) return [];
     115              : 
     116            0 :     final list = await _database.rawQuery(
     117              :       '''
     118              :         SELECT * FROM records ORDER BY record_timestamp ASC
     119              :       ''',
     120              :     );
     121              : 
     122            0 :     return list.map(WritableLogRecord.fromMap).toList();
     123              :   }
     124              : 
     125              :   /// Get all logs as maps (for debug purposes only)
     126            2 :   Future<List<Map<String, Object?>>> getAllLogsAsMaps() async {
     127            4 :     if (!_database.isOpen) return [];
     128              : 
     129            4 :     return _database.rawQuery(
     130              :       '''
     131              :         SELECT * FROM records ORDER BY record_timestamp ASC
     132              :       ''',
     133              :     );
     134              :   }
     135              : 
     136              :   /// Write logs to archived JSON, return file path
     137            1 :   Future<String> writeAllLogsToJson(String filename) async {
     138            2 :     if (!_database.isOpen) return 'Database is not ready';
     139              : 
     140            2 :     final cursor = await _database.rawQueryCursor(
     141              :       '''
     142              :         SELECT
     143              :           logger_name,
     144              :           id,
     145              :           record_timestamp,
     146              :           session_id,
     147              :           level,
     148              :           message,
     149              :           error,
     150              :           stack_trace,
     151              :           time
     152              :         FROM records ORDER BY record_timestamp ASC
     153              :       ''',
     154            1 :       [],
     155              :     );
     156              : 
     157            1 :     final fileAcrhive = _FileAcrhive();
     158            1 :     final filePath = await fileAcrhive.open(filename);
     159              : 
     160            1 :     await fileAcrhive.writeString('{\n  "logs": [\n');
     161              : 
     162              :     try {
     163              :       var isFirst = true;
     164            1 :       while (await cursor.moveNext()) {
     165            1 :         final row = cursor.current;
     166            1 :         final string = json.encode(row);
     167              :         final comma = isFirst ? '' : ',\n';
     168            2 :         await fileAcrhive.writeString('$comma    $string');
     169              :         isFirst = false;
     170              :       }
     171              :     } finally {
     172            1 :       await cursor.close();
     173              :     }
     174              : 
     175            1 :     await fileAcrhive.writeString('\n  ]\n}');
     176              : 
     177            1 :     await fileAcrhive.close();
     178              : 
     179              :     return filePath;
     180              :   }
     181              : 
     182            2 :   Future<void> _cleanup() async {
     183           12 :     if (!_database.isOpen || _sessionId < 0 || _retainStrategy.isEmpty) return;
     184              : 
     185            6 :     final levelList = _retainStrategy.entries.toList()
     186            6 :       ..sort((a, b) => b.key.compareTo(a.key));
     187              : 
     188              :     Level? nextLevel;
     189            2 :     var retain = _sessionId;
     190              :     final leveListWithBouds = levelList
     191            2 :         .map(
     192            2 :           (e) {
     193            2 :             final ret = _LevelBound(
     194            2 :               level: e.key,
     195              :               nextLevel: nextLevel,
     196            2 :               sessionId: e.value,
     197              :             );
     198            2 :             nextLevel = e.key;
     199              : 
     200              :             return ret;
     201              :           },
     202              :         )
     203            2 :         .toList()
     204            2 :         .reversed
     205            4 :         .map((e) {
     206            4 :           retain -= e.sessionId;
     207              : 
     208            2 :           return e.copyWith(sessionId: retain);
     209              :         })
     210            2 :         .toList();
     211              : 
     212            4 :     final where = leveListWithBouds.fold('', (previousValue, element) {
     213            3 :       final previous = previousValue.isNotEmpty ? '$previousValue OR ' : '';
     214            2 :       final next = element.nextLevel == null
     215              :           ? ''
     216            3 :           : 'AND level < ${element.nextLevel!.value}';
     217              : 
     218              :       return '''
     219            6 :         $previous(session_id < ${element.sessionId} AND level >= ${element.level.value} $next)
     220            2 :         ''';
     221              :     });
     222              : 
     223              :     final query =
     224              :         '''
     225              :       DELETE FROM records WHERE $where;
     226            2 :     ''';
     227              : 
     228            4 :     await _database.execute(query);
     229              :   }
     230              : 
     231              :   /// Clear logs (for debug purposes only)
     232            2 :   Future<void> clearAllLogs() async {
     233              :     const query = '''
     234              :       DELETE FROM records;
     235              :     ''';
     236              : 
     237            4 :     await _database.execute(query);
     238              :   }
     239              : 
     240              :   /// Whether to mask logs
     241              :   bool shouldMask = true;
     242              : 
     243            2 :   @override
     244              :   Future<void> dispose() async {
     245            4 :     await Future.wait(_pendingOperations);
     246            4 :     _pendingOperations.clear();
     247            4 :     if (_database.isOpen) {
     248            4 :       await _database.close();
     249              :     }
     250              :   }
     251              : }
     252              : 
     253              : class _LevelBound {
     254            2 :   _LevelBound({
     255              :     required this.level,
     256              :     required this.nextLevel,
     257              :     required this.sessionId,
     258              :   });
     259              :   final Level level;
     260              :   final Level? nextLevel;
     261              :   final int sessionId;
     262              : 
     263            4 :   _LevelBound copyWith({int? sessionId}) => _LevelBound(
     264            2 :     level: level,
     265            2 :     nextLevel: nextLevel,
     266            0 :     sessionId: sessionId ?? this.sessionId,
     267              :   );
     268              : }
     269              : 
     270              : class _FileAcrhive {
     271            1 :   _FileAcrhive();
     272              : 
     273              :   IOSink? _file;
     274              :   final ZLibEncoder _encoder = gzip.encoder;
     275              :   Sink<List<int>>? _sink;
     276              : 
     277            1 :   Future<String> open(String filename) async {
     278            1 :     await close();
     279              : 
     280            1 :     final filePath = join(
     281            2 :       (await getTemporaryDirectory()).path,
     282            1 :       '$filename.gzip',
     283              :     );
     284              : 
     285            1 :     final file = File(filePath);
     286            2 :     await file.parent.create(recursive: true);
     287              : 
     288              :     try {
     289            1 :       await file.delete();
     290            1 :     } on FileSystemException catch (_) {}
     291              : 
     292            2 :     _file = file.openWrite();
     293              : 
     294            4 :     _sink = _encoder.startChunkedConversion(_file!);
     295              : 
     296              :     return filePath;
     297              :   }
     298              : 
     299            1 :   Future<void> close() async {
     300            2 :     _sink?.close();
     301            2 :     await _file?.flush();
     302              :     // TODO(nesquikm): idk, but tests fails when this is awaited
     303            3 :     unawaited(_file?.close());
     304              :   }
     305              : 
     306            1 :   Future<void> writeString(String string) async {
     307            3 :     _sink!.add(utf8.encode(string));
     308              :   }
     309              : }
        

Generated by: LCOV version 2.4-0