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

Generated by: LCOV version 2.1-1